fix: some work on performance
This commit is contained in:
@@ -138,6 +138,20 @@ pub enum Message {
|
|||||||
TemplateLoaded(Result<Template, String>),
|
TemplateLoaded(Result<Template, String>),
|
||||||
ScriptLoaded(Result<Script, String>),
|
ScriptLoaded(Result<Script, String>),
|
||||||
|
|
||||||
|
// Async sidebar data
|
||||||
|
SidebarPostsLoaded(Vec<Post>),
|
||||||
|
SidebarMediaLoaded {
|
||||||
|
items: Vec<Media>,
|
||||||
|
thumbs: HashMap<String, Option<std::path::PathBuf>>,
|
||||||
|
},
|
||||||
|
SidebarPostsAppended(Vec<Post>),
|
||||||
|
SidebarMediaAppended {
|
||||||
|
items: Vec<Media>,
|
||||||
|
thumbs: HashMap<String, Option<std::path::PathBuf>>,
|
||||||
|
},
|
||||||
|
LoadMorePosts,
|
||||||
|
LoadMoreMedia,
|
||||||
|
|
||||||
Noop,
|
Noop,
|
||||||
InitMenuBar,
|
InitMenuBar,
|
||||||
}
|
}
|
||||||
@@ -165,6 +179,9 @@ pub struct BdsApp {
|
|||||||
sidebar_media: Vec<Media>,
|
sidebar_media: Vec<Media>,
|
||||||
sidebar_scripts: Vec<Script>,
|
sidebar_scripts: Vec<Script>,
|
||||||
sidebar_templates: Vec<Template>,
|
sidebar_templates: Vec<Template>,
|
||||||
|
sidebar_media_thumbs: HashMap<String, Option<std::path::PathBuf>>,
|
||||||
|
sidebar_posts_has_more: bool,
|
||||||
|
sidebar_media_has_more: bool,
|
||||||
|
|
||||||
// Sidebar filters (per sidebar_views.allium PostsView / MediaView)
|
// Sidebar filters (per sidebar_views.allium PostsView / MediaView)
|
||||||
post_filter: PostFilter,
|
post_filter: PostFilter,
|
||||||
@@ -313,6 +330,9 @@ impl BdsApp {
|
|||||||
sidebar_media: Vec::new(),
|
sidebar_media: Vec::new(),
|
||||||
sidebar_scripts: Vec::new(),
|
sidebar_scripts: Vec::new(),
|
||||||
sidebar_templates: Vec::new(),
|
sidebar_templates: Vec::new(),
|
||||||
|
sidebar_media_thumbs: HashMap::new(),
|
||||||
|
sidebar_posts_has_more: false,
|
||||||
|
sidebar_media_has_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(),
|
||||||
@@ -375,10 +395,11 @@ impl BdsApp {
|
|||||||
SidebarView::Posts | SidebarView::Pages
|
SidebarView::Posts | SidebarView::Pages
|
||||||
);
|
);
|
||||||
if needs_post_refresh {
|
if needs_post_refresh {
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
}
|
} else {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Message::ToggleSidebar => {
|
Message::ToggleSidebar => {
|
||||||
self.sidebar_visible = !self.sidebar_visible;
|
self.sidebar_visible = !self.sidebar_visible;
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -471,9 +492,9 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.refresh_counts();
|
let sidebar_task = self.refresh_counts();
|
||||||
self.sync_menu_state();
|
self.sync_menu_state();
|
||||||
Task::none()
|
sidebar_task
|
||||||
}
|
}
|
||||||
Message::SwitchProject(project_id) => {
|
Message::SwitchProject(project_id) => {
|
||||||
self.project_dropdown_open = false;
|
self.project_dropdown_open = false;
|
||||||
@@ -767,9 +788,9 @@ impl BdsApp {
|
|||||||
self.notify(ToastLevel::Error, &format!("{label} failed: {err}"));
|
self.notify(ToastLevel::Error, &format!("{label} failed: {err}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.refresh_counts();
|
let sidebar_task = self.refresh_counts();
|
||||||
self.refresh_task_snapshots();
|
self.refresh_task_snapshots();
|
||||||
Task::none()
|
sidebar_task
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toast ──
|
// ── Toast ──
|
||||||
@@ -793,8 +814,7 @@ impl BdsApp {
|
|||||||
_ => &mut self.post_filter,
|
_ => &mut self.post_filter,
|
||||||
};
|
};
|
||||||
filter.search_query = query;
|
filter.search_query = query;
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::TogglePostFilterPanel => {
|
Message::TogglePostFilterPanel => {
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
@@ -811,8 +831,7 @@ impl BdsApp {
|
|||||||
};
|
};
|
||||||
filter.calendar.selected_year = year;
|
filter.calendar.selected_year = year;
|
||||||
filter.calendar.selected_month = None;
|
filter.calendar.selected_month = None;
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::SetPostCalendarMonth(month) => {
|
Message::SetPostCalendarMonth(month) => {
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
@@ -820,8 +839,7 @@ impl BdsApp {
|
|||||||
_ => &mut self.post_filter,
|
_ => &mut self.post_filter,
|
||||||
};
|
};
|
||||||
filter.calendar.selected_month = month;
|
filter.calendar.selected_month = month;
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::TogglePostTagFilter(tag) => {
|
Message::TogglePostTagFilter(tag) => {
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
@@ -833,8 +851,7 @@ impl BdsApp {
|
|||||||
} else {
|
} else {
|
||||||
filter.tag_filter.push(tag);
|
filter.tag_filter.push(tag);
|
||||||
}
|
}
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::TogglePostCategoryFilter(cat) => {
|
Message::TogglePostCategoryFilter(cat) => {
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
@@ -846,8 +863,7 @@ impl BdsApp {
|
|||||||
} else {
|
} else {
|
||||||
filter.category_filter.push(cat);
|
filter.category_filter.push(cat);
|
||||||
}
|
}
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::ClearPostFilters => {
|
Message::ClearPostFilters => {
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
@@ -855,13 +871,11 @@ impl BdsApp {
|
|||||||
_ => &mut self.post_filter,
|
_ => &mut self.post_filter,
|
||||||
};
|
};
|
||||||
filter.clear();
|
filter.clear();
|
||||||
self.refresh_sidebar_posts();
|
self.refresh_sidebar_posts()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::MediaSearchChanged(query) => {
|
Message::MediaSearchChanged(query) => {
|
||||||
self.media_filter.search_query = query;
|
self.media_filter.search_query = query;
|
||||||
self.refresh_sidebar_media();
|
self.refresh_sidebar_media()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::ToggleMediaFilterPanel => {
|
Message::ToggleMediaFilterPanel => {
|
||||||
self.media_filter.filter_panel_visible = !self.media_filter.filter_panel_visible;
|
self.media_filter.filter_panel_visible = !self.media_filter.filter_panel_visible;
|
||||||
@@ -870,13 +884,11 @@ impl BdsApp {
|
|||||||
Message::SetMediaCalendarYear(year) => {
|
Message::SetMediaCalendarYear(year) => {
|
||||||
self.media_filter.calendar.selected_year = year;
|
self.media_filter.calendar.selected_year = year;
|
||||||
self.media_filter.calendar.selected_month = None;
|
self.media_filter.calendar.selected_month = None;
|
||||||
self.refresh_sidebar_media();
|
self.refresh_sidebar_media()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::SetMediaCalendarMonth(month) => {
|
Message::SetMediaCalendarMonth(month) => {
|
||||||
self.media_filter.calendar.selected_month = month;
|
self.media_filter.calendar.selected_month = month;
|
||||||
self.refresh_sidebar_media();
|
self.refresh_sidebar_media()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::ToggleMediaTagFilter(tag) => {
|
Message::ToggleMediaTagFilter(tag) => {
|
||||||
if let Some(pos) = self.media_filter.tag_filter.iter().position(|t| *t == tag) {
|
if let Some(pos) = self.media_filter.tag_filter.iter().position(|t| *t == tag) {
|
||||||
@@ -884,14 +896,46 @@ impl BdsApp {
|
|||||||
} else {
|
} else {
|
||||||
self.media_filter.tag_filter.push(tag);
|
self.media_filter.tag_filter.push(tag);
|
||||||
}
|
}
|
||||||
self.refresh_sidebar_media();
|
self.refresh_sidebar_media()
|
||||||
Task::none()
|
|
||||||
}
|
}
|
||||||
Message::ClearMediaFilters => {
|
Message::ClearMediaFilters => {
|
||||||
self.media_filter.clear();
|
self.media_filter.clear();
|
||||||
self.refresh_sidebar_media();
|
self.refresh_sidebar_media()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Async sidebar data ──
|
||||||
|
Message::SidebarPostsLoaded(mut posts) => {
|
||||||
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::SidebarMediaLoaded { mut items, thumbs } => {
|
||||||
|
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;
|
||||||
|
self.sidebar_media_thumbs = thumbs;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::SidebarPostsAppended(mut posts) => {
|
||||||
|
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_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()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Modal ──
|
// ── Modal ──
|
||||||
Message::ShowModal(state) => {
|
Message::ShowModal(state) => {
|
||||||
@@ -1236,6 +1280,9 @@ impl BdsApp {
|
|||||||
&self.sidebar_templates,
|
&self.sidebar_templates,
|
||||||
active_post_filter,
|
active_post_filter,
|
||||||
&self.media_filter,
|
&self.media_filter,
|
||||||
|
&self.sidebar_media_thumbs,
|
||||||
|
self.sidebar_posts_has_more,
|
||||||
|
self.sidebar_media_has_more,
|
||||||
active_name,
|
active_name,
|
||||||
&self.projects,
|
&self.projects,
|
||||||
self.active_project.as_ref().map(|p| p.id.as_str()),
|
self.active_project.as_ref().map(|p| p.id.as_str()),
|
||||||
@@ -1534,7 +1581,7 @@ impl BdsApp {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_counts(&mut self) {
|
fn refresh_counts(&mut self) -> Task<Message> {
|
||||||
if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
|
if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
|
||||||
self.post_count = bds_core::db::queries::post::count_posts_by_project(
|
self.post_count = bds_core::db::queries::post::count_posts_by_project(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
@@ -1568,17 +1615,27 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh sidebar data with current filters (separate borrows to avoid borrow conflict)
|
// Refresh sidebar data with current filters (async — off main thread)
|
||||||
self.refresh_sidebar_posts();
|
let t1 = self.refresh_sidebar_posts();
|
||||||
self.refresh_sidebar_media();
|
let t2 = self.refresh_sidebar_media();
|
||||||
self.refresh_filter_metadata();
|
self.refresh_filter_metadata();
|
||||||
|
Task::batch([t1, t2])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh only sidebar posts using current filter state.
|
/// Number of items to load per sidebar page.
|
||||||
fn refresh_sidebar_posts(&mut self) {
|
/// Matches the TypeScript app's limit of 500 for initial load.
|
||||||
if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
|
const SIDEBAR_PAGE_SIZE: i64 = 500;
|
||||||
use bds_core::db::queries::post::{PostFilterParams, list_posts_filtered};
|
|
||||||
|
|
||||||
|
/// Refresh only sidebar posts using current filter state (async).
|
||||||
|
fn refresh_sidebar_posts(&mut self) -> Task<Message> {
|
||||||
|
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
use bds_core::db::queries::post::PostFilterParams;
|
||||||
|
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let project_id = project.id.clone();
|
||||||
let filter = match self.sidebar_view {
|
let filter = match self.sidebar_view {
|
||||||
SidebarView::Pages => &self.page_filter,
|
SidebarView::Pages => &self.page_filter,
|
||||||
_ => &self.post_filter,
|
_ => &self.post_filter,
|
||||||
@@ -1595,16 +1652,33 @@ impl BdsApp {
|
|||||||
pages_only: is_pages,
|
pages_only: is_pages,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sidebar_posts = list_posts_filtered(
|
Task::perform(
|
||||||
db.conn(), &project.id, ¶ms, 500, 0,
|
async move {
|
||||||
).unwrap_or_default();
|
tokio::task::spawn_blocking(move || {
|
||||||
}
|
let db = Database::open(&db_path).ok();
|
||||||
|
db.and_then(|db| {
|
||||||
|
bds_core::db::queries::post::list_posts_filtered(
|
||||||
|
db.conn(), &project_id, ¶ms,
|
||||||
|
Self::SIDEBAR_PAGE_SIZE + 1, 0,
|
||||||
|
).ok()
|
||||||
|
}).unwrap_or_default()
|
||||||
|
}).await.unwrap_or_default()
|
||||||
|
},
|
||||||
|
Message::SidebarPostsLoaded,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh only sidebar media using current filter state.
|
/// Refresh only sidebar media using current filter state (async).
|
||||||
fn refresh_sidebar_media(&mut self) {
|
fn refresh_sidebar_media(&mut self) -> Task<Message> {
|
||||||
if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
|
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
|
||||||
use bds_core::db::queries::media::{MediaFilterParams, list_media_filtered};
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
use bds_core::db::queries::media::MediaFilterParams;
|
||||||
|
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let project_id = project.id.clone();
|
||||||
|
let data_dir = self.data_dir.clone();
|
||||||
|
|
||||||
let params = MediaFilterParams {
|
let params = MediaFilterParams {
|
||||||
search_query: self.media_filter.search_query.clone(),
|
search_query: self.media_filter.search_query.clone(),
|
||||||
@@ -1613,10 +1687,124 @@ impl BdsApp {
|
|||||||
tags: self.media_filter.tag_filter.clone(),
|
tags: self.media_filter.tag_filter.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.sidebar_media = list_media_filtered(
|
Task::perform(
|
||||||
db.conn(), &project.id, ¶ms, 500, 0,
|
async move {
|
||||||
).unwrap_or_default();
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let db = Database::open(&db_path).ok();
|
||||||
|
let items = db.and_then(|db| {
|
||||||
|
bds_core::db::queries::media::list_media_filtered(
|
||||||
|
db.conn(), &project_id, ¶ms,
|
||||||
|
Self::SIDEBAR_PAGE_SIZE + 1, 0,
|
||||||
|
).ok()
|
||||||
|
}).unwrap_or_default();
|
||||||
|
|
||||||
|
// Pre-resolve thumbnail paths off the main thread
|
||||||
|
let thumbs: HashMap<String, Option<std::path::PathBuf>> = items.iter().map(|m| {
|
||||||
|
let thumb = data_dir.as_ref().and_then(|dir| {
|
||||||
|
if !m.mime_type.starts_with("image/") { return None; }
|
||||||
|
let rel = bds_core::util::paths::thumbnail_path(&m.id, "small", "webp");
|
||||||
|
let full = dir.join(&rel);
|
||||||
|
if full.exists() { Some(full) } else { None }
|
||||||
|
});
|
||||||
|
(m.id.clone(), thumb)
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
(items, thumbs)
|
||||||
|
}).await.unwrap_or_default()
|
||||||
|
},
|
||||||
|
|(items, thumbs)| Message::SidebarMediaLoaded { items, thumbs },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load more posts (append to existing sidebar data).
|
||||||
|
fn load_more_sidebar_posts(&mut self) -> Task<Message> {
|
||||||
|
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
use bds_core::db::queries::post::PostFilterParams;
|
||||||
|
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let project_id = project.id.clone();
|
||||||
|
let offset = self.sidebar_posts.len() as i64;
|
||||||
|
let filter = match self.sidebar_view {
|
||||||
|
SidebarView::Pages => &self.page_filter,
|
||||||
|
_ => &self.post_filter,
|
||||||
|
};
|
||||||
|
let is_pages = self.sidebar_view == SidebarView::Pages;
|
||||||
|
|
||||||
|
let params = PostFilterParams {
|
||||||
|
search_query: filter.search_query.clone(),
|
||||||
|
year: filter.calendar.selected_year,
|
||||||
|
month: filter.calendar.selected_month,
|
||||||
|
tags: filter.tag_filter.clone(),
|
||||||
|
categories: filter.category_filter.clone(),
|
||||||
|
exclude_pages: !is_pages,
|
||||||
|
pages_only: is_pages,
|
||||||
|
};
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let db = Database::open(&db_path).ok();
|
||||||
|
db.and_then(|db| {
|
||||||
|
bds_core::db::queries::post::list_posts_filtered(
|
||||||
|
db.conn(), &project_id, ¶ms,
|
||||||
|
Self::SIDEBAR_PAGE_SIZE + 1, offset,
|
||||||
|
).ok()
|
||||||
|
}).unwrap_or_default()
|
||||||
|
}).await.unwrap_or_default()
|
||||||
|
},
|
||||||
|
Message::SidebarPostsAppended,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load more media (append to existing sidebar data).
|
||||||
|
fn load_more_sidebar_media(&mut self) -> Task<Message> {
|
||||||
|
let (Some(_), Some(project)) = (&self.db, &self.active_project) else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
|
||||||
|
use bds_core::db::queries::media::MediaFilterParams;
|
||||||
|
|
||||||
|
let db_path = self.db_path.clone();
|
||||||
|
let project_id = project.id.clone();
|
||||||
|
let offset = self.sidebar_media.len() as i64;
|
||||||
|
let data_dir = self.data_dir.clone();
|
||||||
|
|
||||||
|
let params = MediaFilterParams {
|
||||||
|
search_query: self.media_filter.search_query.clone(),
|
||||||
|
year: self.media_filter.calendar.selected_year,
|
||||||
|
month: self.media_filter.calendar.selected_month,
|
||||||
|
tags: self.media_filter.tag_filter.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let db = Database::open(&db_path).ok();
|
||||||
|
let items = db.and_then(|db| {
|
||||||
|
bds_core::db::queries::media::list_media_filtered(
|
||||||
|
db.conn(), &project_id, ¶ms,
|
||||||
|
Self::SIDEBAR_PAGE_SIZE + 1, offset,
|
||||||
|
).ok()
|
||||||
|
}).unwrap_or_default();
|
||||||
|
|
||||||
|
let thumbs: HashMap<String, Option<std::path::PathBuf>> = items.iter().map(|m| {
|
||||||
|
let thumb = data_dir.as_ref().and_then(|dir| {
|
||||||
|
if !m.mime_type.starts_with("image/") { return None; }
|
||||||
|
let rel = bds_core::util::paths::thumbnail_path(&m.id, "small", "webp");
|
||||||
|
let full = dir.join(&rel);
|
||||||
|
if full.exists() { Some(full) } else { None }
|
||||||
|
});
|
||||||
|
(m.id.clone(), thumb)
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
(items, thumbs)
|
||||||
|
}).await.unwrap_or_default()
|
||||||
|
},
|
||||||
|
|(items, thumbs)| Message::SidebarMediaAppended { items, thumbs },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh available tags, categories, and calendar data for filter widgets.
|
/// Refresh available tags, categories, and calendar data for filter widgets.
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ use iced::widget::{button, column, container, image, row, scrollable, text, text
|
|||||||
use iced::widget::text::Shaping;
|
use iced::widget::text::Shaping;
|
||||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::util::paths::thumbnail_path;
|
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
use bds_core::model::{Media, Post, Script, Template};
|
use bds_core::model::{Media, Post, Script, Template};
|
||||||
|
|
||||||
@@ -112,18 +110,6 @@ fn format_file_size(size: i64) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the small thumbnail path for a media item on disk.
|
|
||||||
/// Returns Some(path) if the media is an image and the thumbnail file exists.
|
|
||||||
fn resolve_thumbnail_path(data_dir: Option<&Path>, media: &Media) -> Option<PathBuf> {
|
|
||||||
let data_dir = data_dir?;
|
|
||||||
if !media.mime_type.starts_with("image/") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let rel = thumbnail_path(&media.id, "small", "webp");
|
|
||||||
let full = data_dir.join(&rel);
|
|
||||||
if full.exists() { Some(full) } else { None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Truncate a media title to the max length, appending "..." if over limit.
|
/// Truncate a media title to the max length, appending "..." if over limit.
|
||||||
/// Per sidebar_views.allium: JS hard limit of 60 chars on title (substring + "...").
|
/// Per sidebar_views.allium: JS hard limit of 60 chars on title (substring + "...").
|
||||||
fn truncate_media_title(title: &str) -> String {
|
fn truncate_media_title(title: &str) -> String {
|
||||||
@@ -518,10 +504,13 @@ pub fn view(
|
|||||||
templates: &[Template],
|
templates: &[Template],
|
||||||
post_filter: &PostFilter,
|
post_filter: &PostFilter,
|
||||||
media_filter: &MediaFilter,
|
media_filter: &MediaFilter,
|
||||||
|
media_thumbs: &std::collections::HashMap<String, Option<PathBuf>>,
|
||||||
|
posts_has_more: bool,
|
||||||
|
media_has_more: bool,
|
||||||
width: f32,
|
width: f32,
|
||||||
active_tab: Option<&str>,
|
active_tab: Option<&str>,
|
||||||
locale: UiLocale,
|
locale: UiLocale,
|
||||||
data_dir: Option<&Path>,
|
_data_dir: Option<&Path>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let header_text = t(locale, sidebar_view.i18n_key());
|
let header_text = t(locale, sidebar_view.i18n_key());
|
||||||
let muted = Color::from_rgb(0.50, 0.50, 0.55);
|
let muted = Color::from_rgb(0.50, 0.50, 0.55);
|
||||||
@@ -670,6 +659,23 @@ 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)
|
||||||
@@ -746,8 +752,10 @@ pub fn view(
|
|||||||
let display_name = truncate_to_fit(&display_name, text_px);
|
let display_name = truncate_to_fit(&display_name, text_px);
|
||||||
let style_fn = if is_active { item_active_style } else { item_style };
|
let style_fn = if is_active { item_active_style } else { item_style };
|
||||||
|
|
||||||
// Left: 40x40 thumbnail or file icon
|
// Left: 40x40 thumbnail or file icon (pre-resolved, no filesystem I/O)
|
||||||
let thumb_path = resolve_thumbnail_path(data_dir, m);
|
let thumb_path = media_thumbs
|
||||||
|
.get(&m.id)
|
||||||
|
.and_then(|opt| opt.as_ref());
|
||||||
let left: Element<'static, Message> = if let Some(path) = thumb_path {
|
let left: Element<'static, Message> = if let Some(path) = thumb_path {
|
||||||
container(
|
container(
|
||||||
image(path.to_string_lossy().to_string())
|
image(path.to_string_lossy().to_string())
|
||||||
@@ -813,6 +821,23 @@ 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)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use iced::widget::{button, column, container, mouse_area, row, stack, text, Space};
|
use iced::widget::{button, column, container, mouse_area, row, stack, text, Space};
|
||||||
use iced::widget::text::Shaping;
|
use iced::widget::text::Shaping;
|
||||||
@@ -74,6 +74,11 @@ pub fn view<'a>(
|
|||||||
// Sidebar filters
|
// Sidebar filters
|
||||||
post_filter: &'a PostFilter,
|
post_filter: &'a PostFilter,
|
||||||
media_filter: &'a MediaFilter,
|
media_filter: &'a MediaFilter,
|
||||||
|
// Pre-resolved media thumbnail paths
|
||||||
|
sidebar_media_thumbs: &'a HashMap<String, Option<PathBuf>>,
|
||||||
|
// Pagination
|
||||||
|
sidebar_posts_has_more: bool,
|
||||||
|
sidebar_media_has_more: bool,
|
||||||
// Status bar
|
// Status bar
|
||||||
active_project_name: Option<&'a str>,
|
active_project_name: Option<&'a str>,
|
||||||
projects: &'a [Project],
|
projects: &'a [Project],
|
||||||
@@ -141,7 +146,7 @@ pub fn view<'a>(
|
|||||||
let mut main_row = row![activity];
|
let mut main_row = row![activity];
|
||||||
|
|
||||||
if sidebar_visible {
|
if sidebar_visible {
|
||||||
main_row = main_row.push(sidebar::view(sidebar_view, sidebar_posts, sidebar_media, sidebar_scripts, sidebar_templates, post_filter, media_filter, sidebar_width, active_tab, locale, data_dir));
|
main_row = main_row.push(sidebar::view(sidebar_view, sidebar_posts, sidebar_media, sidebar_scripts, sidebar_templates, post_filter, media_filter, sidebar_media_thumbs, sidebar_posts_has_more, sidebar_media_has_more, sidebar_width, active_tab, locale, data_dir));
|
||||||
|
|
||||||
// Resize drag handle: 4px wide strip between sidebar and content
|
// Resize drag handle: 4px wide strip between sidebar and content
|
||||||
let handle = container(Space::new(0, 0))
|
let handle = container(Space::new(0, 0))
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user