Load sidebar entries automatically in 200-item batches.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user