Load sidebar entries automatically in 200-item batches.

This commit is contained in:
2026-07-20 13:21:14 +02:00
parent 7887d02e0b
commit df71f1de95
10 changed files with 94 additions and 58 deletions

View File

@@ -6,7 +6,7 @@ The project is under active development. Core blogging workflows are broadly ava
## Available Features ## 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. - 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. - 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. - 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.

View File

@@ -347,8 +347,7 @@ pub enum Message {
items: Vec<Media>, items: Vec<Media>,
thumbs: HashMap<String, Option<std::path::PathBuf>>, thumbs: HashMap<String, Option<std::path::PathBuf>>,
}, },
LoadMorePosts, SidebarScrolled(f32),
LoadMoreMedia,
CreatePost, CreatePost,
CreatePage, CreatePage,
@@ -886,6 +885,8 @@ pub struct BdsApp {
sidebar_media_thumbs: HashMap<String, Option<std::path::PathBuf>>, sidebar_media_thumbs: HashMap<String, Option<std::path::PathBuf>>,
sidebar_posts_has_more: bool, sidebar_posts_has_more: bool,
sidebar_media_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) // Sidebar filters (per sidebar_views.allium PostsView / MediaView)
post_filter: PostFilter, post_filter: PostFilter,
@@ -1089,6 +1090,8 @@ impl BdsApp {
sidebar_media_thumbs: HashMap::new(), sidebar_media_thumbs: HashMap::new(),
sidebar_posts_has_more: false, sidebar_posts_has_more: false,
sidebar_media_has_more: false, sidebar_media_has_more: false,
sidebar_posts_loading_more: false,
sidebar_media_loading_more: false,
post_filter: PostFilter::default(), post_filter: PostFilter::default(),
page_filter: PostFilter::default(), page_filter: PostFilter::default(),
media_filter: MediaFilter::default(), media_filter: MediaFilter::default(),
@@ -1180,6 +1183,8 @@ impl BdsApp {
sidebar_media_thumbs: HashMap::new(), sidebar_media_thumbs: HashMap::new(),
sidebar_posts_has_more: false, sidebar_posts_has_more: false,
sidebar_media_has_more: false, sidebar_media_has_more: false,
sidebar_posts_loading_more: false,
sidebar_media_loading_more: false,
post_filter: PostFilter::default(), post_filter: PostFilter::default(),
page_filter: PostFilter::default(), page_filter: PostFilter::default(),
media_filter: MediaFilter::default(), media_filter: MediaFilter::default(),
@@ -2702,8 +2707,7 @@ impl BdsApp {
| Message::SidebarMediaLoaded { .. } | Message::SidebarMediaLoaded { .. }
| Message::SidebarPostsAppended(_) | Message::SidebarPostsAppended(_)
| Message::SidebarMediaAppended { .. } | Message::SidebarMediaAppended { .. }
| Message::LoadMorePosts | Message::SidebarScrolled(_)) => self.handle_sidebar_message(message),
| Message::LoadMoreMedia) => self.handle_sidebar_message(message),
// ── Modal ── // ── Modal ──
Message::ShowModal(state) => { Message::ShowModal(state) => {

View File

@@ -3,8 +3,12 @@ use chrono::Datelike;
impl BdsApp { impl BdsApp {
/// Number of items to load per sidebar page. /// 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 = 200;
pub(super) const SIDEBAR_PAGE_SIZE: i64 = 500; 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). /// Refresh only sidebar posts using current filter state (async).
pub(super) fn refresh_sidebar_posts(&mut self) -> Task<Message> { pub(super) fn refresh_sidebar_posts(&mut self) -> Task<Message> {
@@ -94,9 +98,13 @@ impl BdsApp {
/// Load more posts (append to existing sidebar data). /// Load more posts (append to existing sidebar data).
pub(super) fn load_more_sidebar_posts(&mut self) -> Task<Message> { pub(super) fn load_more_sidebar_posts(&mut self) -> Task<Message> {
if self.sidebar_posts_loading_more || !self.sidebar_posts_has_more {
return Task::none();
}
let (Some(_), Some(project)) = (&self.db, &self.active_project) else { let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
return Task::none(); return Task::none();
}; };
self.sidebar_posts_loading_more = true;
let db_path = self.db_path.clone(); let db_path = self.db_path.clone();
let project_id = project.id.clone(); let project_id = project.id.clone();
@@ -378,9 +386,13 @@ impl BdsApp {
/// Load more media (append to existing sidebar data). /// Load more media (append to existing sidebar data).
pub(super) fn load_more_sidebar_media(&mut self) -> Task<Message> { pub(super) fn load_more_sidebar_media(&mut self) -> Task<Message> {
if self.sidebar_media_loading_more || !self.sidebar_media_has_more {
return Task::none();
}
let (Some(_), Some(project)) = (&self.db, &self.active_project) else { let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
return Task::none(); return Task::none();
}; };
self.sidebar_media_loading_more = true;
let db_path = self.db_path.clone(); let db_path = self.db_path.clone();
let project_id = project.id.clone(); let project_id = project.id.clone();
@@ -617,12 +629,14 @@ impl BdsApp {
// ── Async sidebar data ── // ── Async sidebar data ──
Message::SidebarPostsLoaded(mut posts) => { Message::SidebarPostsLoaded(mut posts) => {
self.sidebar_posts_loading_more = false;
self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize; self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize;
posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize); posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_posts = posts; self.sidebar_posts = posts;
Task::none() Task::none()
} }
Message::SidebarMediaLoaded { mut items, thumbs } => { Message::SidebarMediaLoaded { mut items, thumbs } => {
self.sidebar_media_loading_more = false;
self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize; self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize;
items.truncate(Self::SIDEBAR_PAGE_SIZE as usize); items.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_media = items; self.sidebar_media = items;
@@ -630,21 +644,60 @@ impl BdsApp {
Task::none() Task::none()
} }
Message::SidebarPostsAppended(mut posts) => { Message::SidebarPostsAppended(mut posts) => {
self.sidebar_posts_loading_more = false;
self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize; self.sidebar_posts_has_more = posts.len() > Self::SIDEBAR_PAGE_SIZE as usize;
posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize); posts.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_posts.extend(posts); self.sidebar_posts.extend(posts);
Task::none() Task::none()
} }
Message::SidebarMediaAppended { mut items, thumbs } => { Message::SidebarMediaAppended { mut items, thumbs } => {
self.sidebar_media_loading_more = false;
self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize; self.sidebar_media_has_more = items.len() > Self::SIDEBAR_PAGE_SIZE as usize;
items.truncate(Self::SIDEBAR_PAGE_SIZE as usize); items.truncate(Self::SIDEBAR_PAGE_SIZE as usize);
self.sidebar_media.extend(items); self.sidebar_media.extend(items);
self.sidebar_media_thumbs.extend(thumbs); self.sidebar_media_thumbs.extend(thumbs);
Task::none() Task::none()
} }
Message::LoadMorePosts => self.load_more_sidebar_posts(), Message::SidebarScrolled(relative_y) => match self.sidebar_view {
Message::LoadMoreMedia => self.load_more_sidebar_media(), 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"), _ => 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));
}
}

View File

@@ -825,23 +825,6 @@ pub fn view(
top_items.push(make_post_item(p)); 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) iced::widget::Column::with_children(top_items)
@@ -980,23 +963,6 @@ pub fn view(
}) })
.collect(); .collect();
top_items.extend(items); 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) iced::widget::Column::with_children(top_items)
@@ -1254,15 +1220,21 @@ pub fn view(
.padding(12); .padding(12);
// layout.allium: sidebar width is resizable, passed as parameter // layout.allium: sidebar width is resizable, passed as parameter
container( let mut scrolling = scrollable(content)
scrollable(content) .direction(scrollable::Direction::Vertical(inputs::compact_scrollbar()))
.direction(scrollable::Direction::Vertical(inputs::compact_scrollbar())) .style(inputs::scrollable_style);
.style(inputs::scrollable_style), if matches!(sidebar_view, SidebarView::Posts | SidebarView::Pages) && posts_has_more
) || sidebar_view == SidebarView::Media && media_has_more
.width(Length::Fixed(width)) {
.height(Length::Fill) scrolling =
.style(sidebar_style) scrolling.on_scroll(|viewport| Message::SidebarScrolled(viewport.relative_offset().y));
.into() }
container(scrolling)
.width(Length::Fixed(width))
.height(Length::Fill)
.style(sidebar_style)
.into()
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -259,7 +259,6 @@ sidebar-tagsHeader = Schlagwörter
sidebar-drafts = Entwürfe sidebar-drafts = Entwürfe
sidebar-published = Veröffentlicht sidebar-published = Veröffentlicht
sidebar-archived = Archiviert sidebar-archived = Archiviert
sidebar-loadMore = Mehr laden…
language-en = Englisch language-en = Englisch
language-de = Deutsch language-de = Deutsch
language-fr = Französisch language-fr = Französisch

View File

@@ -259,7 +259,6 @@ sidebar-tagsHeader = Tags
sidebar-drafts = Drafts sidebar-drafts = Drafts
sidebar-published = Published sidebar-published = Published
sidebar-archived = Archived sidebar-archived = Archived
sidebar-loadMore = Load More…
language-en = English language-en = English
language-de = German language-de = German
language-fr = French language-fr = French

View File

@@ -259,7 +259,6 @@ sidebar-tagsHeader = Etiquetas
sidebar-drafts = Borradores sidebar-drafts = Borradores
sidebar-published = Publicados sidebar-published = Publicados
sidebar-archived = Archivados sidebar-archived = Archivados
sidebar-loadMore = Cargar más…
language-en = Inglés language-en = Inglés
language-de = Alemán language-de = Alemán
language-fr = Francés language-fr = Francés

View File

@@ -259,7 +259,6 @@ sidebar-tagsHeader = Étiquettes
sidebar-drafts = Brouillons sidebar-drafts = Brouillons
sidebar-published = Publiés sidebar-published = Publiés
sidebar-archived = Archivés sidebar-archived = Archivés
sidebar-loadMore = Charger plus…
language-en = Anglais language-en = Anglais
language-de = Allemand language-de = Allemand
language-fr = Français language-fr = Français

View File

@@ -259,7 +259,6 @@ sidebar-tagsHeader = Tag
sidebar-drafts = Bozze sidebar-drafts = Bozze
sidebar-published = Pubblicati sidebar-published = Pubblicati
sidebar-archived = Archiviati sidebar-archived = Archiviati
sidebar-loadMore = Carica di più…
language-en = Inglese language-en = Inglese
language-de = Tedesco language-de = Tedesco
language-fr = Francese language-fr = Francese

View File

@@ -26,6 +26,8 @@ use "./i18n.allium" as i18n
config { config {
default_sidebar_view: String = "posts" default_sidebar_view: String = "posts"
sidebar_page_size: Integer = 200
sidebar_auto_load_threshold: Decimal = 0.9
} }
-- ─── Shared patterns ───────────────────────────────────────── -- ─── Shared patterns ─────────────────────────────────────────
@@ -127,7 +129,7 @@ value PostsView {
draft_section: List<PostListItem> draft_section: List<PostListItem>
published_section: List<PostListItem> published_section: List<PostListItem>
archived_section: List<PostListItem> archived_section: List<PostListItem>
has_more: Boolean -- pagination, 500 per batch has_more: Boolean -- pagination, config.sidebar_page_size per batch
} }
surface PostsViewSurface { surface PostsViewSurface {
@@ -144,6 +146,10 @@ surface PostsViewSurface {
view.published_section.count view.published_section.count
view.archived_section.count view.archived_section.count
view.has_more 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. -- Drafts section always shows all drafts regardless of filters.
@@ -216,6 +222,7 @@ value MediaView {
calendar_filter: CalendarFilter? calendar_filter: CalendarFilter?
tag_filter: List<String> -- tags only, no categories for media tag_filter: List<String> -- tags only, no categories for media
grid: List<MediaGridItem> -- grid layout (not list) grid: List<MediaGridItem> -- grid layout (not list)
has_more: Boolean
} }
surface MediaViewSurface { surface MediaViewSurface {
@@ -227,6 +234,11 @@ surface MediaViewSurface {
view.calendar_filter when view.calendar_filter != null view.calendar_filter when view.calendar_filter != null
view.tag_filter view.tag_filter
view.grid.count 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 { value MediaGridItem {