From df71f1de954984db6e9dc294f95b6e79bc071dc9 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Mon, 20 Jul 2026 13:21:14 +0200 Subject: [PATCH] Load sidebar entries automatically in 200-item batches. --- README.md | 2 +- crates/bds-ui/src/app.rs | 12 ++++-- crates/bds-ui/src/app/search.rs | 61 ++++++++++++++++++++++++++++-- crates/bds-ui/src/views/sidebar.rs | 58 ++++++++-------------------- locales/ui/de.ftl | 1 - locales/ui/en.ftl | 1 - locales/ui/es.ftl | 1 - locales/ui/fr.ftl | 1 - locales/ui/it.ftl | 1 - specs/sidebar_views.allium | 14 ++++++- 10 files changed, 94 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index d94d7ff..78e04e2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The project is under active development. Core blogging workflows are broadly ava ## Available Features -- Native Iced desktop workspace with localized menus, tabs, sidebars, dialogs, tasks, and embedded Wry previews. +- Native Iced desktop workspace with localized menus, tabs, automatically paged post/media sidebars, dialogs, tasks, and embedded Wry previews. - Post and translation authoring with draft/published lifecycle, metadata, tags, categories, links, media, and batch gallery-image import. - Media import, thumbnails, metadata translations, filters, validation, and post assignment. - WordPress WXR migration with saved analyses, HTML-to-Markdown and shortcode conversion, conflict/taxonomy review, recoverable 500-item execution batches, media-parent linking, progress reporting, and optional AI-assisted taxonomy mapping. diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index b435fb2..6b610fa 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -347,8 +347,7 @@ pub enum Message { items: Vec, thumbs: HashMap>, }, - LoadMorePosts, - LoadMoreMedia, + SidebarScrolled(f32), CreatePost, CreatePage, @@ -886,6 +885,8 @@ pub struct BdsApp { sidebar_media_thumbs: HashMap>, sidebar_posts_has_more: bool, sidebar_media_has_more: bool, + sidebar_posts_loading_more: bool, + sidebar_media_loading_more: bool, // Sidebar filters (per sidebar_views.allium PostsView / MediaView) post_filter: PostFilter, @@ -1089,6 +1090,8 @@ impl BdsApp { sidebar_media_thumbs: HashMap::new(), sidebar_posts_has_more: false, sidebar_media_has_more: false, + sidebar_posts_loading_more: false, + sidebar_media_loading_more: false, post_filter: PostFilter::default(), page_filter: PostFilter::default(), media_filter: MediaFilter::default(), @@ -1180,6 +1183,8 @@ impl BdsApp { sidebar_media_thumbs: HashMap::new(), sidebar_posts_has_more: false, sidebar_media_has_more: false, + sidebar_posts_loading_more: false, + sidebar_media_loading_more: false, post_filter: PostFilter::default(), page_filter: PostFilter::default(), media_filter: MediaFilter::default(), @@ -2702,8 +2707,7 @@ impl BdsApp { | Message::SidebarMediaLoaded { .. } | Message::SidebarPostsAppended(_) | Message::SidebarMediaAppended { .. } - | Message::LoadMorePosts - | Message::LoadMoreMedia) => self.handle_sidebar_message(message), + | Message::SidebarScrolled(_)) => self.handle_sidebar_message(message), // ── Modal ── Message::ShowModal(state) => { diff --git a/crates/bds-ui/src/app/search.rs b/crates/bds-ui/src/app/search.rs index d8b3267..fc83d5e 100644 --- a/crates/bds-ui/src/app/search.rs +++ b/crates/bds-ui/src/app/search.rs @@ -3,8 +3,12 @@ use chrono::Datelike; impl BdsApp { /// Number of items to load per sidebar page. - /// Matches the TypeScript app's limit of 500 for initial load. - pub(super) const SIDEBAR_PAGE_SIZE: i64 = 500; + pub(super) const SIDEBAR_PAGE_SIZE: i64 = 200; + const SIDEBAR_AUTO_LOAD_THRESHOLD: f32 = 0.9; + + fn should_auto_load_more(relative_y: f32, has_more: bool, loading: bool) -> bool { + relative_y >= Self::SIDEBAR_AUTO_LOAD_THRESHOLD && has_more && !loading + } /// Refresh only sidebar posts using current filter state (async). pub(super) fn refresh_sidebar_posts(&mut self) -> Task { @@ -94,9 +98,13 @@ impl BdsApp { /// Load more posts (append to existing sidebar data). pub(super) fn load_more_sidebar_posts(&mut self) -> Task { + if self.sidebar_posts_loading_more || !self.sidebar_posts_has_more { + return Task::none(); + } let (Some(_), Some(project)) = (&self.db, &self.active_project) else { return Task::none(); }; + self.sidebar_posts_loading_more = true; let db_path = self.db_path.clone(); let project_id = project.id.clone(); @@ -378,9 +386,13 @@ impl BdsApp { /// Load more media (append to existing sidebar data). pub(super) fn load_more_sidebar_media(&mut self) -> Task { + if self.sidebar_media_loading_more || !self.sidebar_media_has_more { + return Task::none(); + } let (Some(_), Some(project)) = (&self.db, &self.active_project) else { return Task::none(); }; + self.sidebar_media_loading_more = true; let db_path = self.db_path.clone(); let project_id = project.id.clone(); @@ -617,12 +629,14 @@ impl BdsApp { // ── Async sidebar data ── Message::SidebarPostsLoaded(mut posts) => { + self.sidebar_posts_loading_more = false; self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize; posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize); self.sidebar_posts = posts; Task::none() } Message::SidebarMediaLoaded { mut items, thumbs } => { + self.sidebar_media_loading_more = false; self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize; items.truncate(Self::SIDEBAR_PAGE_SIZE as usize); self.sidebar_media = items; @@ -630,21 +644,60 @@ impl BdsApp { Task::none() } Message::SidebarPostsAppended(mut posts) => { + self.sidebar_posts_loading_more = false; self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize; posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize); self.sidebar_posts.extend(posts); Task::none() } Message::SidebarMediaAppended { mut items, thumbs } => { + self.sidebar_media_loading_more = false; self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize; items.truncate(Self::SIDEBAR_PAGE_SIZE as usize); self.sidebar_media.extend(items); self.sidebar_media_thumbs.extend(thumbs); Task::none() } - Message::LoadMorePosts => self.load_more_sidebar_posts(), - Message::LoadMoreMedia => self.load_more_sidebar_media(), + Message::SidebarScrolled(relative_y) => match self.sidebar_view { + SidebarView::Posts | SidebarView::Pages + if Self::should_auto_load_more( + relative_y, + self.sidebar_posts_has_more, + self.sidebar_posts_loading_more, + ) => + { + self.load_more_sidebar_posts() + } + SidebarView::Media + if Self::should_auto_load_more( + relative_y, + self.sidebar_media_has_more, + self.sidebar_media_loading_more, + ) => + { + self.load_more_sidebar_media() + } + _ => Task::none(), + }, _ => unreachable!("non-sidebar message routed to sidebar handler"), } } } + +#[cfg(test)] +mod tests { + use super::BdsApp; + + #[test] + fn sidebar_page_size_is_200() { + assert_eq!(BdsApp::SIDEBAR_PAGE_SIZE, 200); + } + + #[test] + fn sidebar_auto_loads_once_near_the_end() { + assert!(!BdsApp::should_auto_load_more(0.89, true, false)); + assert!(BdsApp::should_auto_load_more(0.9, true, false)); + assert!(!BdsApp::should_auto_load_more(1.0, false, false)); + assert!(!BdsApp::should_auto_load_more(1.0, true, true)); + } +} diff --git a/crates/bds-ui/src/views/sidebar.rs b/crates/bds-ui/src/views/sidebar.rs index ff47d1b..eae653c 100644 --- a/crates/bds-ui/src/views/sidebar.rs +++ b/crates/bds-ui/src/views/sidebar.rs @@ -825,23 +825,6 @@ pub fn view( top_items.push(make_post_item(p)); } } - - // "Load More" button - if posts_has_more { - top_items.push(Space::with_height(4.0).into()); - top_items.push( - button( - text(t(locale, "sidebar.loadMore")) - .size(11) - .shaping(Shaping::Advanced), - ) - .on_press(Message::LoadMorePosts) - .padding([4, 8]) - .width(Length::Fill) - .style(filter_button_style) - .into(), - ); - } } iced::widget::Column::with_children(top_items) @@ -980,23 +963,6 @@ pub fn view( }) .collect(); top_items.extend(items); - - // "Load More" button - if media_has_more { - top_items.push(Space::with_height(4.0).into()); - top_items.push( - button( - text(t(locale, "sidebar.loadMore")) - .size(11) - .shaping(Shaping::Advanced), - ) - .on_press(Message::LoadMoreMedia) - .padding([4, 8]) - .width(Length::Fill) - .style(filter_button_style) - .into(), - ); - } } iced::widget::Column::with_children(top_items) @@ -1254,15 +1220,21 @@ pub fn view( .padding(12); // layout.allium: sidebar width is resizable, passed as parameter - container( - scrollable(content) - .direction(scrollable::Direction::Vertical(inputs::compact_scrollbar())) - .style(inputs::scrollable_style), - ) - .width(Length::Fixed(width)) - .height(Length::Fill) - .style(sidebar_style) - .into() + let mut scrolling = scrollable(content) + .direction(scrollable::Direction::Vertical(inputs::compact_scrollbar())) + .style(inputs::scrollable_style); + if matches!(sidebar_view, SidebarView::Posts | SidebarView::Pages) && posts_has_more + || sidebar_view == SidebarView::Media && media_has_more + { + scrolling = + scrolling.on_scroll(|viewport| Message::SidebarScrolled(viewport.relative_offset().y)); + } + + container(scrolling) + .width(Length::Fixed(width)) + .height(Length::Fill) + .style(sidebar_style) + .into() } #[cfg(test)] diff --git a/locales/ui/de.ftl b/locales/ui/de.ftl index 239a07c..a8c2d07 100644 --- a/locales/ui/de.ftl +++ b/locales/ui/de.ftl @@ -259,7 +259,6 @@ sidebar-tagsHeader = Schlagwörter sidebar-drafts = Entwürfe sidebar-published = Veröffentlicht sidebar-archived = Archiviert -sidebar-loadMore = Mehr laden… language-en = Englisch language-de = Deutsch language-fr = Französisch diff --git a/locales/ui/en.ftl b/locales/ui/en.ftl index 039c8a6..c6861f1 100644 --- a/locales/ui/en.ftl +++ b/locales/ui/en.ftl @@ -259,7 +259,6 @@ sidebar-tagsHeader = Tags sidebar-drafts = Drafts sidebar-published = Published sidebar-archived = Archived -sidebar-loadMore = Load More… language-en = English language-de = German language-fr = French diff --git a/locales/ui/es.ftl b/locales/ui/es.ftl index 084f11f..16bc549 100644 --- a/locales/ui/es.ftl +++ b/locales/ui/es.ftl @@ -259,7 +259,6 @@ sidebar-tagsHeader = Etiquetas sidebar-drafts = Borradores sidebar-published = Publicados sidebar-archived = Archivados -sidebar-loadMore = Cargar más… language-en = Inglés language-de = Alemán language-fr = Francés diff --git a/locales/ui/fr.ftl b/locales/ui/fr.ftl index 38c0cac..07e3a1c 100644 --- a/locales/ui/fr.ftl +++ b/locales/ui/fr.ftl @@ -259,7 +259,6 @@ sidebar-tagsHeader = Étiquettes sidebar-drafts = Brouillons sidebar-published = Publiés sidebar-archived = Archivés -sidebar-loadMore = Charger plus… language-en = Anglais language-de = Allemand language-fr = Français diff --git a/locales/ui/it.ftl b/locales/ui/it.ftl index ad843ee..c465b2a 100644 --- a/locales/ui/it.ftl +++ b/locales/ui/it.ftl @@ -259,7 +259,6 @@ sidebar-tagsHeader = Tag sidebar-drafts = Bozze sidebar-published = Pubblicati sidebar-archived = Archiviati -sidebar-loadMore = Carica di più… language-en = Inglese language-de = Tedesco language-fr = Francese diff --git a/specs/sidebar_views.allium b/specs/sidebar_views.allium index 00df891..0c9e12e 100644 --- a/specs/sidebar_views.allium +++ b/specs/sidebar_views.allium @@ -26,6 +26,8 @@ use "./i18n.allium" as i18n config { default_sidebar_view: String = "posts" + sidebar_page_size: Integer = 200 + sidebar_auto_load_threshold: Decimal = 0.9 } -- ─── Shared patterns ───────────────────────────────────────── @@ -127,7 +129,7 @@ value PostsView { draft_section: List published_section: List archived_section: List - has_more: Boolean -- pagination, 500 per batch + has_more: Boolean -- pagination, config.sidebar_page_size per batch } surface PostsViewSurface { @@ -144,6 +146,10 @@ surface PostsViewSurface { view.published_section.count view.archived_section.count view.has_more + + @guarantee AutomaticPagination + -- When scrolling reaches config.sidebar_auto_load_threshold and more items exist, + -- the next batch loads in the background. At most one batch loads at a time. } -- Drafts section always shows all drafts regardless of filters. @@ -216,6 +222,7 @@ value MediaView { calendar_filter: CalendarFilter? tag_filter: List -- tags only, no categories for media grid: List -- grid layout (not list) + has_more: Boolean } surface MediaViewSurface { @@ -227,6 +234,11 @@ surface MediaViewSurface { view.calendar_filter when view.calendar_filter != null view.tag_filter view.grid.count + view.has_more + + @guarantee AutomaticPagination + -- When scrolling reaches config.sidebar_auto_load_threshold and more items exist, + -- the next batch loads in the background. At most one batch loads at a time. } value MediaGridItem {