fix: windowed pagination — show 1…current±2…last instead of all pages

This commit is contained in:
2026-05-08 13:47:34 +02:00
parent 4ea5f4cecf
commit 06b3761401
4 changed files with 59 additions and 30 deletions

View File

@@ -9,6 +9,35 @@ use domain::models::{
collections::Paginated, collections::Paginated,
}; };
struct PageItem {
number: u32,
is_current: bool,
is_ellipsis: bool,
}
fn build_page_items(total_pages: u32, current_page: u32) -> Vec<PageItem> {
if total_pages <= 1 {
return vec![];
}
let mut set = std::collections::BTreeSet::new();
set.insert(0u32);
set.insert(total_pages - 1);
let start = current_page.saturating_sub(2);
let end = (current_page + 2).min(total_pages - 1);
for p in start..=end {
set.insert(p);
}
let pages: Vec<u32> = set.into_iter().collect();
let mut items = Vec::new();
for (i, &p) in pages.iter().enumerate() {
if i > 0 && p > pages[i - 1] + 1 {
items.push(PageItem { number: 0, is_current: false, is_ellipsis: true });
}
items.push(PageItem { number: p, is_current: p == current_page, is_ellipsis: false });
}
items
}
#[derive(Template)] #[derive(Template)]
#[template(path = "diary.html")] #[template(path = "diary.html")]
struct DiaryTemplate<'a> { struct DiaryTemplate<'a> {
@@ -17,8 +46,7 @@ struct DiaryTemplate<'a> {
limit: u32, limit: u32,
has_more: bool, has_more: bool,
ctx: &'a HtmlPageContext, ctx: &'a HtmlPageContext,
total_pages: u32, page_items: Vec<PageItem>,
current_page: u32,
} }
#[derive(Template)] #[derive(Template)]
@@ -50,8 +78,7 @@ struct ActivityFeedTemplate<'a> {
limit: u32, limit: u32,
has_more: bool, has_more: bool,
ctx: &'a HtmlPageContext, ctx: &'a HtmlPageContext,
total_pages: u32, page_items: Vec<PageItem>,
current_page: u32,
} }
#[derive(Template)] #[derive(Template)]
@@ -82,8 +109,7 @@ struct ProfileTemplate<'a> {
trends: Option<&'a UserTrends>, trends: Option<&'a UserTrends>,
monthly_rating_rows: Vec<MonthlyRatingRow<'a>>, monthly_rating_rows: Vec<MonthlyRatingRow<'a>>,
heatmap: Vec<HeatmapCell>, heatmap: Vec<HeatmapCell>,
total_pages: u32, page_items: Vec<PageItem>,
current_page: u32,
} }
struct HeatmapCell { struct HeatmapCell {
@@ -148,9 +174,8 @@ impl HtmlRenderer for AskamaHtmlRenderer {
fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String> { fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String> {
let has_more = (data.offset + data.limit) < data.total_count as u32; let has_more = (data.offset + data.limit) < data.total_count as u32;
let (total_pages, current_page) = if data.limit > 0 { let (total_pages, current_page) = if data.limit > 0 {
let total_pages = ((data.total_count + data.limit as u64 - 1) / data.limit as u64) as u32; let tp = ((data.total_count + data.limit as u64 - 1) / data.limit as u64) as u32;
let current_page = data.offset / data.limit; (tp, data.offset / data.limit)
(total_pages, current_page)
} else { } else {
(0, 0) (0, 0)
}; };
@@ -160,8 +185,7 @@ impl HtmlRenderer for AskamaHtmlRenderer {
limit: data.limit, limit: data.limit,
has_more, has_more,
ctx: &ctx, ctx: &ctx,
total_pages, page_items: build_page_items(total_pages, current_page),
current_page,
} }
.render() .render()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@@ -195,9 +219,10 @@ impl HtmlRenderer for AskamaHtmlRenderer {
} }
fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String> { fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String> {
let total_count = data.entries.total_count;
let limit = data.limit; let limit = data.limit;
let total_pages = ((total_count + limit as u64 - 1) / limit as u64) as u32; let total_pages = if limit > 0 {
((data.entries.total_count + limit as u64 - 1) / limit as u64) as u32
} else { 0 };
let current_page = if limit > 0 { data.current_offset / limit } else { 0 }; let current_page = if limit > 0 { data.current_offset / limit } else { 0 };
ActivityFeedTemplate { ActivityFeedTemplate {
entries: &data.entries.items, entries: &data.entries.items,
@@ -205,8 +230,7 @@ impl HtmlRenderer for AskamaHtmlRenderer {
limit, limit,
has_more: data.has_more, has_more: data.has_more,
ctx: &data.ctx, ctx: &data.ctx,
total_pages, page_items: build_page_items(total_pages, current_page),
current_page,
} }
.render() .render()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@@ -234,7 +258,7 @@ impl HtmlRenderer for AskamaHtmlRenderer {
}).collect()) }).collect())
.unwrap_or_default(); .unwrap_or_default();
let total_pages = data.entries.as_ref() let total_pages = data.entries.as_ref()
.map(|e| ((e.total_count + e.limit as u64 - 1) / e.limit as u64) as u32) .map(|e| if e.limit > 0 { ((e.total_count + e.limit as u64 - 1) / e.limit as u64) as u32 } else { 0 })
.unwrap_or(0); .unwrap_or(0);
let current_page = if data.limit > 0 { data.current_offset / data.limit } else { 0 }; let current_page = if data.limit > 0 { data.current_offset / data.limit } else { 0 };
ProfileTemplate { ProfileTemplate {
@@ -251,8 +275,7 @@ impl HtmlRenderer for AskamaHtmlRenderer {
trends: data.trends.as_ref(), trends: data.trends.as_ref(),
monthly_rating_rows, monthly_rating_rows,
heatmap, heatmap,
total_pages, page_items: build_page_items(total_pages, current_page),
current_page,
} }
.render() .render()
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())

View File

@@ -44,11 +44,13 @@
{% if current_offset >= limit %} {% if current_offset >= limit %}
<a href="/?offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a> <a href="/?offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a>
{% endif %} {% endif %}
{% for p in (0..total_pages) %} {% for item in page_items %}
{% if p == current_page %} {% if item.is_ellipsis %}
<span class="page-num current">{{ p + 1 }}</span> <span class="page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="page-num current">{{ item.number + 1 }}</span>
{% else %} {% else %}
<a href="/?offset={{ p * limit }}" class="page-num">{{ p + 1 }}</a> <a href="/?offset={{ item.number * limit }}" class="page-num">{{ item.number + 1 }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if has_more %} {% if has_more %}

View File

@@ -44,11 +44,13 @@
{% if current_offset > 0 %} {% if current_offset > 0 %}
<a href="/?offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a> <a href="/?offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a>
{% endif %} {% endif %}
{% for p in (0..total_pages) %} {% for item in page_items %}
{% if p == current_page %} {% if item.is_ellipsis %}
<span class="page-num current">{{ p + 1 }}</span> <span class="page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="page-num current">{{ item.number + 1 }}</span>
{% else %} {% else %}
<a href="/?offset={{ p * limit }}" class="page-num">{{ p + 1 }}</a> <a href="/?offset={{ item.number * limit }}" class="page-num">{{ item.number + 1 }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if has_more %} {% if has_more %}

View File

@@ -155,11 +155,13 @@
{% if current_offset >= limit %} {% if current_offset >= limit %}
<a href="?view={{ view }}&offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a> <a href="?view={{ view }}&offset={{ current_offset - limit }}" class="page-nav">&larr; Prev</a>
{% endif %} {% endif %}
{% for p in (0..total_pages) %} {% for item in page_items %}
{% if p == current_page %} {% if item.is_ellipsis %}
<span class="page-num current">{{ p + 1 }}</span> <span class="page-ellipsis">&hellip;</span>
{% elif item.is_current %}
<span class="page-num current">{{ item.number + 1 }}</span>
{% else %} {% else %}
<a href="?view={{ view }}&offset={{ p * limit }}" class="page-num">{{ p + 1 }}</a> <a href="?view={{ view }}&offset={{ item.number * limit }}" class="page-num">{{ item.number + 1 }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if has_more %} {% if has_more %}