fix: and evren more gaps in M3
This commit is contained in:
@@ -209,10 +209,18 @@ pub fn count_posts_by_project(conn: &Connection, project_id: &str) -> rusqlite::
|
||||
pub struct PostFilterParams {
|
||||
/// FTS search query (empty = no search filter).
|
||||
pub search_query: String,
|
||||
/// Exact status filter.
|
||||
pub status: Option<String>,
|
||||
/// Exact language filter.
|
||||
pub language: Option<String>,
|
||||
/// Year filter from calendar archive.
|
||||
pub year: Option<i32>,
|
||||
/// Month filter (1-12) from calendar archive.
|
||||
pub month: Option<u32>,
|
||||
/// Inclusive start timestamp filter.
|
||||
pub from: Option<i64>,
|
||||
/// Inclusive end timestamp filter.
|
||||
pub to: Option<i64>,
|
||||
/// Tag filter (post must have ALL of these tags).
|
||||
pub tags: Vec<String>,
|
||||
/// Category filter (post must have at least one of these categories).
|
||||
@@ -228,7 +236,11 @@ pub struct PostFilterParams {
|
||||
impl PostFilterParams {
|
||||
pub fn has_active_filters(&self) -> bool {
|
||||
!self.search_query.is_empty()
|
||||
|| self.status.is_some()
|
||||
|| self.language.is_some()
|
||||
|| self.year.is_some()
|
||||
|| self.from.is_some()
|
||||
|| self.to.is_some()
|
||||
|| !self.tags.is_empty()
|
||||
|| !self.categories.is_empty()
|
||||
}
|
||||
@@ -270,6 +282,18 @@ pub fn list_posts_filtered(
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
if let Some(status) = &filters.status {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.status = ?{idx})"));
|
||||
param_values.push(Box::new(status.clone()));
|
||||
}
|
||||
|
||||
if let Some(language) = &filters.language {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.language = ?{idx} OR p.language IS NULL)"));
|
||||
param_values.push(Box::new(language.clone()));
|
||||
}
|
||||
|
||||
if let Some(year) = filters.year {
|
||||
// created_at is unix ms; compute year range
|
||||
let start = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
|
||||
@@ -331,10 +355,27 @@ pub fn list_posts_filtered(
|
||||
param_values.push(Box::new(pattern));
|
||||
}
|
||||
|
||||
// If there are active filter conditions, apply them only to non-draft posts
|
||||
if let Some(from) = filters.from {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.created_at >= ?{idx})"));
|
||||
param_values.push(Box::new(from));
|
||||
}
|
||||
|
||||
if let Some(to) = filters.to {
|
||||
let idx = param_values.len() + 1;
|
||||
filter_conditions.push(format!("(p.created_at <= ?{idx})"));
|
||||
param_values.push(Box::new(to));
|
||||
}
|
||||
|
||||
// Without an explicit status filter, drafts remain visible regardless of the
|
||||
// rest of the active filters.
|
||||
if !filter_conditions.is_empty() {
|
||||
let combined = filter_conditions.join(" AND ");
|
||||
conditions.push(format!("(p.status = 'draft' OR ({combined}))"));
|
||||
if filters.status.is_some() {
|
||||
conditions.push(format!("({combined})"));
|
||||
} else {
|
||||
conditions.push(format!("(p.status = 'draft' OR ({combined}))"));
|
||||
}
|
||||
}
|
||||
|
||||
let where_clause = conditions.join(" AND ");
|
||||
|
||||
@@ -97,8 +97,12 @@ pub enum Message {
|
||||
// Sidebar filters
|
||||
PostSearchChanged(String),
|
||||
TogglePostFilterPanel,
|
||||
SetPostStatusFilter(Option<String>),
|
||||
SetPostLanguageFilter(Option<String>),
|
||||
SetPostCalendarYear(Option<i32>),
|
||||
SetPostCalendarMonth(Option<u32>),
|
||||
SetPostFromDate(String),
|
||||
SetPostToDate(String),
|
||||
TogglePostTagFilter(String),
|
||||
TogglePostCategoryFilter(String),
|
||||
ClearPostFilters,
|
||||
@@ -152,6 +156,12 @@ pub enum Message {
|
||||
LoadMorePosts,
|
||||
LoadMoreMedia,
|
||||
|
||||
CreatePost,
|
||||
CreatePage,
|
||||
CreateMedia,
|
||||
CreateScript,
|
||||
CreateTemplate,
|
||||
|
||||
Noop,
|
||||
InitMenuBar,
|
||||
}
|
||||
@@ -347,6 +357,7 @@ fn format_bytes(size: i64) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{month_abbreviation, persist_media_editor_state_impl, persist_post_editor_state_impl, save_editor_settings_state_impl, save_script_editor_state_impl, save_template_editor_state_impl, BdsApp, Message, PersistedMediaState, PersistedPostState, PostStatus, SettingsMsg, POST_AUTO_SAVE_DELAY_MS};
|
||||
use crate::state::sidebar_filter::PostFilter;
|
||||
use crate::views::media_editor::{MediaEditorMsg, MediaEditorState};
|
||||
use crate::views::post_editor::PostEditorState;
|
||||
use crate::views::script_editor::ScriptEditorState;
|
||||
@@ -914,6 +925,156 @@ mod tests {
|
||||
"picture".to_string(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_script_opens_editor_and_refreshes_sidebar() {
|
||||
let (db, project, tmp) = setup();
|
||||
let mut app = make_app(db, project, &tmp);
|
||||
|
||||
let _ = app.update(Message::CreateScript);
|
||||
|
||||
assert_eq!(app.sidebar_scripts.len(), 1);
|
||||
assert_eq!(app.tabs.len(), 1);
|
||||
assert_eq!(app.tabs[0].tab_type, crate::state::tabs::TabType::Scripts);
|
||||
assert_eq!(app.active_tab.as_deref(), Some(app.sidebar_scripts[0].id.as_str()));
|
||||
let editor = app
|
||||
.script_editors
|
||||
.get(&app.sidebar_scripts[0].id)
|
||||
.expect("script editor should be hydrated");
|
||||
assert_eq!(editor.entrypoint, "render");
|
||||
assert!(editor.content.contains("new script"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_template_opens_editor_and_refreshes_sidebar() {
|
||||
let (db, project, tmp) = setup();
|
||||
let mut app = make_app(db, project, &tmp);
|
||||
|
||||
let _ = app.update(Message::CreateTemplate);
|
||||
|
||||
assert_eq!(app.sidebar_templates.len(), 1);
|
||||
assert_eq!(app.tabs.len(), 1);
|
||||
assert_eq!(app.tabs[0].tab_type, crate::state::tabs::TabType::Templates);
|
||||
assert_eq!(app.active_tab.as_deref(), Some(app.sidebar_templates[0].id.as_str()));
|
||||
let editor = app
|
||||
.template_editors
|
||||
.get(&app.sidebar_templates[0].id)
|
||||
.expect("template editor should be hydrated");
|
||||
assert!(editor.content.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_sidebar_posts_filters_status_language_and_date_range() {
|
||||
let (_db, project, tmp) = setup();
|
||||
let db_path = tmp.path().join("sidebar.db");
|
||||
let mut db = Database::open(&db_path).unwrap();
|
||||
db.migrate().unwrap();
|
||||
ensure_fts_tables(db.conn()).unwrap();
|
||||
insert_project(db.conn(), &project).unwrap();
|
||||
|
||||
let draft = post::create_post(
|
||||
db.conn(),
|
||||
tmp.path(),
|
||||
&project.id,
|
||||
"Draft German",
|
||||
Some("Body"),
|
||||
vec![],
|
||||
vec!["article".to_string()],
|
||||
None,
|
||||
Some("de"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let published = post::create_post(
|
||||
db.conn(),
|
||||
tmp.path(),
|
||||
&project.id,
|
||||
"Published English",
|
||||
Some("Body"),
|
||||
vec![],
|
||||
vec!["article".to_string()],
|
||||
None,
|
||||
Some("en"),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut published_saved = bds_core::db::queries::post::get_post_by_id(db.conn(), &published.id).unwrap();
|
||||
published_saved.status = PostStatus::Published;
|
||||
published_saved.created_at = chrono::Utc
|
||||
.with_ymd_and_hms(2026, 4, 12, 8, 0, 0)
|
||||
.single()
|
||||
.unwrap()
|
||||
.timestamp_millis();
|
||||
bds_core::db::queries::post::update_post(db.conn(), &published_saved).unwrap();
|
||||
|
||||
let mut draft_saved = bds_core::db::queries::post::get_post_by_id(db.conn(), &draft.id).unwrap();
|
||||
draft_saved.created_at = chrono::Utc
|
||||
.with_ymd_and_hms(2025, 1, 5, 8, 0, 0)
|
||||
.single()
|
||||
.unwrap()
|
||||
.timestamp_millis();
|
||||
bds_core::db::queries::post::update_post(db.conn(), &draft_saved).unwrap();
|
||||
|
||||
let filter = PostFilter {
|
||||
status_filter: Some("published".to_string()),
|
||||
language_filter: Some("en".to_string()),
|
||||
from_date: "2026-04-01".to_string(),
|
||||
to_date: "2026-04-30".to_string(),
|
||||
..PostFilter::default()
|
||||
};
|
||||
|
||||
let posts = BdsApp::query_sidebar_posts_blocking(
|
||||
db_path.as_path(),
|
||||
&project.id,
|
||||
"en",
|
||||
&filter,
|
||||
false,
|
||||
50,
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(posts.len(), 1);
|
||||
assert_eq!(posts[0].id, published.id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regenerate_project_thumbnails_recreates_missing_files() {
|
||||
let (db, project, tmp) = setup();
|
||||
let source = tmp.path().join("tiny.png");
|
||||
std::fs::write(&source, tiny_png_bytes()).unwrap();
|
||||
let media = media::import_media(
|
||||
db.conn(),
|
||||
tmp.path(),
|
||||
&project.id,
|
||||
&source,
|
||||
"tiny.png",
|
||||
Some("Tiny"),
|
||||
Some("Alt"),
|
||||
None,
|
||||
None,
|
||||
Some("en"),
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let small_thumb = tmp
|
||||
.path()
|
||||
.join(bds_core::util::paths::thumbnail_path(&media.id, "small", "webp"));
|
||||
std::fs::remove_file(&small_thumb).unwrap();
|
||||
assert!(!small_thumb.exists());
|
||||
|
||||
let regenerated = BdsApp::regenerate_project_thumbnails(
|
||||
&db,
|
||||
tmp.path(),
|
||||
&project.id,
|
||||
|_index, _total, _name| {},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(regenerated, 1);
|
||||
assert!(small_thumb.exists());
|
||||
}
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────
|
||||
@@ -1218,6 +1379,14 @@ impl BdsApp {
|
||||
self.panel_visible = !self.panel_visible;
|
||||
Task::none()
|
||||
}
|
||||
Message::CreatePost => self.create_sidebar_post(false),
|
||||
Message::CreatePage => self.create_sidebar_post(true),
|
||||
Message::CreateMedia => crate::platform::dialog::pick_media_files(
|
||||
t(self.ui_locale, "dialog.importMedia"),
|
||||
t(self.ui_locale, "dialog.imageFilter"),
|
||||
),
|
||||
Message::CreateScript => self.create_sidebar_script(),
|
||||
Message::CreateTemplate => self.create_sidebar_template(),
|
||||
Message::OpenSettingsSection(section) => {
|
||||
self.sidebar_view = SidebarView::Settings;
|
||||
self.sidebar_visible = true;
|
||||
@@ -1656,6 +1825,22 @@ impl BdsApp {
|
||||
filter.filter_panel_visible = !filter.filter_panel_visible;
|
||||
Task::none()
|
||||
}
|
||||
Message::SetPostStatusFilter(status) => {
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &mut self.page_filter,
|
||||
_ => &mut self.post_filter,
|
||||
};
|
||||
filter.status_filter = status;
|
||||
self.refresh_sidebar_posts()
|
||||
}
|
||||
Message::SetPostLanguageFilter(language) => {
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &mut self.page_filter,
|
||||
_ => &mut self.post_filter,
|
||||
};
|
||||
filter.language_filter = language;
|
||||
self.refresh_sidebar_posts()
|
||||
}
|
||||
Message::SetPostCalendarYear(year) => {
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &mut self.page_filter,
|
||||
@@ -1673,6 +1858,22 @@ impl BdsApp {
|
||||
filter.calendar.selected_month = month;
|
||||
self.refresh_sidebar_posts()
|
||||
}
|
||||
Message::SetPostFromDate(value) => {
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &mut self.page_filter,
|
||||
_ => &mut self.post_filter,
|
||||
};
|
||||
filter.from_date = value;
|
||||
self.refresh_sidebar_posts()
|
||||
}
|
||||
Message::SetPostToDate(value) => {
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &mut self.page_filter,
|
||||
_ => &mut self.post_filter,
|
||||
};
|
||||
filter.to_date = value;
|
||||
self.refresh_sidebar_posts()
|
||||
}
|
||||
Message::TogglePostTagFilter(tag) => {
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &mut self.page_filter,
|
||||
@@ -2451,43 +2652,7 @@ impl BdsApp {
|
||||
fn dispatch_menu_action(&mut self, action: MenuAction) -> Task<Message> {
|
||||
match action {
|
||||
// File
|
||||
MenuAction::NewPost => {
|
||||
if let (Some(db), Some(project), Some(data_dir)) =
|
||||
(&self.db, &self.active_project, &self.data_dir)
|
||||
{
|
||||
let display_title = t(self.ui_locale, "post.untitled");
|
||||
match engine::post::create_post(
|
||||
db.conn(),
|
||||
data_dir,
|
||||
&project.id,
|
||||
"",
|
||||
Some(""),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Ok(post) => {
|
||||
let tab = Tab {
|
||||
id: post.id.clone(),
|
||||
tab_type: TabType::Post,
|
||||
title: display_title.to_string(),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(t) = self.tabs.get(idx) {
|
||||
self.active_tab = Some(t.id.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.add_output(&format!("Failed to create post: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
MenuAction::NewPost => self.create_sidebar_post(false),
|
||||
MenuAction::ImportMedia => crate::platform::dialog::pick_media_files(
|
||||
t(self.ui_locale, "dialog.importMedia"),
|
||||
t(self.ui_locale, "dialog.imageFilter"),
|
||||
@@ -2601,6 +2766,133 @@ impl BdsApp {
|
||||
self.enforce_panel_tab_fallback();
|
||||
}
|
||||
|
||||
fn create_sidebar_post(&mut self, is_page: bool) -> Task<Message> {
|
||||
let (Some(db), Some(project), Some(data_dir)) =
|
||||
(&self.db, &self.active_project, &self.data_dir)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let categories = if is_page {
|
||||
vec!["page".to_string()]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
match engine::post::create_post(
|
||||
db.conn(),
|
||||
data_dir,
|
||||
&project.id,
|
||||
"",
|
||||
Some(""),
|
||||
Vec::new(),
|
||||
categories,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
Ok(post) => {
|
||||
let tab = Tab {
|
||||
id: post.id.clone(),
|
||||
tab_type: TabType::Post,
|
||||
title: t(self.ui_locale, "post.untitled"),
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(tab) = self.tabs.get(idx).cloned() {
|
||||
self.active_tab = Some(tab.id.clone());
|
||||
self.load_editor_for_tab(&tab);
|
||||
}
|
||||
self.sync_menu_state();
|
||||
self.refresh_counts()
|
||||
}
|
||||
Err(error) => {
|
||||
self.notify(ToastLevel::Error, &format!("Failed to create post: {error}"));
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sidebar_script(&mut self) -> Task<Message> {
|
||||
let (Some(db), Some(project)) = (&self.db, &self.active_project) else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
match engine::script::create_script(
|
||||
db.conn(),
|
||||
&project.id,
|
||||
&t(self.ui_locale, "editor.untitled"),
|
||||
bds_core::model::ScriptKind::Utility,
|
||||
"print(\"new script\")",
|
||||
Some("render"),
|
||||
) {
|
||||
Ok(script) => {
|
||||
let tab = Tab {
|
||||
id: script.id.clone(),
|
||||
tab_type: TabType::Scripts,
|
||||
title: if script.title.is_empty() {
|
||||
t(self.ui_locale, "editor.untitled")
|
||||
} else {
|
||||
script.title.clone()
|
||||
},
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(tab) = self.tabs.get(idx).cloned() {
|
||||
self.active_tab = Some(tab.id.clone());
|
||||
self.load_editor_for_tab(&tab);
|
||||
}
|
||||
self.sync_menu_state();
|
||||
self.refresh_counts()
|
||||
}
|
||||
Err(error) => {
|
||||
self.notify(ToastLevel::Error, &format!("Failed to create script: {error}"));
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_sidebar_template(&mut self) -> Task<Message> {
|
||||
let (Some(db), Some(project)) = (&self.db, &self.active_project) else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
match engine::template::create_template(
|
||||
db.conn(),
|
||||
&project.id,
|
||||
&t(self.ui_locale, "editor.untitled"),
|
||||
bds_core::model::TemplateKind::Post,
|
||||
"",
|
||||
) {
|
||||
Ok(template) => {
|
||||
let tab = Tab {
|
||||
id: template.id.clone(),
|
||||
tab_type: TabType::Templates,
|
||||
title: if template.title.is_empty() {
|
||||
t(self.ui_locale, "editor.untitled")
|
||||
} else {
|
||||
template.title.clone()
|
||||
},
|
||||
is_transient: true,
|
||||
is_dirty: false,
|
||||
};
|
||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
||||
if let Some(tab) = self.tabs.get(idx).cloned() {
|
||||
self.active_tab = Some(tab.id.clone());
|
||||
self.load_editor_for_tab(&tab);
|
||||
}
|
||||
self.sync_menu_state();
|
||||
self.refresh_counts()
|
||||
}
|
||||
Err(error) => {
|
||||
self.notify(ToastLevel::Error, &format!("Failed to create template: {error}"));
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_task_snapshots(&mut self) {
|
||||
self.task_snapshots = self
|
||||
.task_manager
|
||||
@@ -2901,36 +3193,27 @@ impl BdsApp {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
use bds_core::db::queries::post::PostFilterParams;
|
||||
|
||||
let db_path = self.db_path.clone();
|
||||
let project_id = project.id.clone();
|
||||
let content_language = self.content_language.clone();
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &self.page_filter,
|
||||
_ => &self.post_filter,
|
||||
SidebarView::Pages => self.page_filter.clone(),
|
||||
_ => self.post_filter.clone(),
|
||||
};
|
||||
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, 0,
|
||||
).ok()
|
||||
}).unwrap_or_default()
|
||||
Self::query_sidebar_posts_blocking(
|
||||
&db_path,
|
||||
&project_id,
|
||||
&content_language,
|
||||
&filter,
|
||||
is_pages,
|
||||
Self::SIDEBAR_PAGE_SIZE + 1,
|
||||
0,
|
||||
)
|
||||
}).await.unwrap_or_default()
|
||||
},
|
||||
Message::SidebarPostsLoaded,
|
||||
@@ -2991,43 +3274,161 @@ impl BdsApp {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
use bds_core::db::queries::post::PostFilterParams;
|
||||
|
||||
let db_path = self.db_path.clone();
|
||||
let project_id = project.id.clone();
|
||||
let content_language = self.content_language.clone();
|
||||
let offset = self.sidebar_posts.len() as i64;
|
||||
let filter = match self.sidebar_view {
|
||||
SidebarView::Pages => &self.page_filter,
|
||||
_ => &self.post_filter,
|
||||
SidebarView::Pages => self.page_filter.clone(),
|
||||
_ => self.post_filter.clone(),
|
||||
};
|
||||
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()
|
||||
Self::query_sidebar_posts_blocking(
|
||||
&db_path,
|
||||
&project_id,
|
||||
&content_language,
|
||||
&filter,
|
||||
is_pages,
|
||||
Self::SIDEBAR_PAGE_SIZE + 1,
|
||||
offset,
|
||||
)
|
||||
}).await.unwrap_or_default()
|
||||
},
|
||||
Message::SidebarPostsAppended,
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_filter_date(input: &str, end_of_day: bool) -> Option<i64> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let date = chrono::NaiveDate::parse_from_str(trimmed, "%Y-%m-%d").ok()?;
|
||||
let time = if end_of_day {
|
||||
date.and_hms_opt(23, 59, 59)?
|
||||
} else {
|
||||
date.and_hms_opt(0, 0, 0)?
|
||||
};
|
||||
Some(time.and_utc().timestamp_millis())
|
||||
}
|
||||
|
||||
fn build_post_filter_params(
|
||||
filter: &PostFilter,
|
||||
is_pages: bool,
|
||||
) -> bds_core::db::queries::post::PostFilterParams {
|
||||
bds_core::db::queries::post::PostFilterParams {
|
||||
search_query: filter.search_query.clone(),
|
||||
status: filter.status_filter.clone(),
|
||||
language: filter.language_filter.clone(),
|
||||
year: filter.calendar.selected_year,
|
||||
month: filter.calendar.selected_month,
|
||||
from: Self::parse_filter_date(&filter.from_date, false),
|
||||
to: Self::parse_filter_date(&filter.to_date, true),
|
||||
tags: filter.tag_filter.clone(),
|
||||
categories: filter.category_filter.clone(),
|
||||
exclude_pages: !is_pages,
|
||||
pages_only: is_pages,
|
||||
}
|
||||
}
|
||||
|
||||
fn query_sidebar_posts_blocking(
|
||||
db_path: &Path,
|
||||
project_id: &str,
|
||||
content_language: &str,
|
||||
filter: &PostFilter,
|
||||
is_pages: bool,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Vec<Post> {
|
||||
let Ok(db) = Database::open(db_path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let params = Self::build_post_filter_params(filter, is_pages);
|
||||
if filter.search_query.trim().is_empty() {
|
||||
return bds_core::db::queries::post::list_posts_filtered(
|
||||
db.conn(),
|
||||
project_id,
|
||||
¶ms,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let fts_filters = bds_core::db::fts::PostSearchFilters {
|
||||
status: params.status.as_deref(),
|
||||
tags: (!params.tags.is_empty()).then_some(params.tags.as_slice()),
|
||||
categories: (!params.categories.is_empty()).then_some(params.categories.as_slice()),
|
||||
language: params.language.as_deref(),
|
||||
year: params.year,
|
||||
month: params.month,
|
||||
from: params.from,
|
||||
to: params.to,
|
||||
limit: Some(limit as usize),
|
||||
offset: Some(offset as usize),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let ids = bds_core::db::fts::search_posts_filtered(
|
||||
db.conn(),
|
||||
¶ms.search_query,
|
||||
content_language,
|
||||
&fts_filters,
|
||||
)
|
||||
.map(|results| results.post_ids)
|
||||
.unwrap_or_default();
|
||||
|
||||
ids.into_iter()
|
||||
.filter_map(|post_id| bds_core::db::queries::post::get_post_by_id(db.conn(), &post_id).ok())
|
||||
.filter(|post| post.project_id == project_id)
|
||||
.filter(|post| {
|
||||
let is_page_post = post
|
||||
.categories
|
||||
.iter()
|
||||
.any(|category| category.eq_ignore_ascii_case("page"));
|
||||
if is_pages {
|
||||
is_page_post
|
||||
} else {
|
||||
!is_page_post
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn regenerate_project_thumbnails(
|
||||
db: &Database,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
mut progress: impl FnMut(usize, usize, &str),
|
||||
) -> Result<usize, String> {
|
||||
let media = bds_core::db::queries::media::list_media_by_project(db.conn(), project_id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let total = media.len();
|
||||
let thumbnails_dir = data_dir.join("thumbnails");
|
||||
let mut regenerated = 0usize;
|
||||
|
||||
for (index, item) in media.iter().enumerate() {
|
||||
progress(index, total, &item.original_name);
|
||||
if !item.mime_type.starts_with("image/") {
|
||||
continue;
|
||||
}
|
||||
let source = data_dir.join(&item.file_path);
|
||||
if !source.exists() {
|
||||
continue;
|
||||
}
|
||||
bds_core::util::thumbnail::generate_all_thumbnails(&source, &thumbnails_dir, &item.id)
|
||||
.map_err(|e| e.to_string())?;
|
||||
regenerated += 1;
|
||||
}
|
||||
|
||||
Ok(regenerated)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -3094,6 +3495,7 @@ impl BdsApp {
|
||||
).unwrap_or_default();
|
||||
self.post_filter.available_tags = all_tags.clone();
|
||||
self.post_filter.available_categories = all_cats.clone();
|
||||
self.post_filter.available_languages = self.blog_languages.clone();
|
||||
self.post_filter.calendar_years = Self::build_calendar_tree(&post_cal);
|
||||
|
||||
// Calendar counts for pages only
|
||||
@@ -3102,6 +3504,7 @@ impl BdsApp {
|
||||
).unwrap_or_default();
|
||||
self.page_filter.available_tags = all_tags;
|
||||
self.page_filter.available_categories = all_cats;
|
||||
self.page_filter.available_languages = self.blog_languages.clone();
|
||||
self.page_filter.calendar_years = Self::build_calendar_tree(&page_cal);
|
||||
|
||||
// Media filter metadata
|
||||
@@ -4591,34 +4994,126 @@ impl BdsApp {
|
||||
SettingsMsg::SemanticSimilarityChanged(value) => {
|
||||
state.semantic_similarity_enabled = value;
|
||||
}
|
||||
SettingsMsg::RebuildPosts => { return Task::done(Message::RebuildDatabase); }
|
||||
SettingsMsg::RebuildMedia => { return Task::done(Message::RebuildDatabase); }
|
||||
SettingsMsg::RebuildScripts => { return Task::done(Message::RebuildDatabase); }
|
||||
SettingsMsg::RebuildTemplates => { return Task::done(Message::RebuildDatabase); }
|
||||
SettingsMsg::RebuildPosts => {
|
||||
return self.spawn_engine_task(
|
||||
"settings.rebuildPosts",
|
||||
|db_path, project_id, data_dir, _tm, _tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let report = engine::post::rebuild_posts_from_filesystem(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
"created={}, updated={}, translations={}",
|
||||
report.posts_created,
|
||||
report.posts_updated,
|
||||
report.translations_created + report.translations_updated,
|
||||
))
|
||||
},
|
||||
);
|
||||
}
|
||||
SettingsMsg::RebuildMedia => {
|
||||
return self.spawn_engine_task(
|
||||
"settings.rebuildMedia",
|
||||
|db_path, project_id, data_dir, _tm, _tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let report = engine::media::rebuild_media_from_filesystem(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
"created={}, updated={}, translations={}",
|
||||
report.media_created,
|
||||
report.media_updated,
|
||||
report.translations_created + report.translations_updated,
|
||||
))
|
||||
},
|
||||
);
|
||||
}
|
||||
SettingsMsg::RebuildScripts => {
|
||||
return self.spawn_engine_task(
|
||||
"settings.rebuildScripts",
|
||||
|db_path, project_id, data_dir, _tm, _tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let report = engine::script_rebuild::rebuild_scripts_from_filesystem(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
"created={}, updated={}, errors={}",
|
||||
report.created,
|
||||
report.updated,
|
||||
report.errors.len(),
|
||||
))
|
||||
},
|
||||
);
|
||||
}
|
||||
SettingsMsg::RebuildTemplates => {
|
||||
return self.spawn_engine_task(
|
||||
"settings.rebuildTemplates",
|
||||
|db_path, project_id, data_dir, _tm, _tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let report = engine::template_rebuild::rebuild_templates_from_filesystem(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!(
|
||||
"created={}, updated={}, errors={}",
|
||||
report.created,
|
||||
report.updated,
|
||||
report.errors.len(),
|
||||
))
|
||||
},
|
||||
);
|
||||
}
|
||||
SettingsMsg::RebuildLinks => {
|
||||
if let (Some(db), Some(data_dir), Some(project)) =
|
||||
(&self.db, &self.data_dir, &self.active_project)
|
||||
{
|
||||
match bds_core::engine::post::rebuild_all_links(
|
||||
db.conn(), data_dir, &project.id,
|
||||
) {
|
||||
Ok(count) => {
|
||||
self.notify(
|
||||
ToastLevel::Info,
|
||||
&format!("Rebuilt {} links", count),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(
|
||||
ToastLevel::Error,
|
||||
&format!("Failed to rebuild links: {e}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return self.spawn_engine_task(
|
||||
"settings.rebuildLinks",
|
||||
|db_path, project_id, data_dir, _tm, _tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let rebuilt = bds_core::engine::post::rebuild_all_links(
|
||||
db.conn(),
|
||||
&data_dir,
|
||||
&project_id,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(format!("rebuilt={rebuilt}"))
|
||||
},
|
||||
);
|
||||
}
|
||||
SettingsMsg::RegenerateThumbnails => {
|
||||
self.notify(ToastLevel::Info, &t(self.ui_locale, "settings.regeneratingThumbnails"));
|
||||
return self.spawn_engine_task(
|
||||
"settings.regenerateThumbnails",
|
||||
|db_path, project_id, data_dir, tm, tid| {
|
||||
let db = Database::open(&db_path).map_err(|e| e.to_string())?;
|
||||
let regenerated = Self::regenerate_project_thumbnails(
|
||||
&db,
|
||||
&data_dir,
|
||||
&project_id,
|
||||
|index, total, name| {
|
||||
let progress = if total > 0 {
|
||||
index as f32 / total as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
tm.report_progress(
|
||||
tid,
|
||||
Some(progress),
|
||||
Some(format!("Regenerating: {name}")),
|
||||
);
|
||||
},
|
||||
)?;
|
||||
Ok(format!("regenerated={regenerated}"))
|
||||
},
|
||||
);
|
||||
}
|
||||
SettingsMsg::OpenDataFolder => {
|
||||
if let Some(ref dir) = self.data_dir {
|
||||
|
||||
@@ -35,8 +35,16 @@ pub struct PostFilter {
|
||||
pub search_query: String,
|
||||
/// Whether the collapsible filter panel is visible.
|
||||
pub filter_panel_visible: bool,
|
||||
/// Exact status filter.
|
||||
pub status_filter: Option<String>,
|
||||
/// Exact language filter.
|
||||
pub language_filter: Option<String>,
|
||||
/// Year/month archive filter.
|
||||
pub calendar: CalendarFilter,
|
||||
/// Inclusive start date input (`YYYY-MM-DD`).
|
||||
pub from_date: String,
|
||||
/// Inclusive end date input (`YYYY-MM-DD`).
|
||||
pub to_date: String,
|
||||
/// Selected tag names (multi-select).
|
||||
pub tag_filter: Vec<String>,
|
||||
/// Selected category names (multi-select).
|
||||
@@ -47,6 +55,8 @@ pub struct PostFilter {
|
||||
pub available_tags: Vec<String>,
|
||||
/// All available categories for the chip selector.
|
||||
pub available_categories: Vec<String>,
|
||||
/// Available languages for the chip selector.
|
||||
pub available_languages: Vec<String>,
|
||||
}
|
||||
|
||||
/// Filter state for the Media sidebar.
|
||||
@@ -70,7 +80,11 @@ impl PostFilter {
|
||||
/// Returns true if any filter is active (for "Clear All" visibility).
|
||||
pub fn has_active_filters(&self) -> bool {
|
||||
!self.search_query.is_empty()
|
||||
|| self.status_filter.is_some()
|
||||
|| self.language_filter.is_some()
|
||||
|| self.calendar.selected_year.is_some()
|
||||
|| !self.from_date.trim().is_empty()
|
||||
|| !self.to_date.trim().is_empty()
|
||||
|| !self.tag_filter.is_empty()
|
||||
|| !self.category_filter.is_empty()
|
||||
}
|
||||
@@ -78,7 +92,11 @@ impl PostFilter {
|
||||
/// Reset all filters to defaults.
|
||||
pub fn clear(&mut self) {
|
||||
self.search_query.clear();
|
||||
self.status_filter = None;
|
||||
self.language_filter = None;
|
||||
self.calendar = CalendarFilter::default();
|
||||
self.from_date.clear();
|
||||
self.to_date.clear();
|
||||
self.tag_filter.clear();
|
||||
self.category_filter.clear();
|
||||
}
|
||||
|
||||
@@ -156,9 +156,6 @@ impl MediaEditorState {
|
||||
|
||||
/// Build translation flags for the view.
|
||||
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
|
||||
if self.translation_drafts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut flags = Vec::new();
|
||||
let canon = &self.canonical_language;
|
||||
let canon_locale = i18n::normalize_language(canon);
|
||||
@@ -168,15 +165,29 @@ impl MediaEditorState {
|
||||
status: "canonical".to_string(),
|
||||
is_active: self.active_language == *canon,
|
||||
});
|
||||
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
|
||||
let mut langs: Vec<String> = self
|
||||
.blog_languages
|
||||
.iter()
|
||||
.filter(|lang| **lang != *canon)
|
||||
.cloned()
|
||||
.collect();
|
||||
for lang in self.translation_drafts.keys() {
|
||||
if lang != canon && !langs.contains(lang) {
|
||||
langs.push(lang.clone());
|
||||
}
|
||||
}
|
||||
langs.sort();
|
||||
for lang in langs {
|
||||
let locale = i18n::normalize_language(lang);
|
||||
let locale = i18n::normalize_language(&lang);
|
||||
flags.push(TranslationFlag {
|
||||
language: lang.clone(),
|
||||
flag_emoji: locale.flag_emoji().to_string(),
|
||||
status: "translation".to_string(),
|
||||
is_active: self.active_language == **lang,
|
||||
status: if self.translation_drafts.contains_key(&lang) {
|
||||
"translation".to_string()
|
||||
} else {
|
||||
"missing".to_string()
|
||||
},
|
||||
is_active: self.active_language == lang,
|
||||
});
|
||||
}
|
||||
flags
|
||||
@@ -448,6 +459,50 @@ pub fn view<'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::MediaEditorState;
|
||||
use bds_core::model::Media;
|
||||
|
||||
fn make_media() -> Media {
|
||||
Media {
|
||||
id: "m1".to_string(),
|
||||
project_id: "proj".to_string(),
|
||||
filename: "image.png".to_string(),
|
||||
original_name: "image.png".to_string(),
|
||||
mime_type: "image/png".to_string(),
|
||||
size: 10,
|
||||
width: Some(1),
|
||||
height: Some(1),
|
||||
title: Some("Image".to_string()),
|
||||
alt: Some("Alt".to_string()),
|
||||
caption: None,
|
||||
author: None,
|
||||
language: Some("en".to_string()),
|
||||
file_path: "media/test/image.png".to_string(),
|
||||
sidecar_path: "media/test/image.png.meta".to_string(),
|
||||
checksum: None,
|
||||
tags: Vec::new(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translation_flags_include_configured_languages_without_translations() {
|
||||
let state = MediaEditorState::from_media(
|
||||
&make_media(),
|
||||
&["en".to_string(), "de".to_string()],
|
||||
&[],
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
let flags = state.translation_flags();
|
||||
let languages = flags.into_iter().map(|flag| flag.language).collect::<Vec<_>>();
|
||||
assert_eq!(languages, vec!["en".to_string(), "de".to_string()]);
|
||||
}
|
||||
}
|
||||
|
||||
fn no_preview<'a>() -> Element<'a, Message> {
|
||||
container(
|
||||
text("No preview available")
|
||||
|
||||
@@ -276,14 +276,9 @@ impl PostEditorState {
|
||||
}
|
||||
|
||||
/// Build the translation flags list for the view.
|
||||
/// Flags are driven by the post's actual translations, not blog-level languages.
|
||||
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
|
||||
if self.translation_drafts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut flags = Vec::new();
|
||||
|
||||
// Canonical language first (always shown when translations exist)
|
||||
let canon = &self.canonical_language;
|
||||
let canon_locale = i18n::normalize_language(canon);
|
||||
flags.push(TranslationFlag {
|
||||
@@ -293,20 +288,30 @@ impl PostEditorState {
|
||||
is_active: self.active_language == *canon,
|
||||
});
|
||||
|
||||
// Each existing translation for this post
|
||||
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
|
||||
let mut langs: Vec<String> = self
|
||||
.blog_languages
|
||||
.iter()
|
||||
.filter(|lang| **lang != *canon)
|
||||
.cloned()
|
||||
.collect();
|
||||
for lang in self.translation_drafts.keys() {
|
||||
if lang != canon && !langs.contains(lang) {
|
||||
langs.push(lang.clone());
|
||||
}
|
||||
}
|
||||
langs.sort();
|
||||
for lang in langs {
|
||||
let locale = i18n::normalize_language(lang);
|
||||
let status = match self.translation_drafts[lang].status {
|
||||
PostStatus::Published => "published",
|
||||
_ => "draft",
|
||||
let locale = i18n::normalize_language(&lang);
|
||||
let status = match self.translation_drafts.get(&lang).map(|draft| &draft.status) {
|
||||
Some(PostStatus::Published) => "published",
|
||||
Some(_) => "draft",
|
||||
None => "missing",
|
||||
};
|
||||
flags.push(TranslationFlag {
|
||||
language: lang.clone(),
|
||||
flag_emoji: locale.flag_emoji().to_string(),
|
||||
status: status.to_string(),
|
||||
is_active: self.active_language == **lang,
|
||||
is_active: self.active_language == lang,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -386,6 +386,50 @@ fn chip_selector(
|
||||
iced::widget::Column::with_children(items).spacing(2).into()
|
||||
}
|
||||
|
||||
fn single_select_chip_selector(
|
||||
label: &str,
|
||||
available: &[(String, String)],
|
||||
selected: Option<&str>,
|
||||
on_toggle: impl Fn(Option<String>) -> Message + 'static + Clone,
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let muted = Color::from_rgb(0.50, 0.50, 0.55);
|
||||
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
items.push(
|
||||
text(t(locale, label))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted)
|
||||
.into(),
|
||||
);
|
||||
|
||||
let chips = available
|
||||
.iter()
|
||||
.map(|(value, display)| {
|
||||
let is_selected = selected == Some(value.as_str());
|
||||
let style_fn = if is_selected { chip_selected_style } else { chip_style };
|
||||
let value = value.clone();
|
||||
let on_toggle = on_toggle.clone();
|
||||
button(text(display.clone()).size(10).shaping(Shaping::Advanced))
|
||||
.on_press(if is_selected {
|
||||
on_toggle(None)
|
||||
} else {
|
||||
on_toggle(Some(value))
|
||||
})
|
||||
.padding([2, 6])
|
||||
.style(style_fn)
|
||||
.into()
|
||||
})
|
||||
.collect::<Vec<Element<'static, Message>>>();
|
||||
|
||||
if !chips.is_empty() {
|
||||
items.push(row(chips).spacing(4).into());
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(items).spacing(2).into()
|
||||
}
|
||||
|
||||
/// Build the filter panel for Posts/Pages view.
|
||||
fn post_filter_panel(
|
||||
filter: &PostFilter,
|
||||
@@ -394,6 +438,35 @@ fn post_filter_panel(
|
||||
) -> Element<'static, Message> {
|
||||
let mut sections: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
sections.push(single_select_chip_selector(
|
||||
"sidebar.filter.status",
|
||||
&[
|
||||
("draft".to_string(), t(locale, "sidebar.drafts")),
|
||||
("published".to_string(), t(locale, "sidebar.published")),
|
||||
("archived".to_string(), t(locale, "sidebar.archived")),
|
||||
],
|
||||
filter.status_filter.as_deref(),
|
||||
Message::SetPostStatusFilter,
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
|
||||
if !filter.available_languages.is_empty() {
|
||||
let languages = filter
|
||||
.available_languages
|
||||
.iter()
|
||||
.map(|lang| (lang.clone(), lang.to_uppercase()))
|
||||
.collect::<Vec<_>>();
|
||||
sections.push(single_select_chip_selector(
|
||||
"sidebar.filter.language",
|
||||
&languages,
|
||||
filter.language_filter.as_deref(),
|
||||
Message::SetPostLanguageFilter,
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
// Calendar archive
|
||||
if !filter.calendar_years.is_empty() {
|
||||
sections.push(calendar_widget(
|
||||
@@ -431,6 +504,33 @@ fn post_filter_panel(
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
sections.push(
|
||||
iced::widget::Column::with_children(vec![
|
||||
text(t(locale, "sidebar.filter.dateRange"))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.50, 0.50, 0.55))
|
||||
.into(),
|
||||
row![
|
||||
text_input(&t(locale, "sidebar.filter.from"), &filter.from_date)
|
||||
.on_input(Message::SetPostFromDate)
|
||||
.size(10)
|
||||
.padding([3, 5])
|
||||
.style(search_input_style),
|
||||
text_input(&t(locale, "sidebar.filter.to"), &filter.to_date)
|
||||
.on_input(Message::SetPostToDate)
|
||||
.size(10)
|
||||
.padding([3, 5])
|
||||
.style(search_input_style),
|
||||
]
|
||||
.spacing(4)
|
||||
.into(),
|
||||
])
|
||||
.spacing(2)
|
||||
.into(),
|
||||
);
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
|
||||
// Clear all filters button
|
||||
if filter.has_active_filters() {
|
||||
sections.push(
|
||||
@@ -520,6 +620,30 @@ pub fn view(
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.85, 0.85, 0.90));
|
||||
|
||||
let create_action = match sidebar_view {
|
||||
SidebarView::Posts => Some(Message::CreatePost),
|
||||
SidebarView::Pages => Some(Message::CreatePage),
|
||||
SidebarView::Media => Some(Message::CreateMedia),
|
||||
SidebarView::Scripts => Some(Message::CreateScript),
|
||||
SidebarView::Templates => Some(Message::CreateTemplate),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let header_row: Element<'static, Message> = if let Some(action) = create_action {
|
||||
row![
|
||||
header,
|
||||
Space::with_width(Length::Fill),
|
||||
button(text(t(locale, "common.add")).size(11).shaping(Shaping::Advanced))
|
||||
.on_press(action)
|
||||
.padding([3, 8])
|
||||
.style(filter_button_style),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.into()
|
||||
} else {
|
||||
header.into()
|
||||
};
|
||||
|
||||
let body: Element<'static, Message> = match sidebar_view {
|
||||
SidebarView::Posts | SidebarView::Pages => {
|
||||
let is_pages = sidebar_view == SidebarView::Pages;
|
||||
@@ -1020,7 +1144,7 @@ pub fn view(
|
||||
};
|
||||
|
||||
let content = column![
|
||||
header,
|
||||
header_row,
|
||||
Space::with_height(8.0),
|
||||
body,
|
||||
]
|
||||
|
||||
@@ -196,6 +196,11 @@
|
||||
"sidebar.filter.search": "Suchen...",
|
||||
"sidebar.filter.toggle": "Filter",
|
||||
"sidebar.filter.clearAll": "Alle Filter zurücksetzen",
|
||||
"sidebar.filter.status": "Status",
|
||||
"sidebar.filter.language": "Sprache",
|
||||
"sidebar.filter.dateRange": "Zeitraum",
|
||||
"sidebar.filter.from": "Von (JJJJ-MM-TT)",
|
||||
"sidebar.filter.to": "Bis (JJJJ-MM-TT)",
|
||||
"editor.insertLink": "Link einfügen",
|
||||
"editor.insertMedia": "Medium einfügen",
|
||||
"editor.gallery": "Galerie",
|
||||
|
||||
@@ -196,6 +196,11 @@
|
||||
"sidebar.filter.search": "Search...",
|
||||
"sidebar.filter.toggle": "Filters",
|
||||
"sidebar.filter.clearAll": "Clear All Filters",
|
||||
"sidebar.filter.status": "Status",
|
||||
"sidebar.filter.language": "Language",
|
||||
"sidebar.filter.dateRange": "Date Range",
|
||||
"sidebar.filter.from": "From (YYYY-MM-DD)",
|
||||
"sidebar.filter.to": "To (YYYY-MM-DD)",
|
||||
"sidebar.filter.tags": "Tags",
|
||||
"sidebar.filter.categories": "Categories",
|
||||
"sidebar.filter.calendar": "Archive",
|
||||
|
||||
@@ -196,6 +196,11 @@
|
||||
"sidebar.filter.search": "Buscar...",
|
||||
"sidebar.filter.toggle": "Filtros",
|
||||
"sidebar.filter.clearAll": "Borrar todos los filtros",
|
||||
"sidebar.filter.status": "Estado",
|
||||
"sidebar.filter.language": "Idioma",
|
||||
"sidebar.filter.dateRange": "Rango de fechas",
|
||||
"sidebar.filter.from": "Desde (AAAA-MM-DD)",
|
||||
"sidebar.filter.to": "Hasta (AAAA-MM-DD)",
|
||||
"editor.insertLink": "Insertar enlace",
|
||||
"editor.insertMedia": "Insertar medio",
|
||||
"editor.gallery": "Galería",
|
||||
|
||||
@@ -196,6 +196,11 @@
|
||||
"sidebar.filter.search": "Rechercher...",
|
||||
"sidebar.filter.toggle": "Filtres",
|
||||
"sidebar.filter.clearAll": "Effacer tous les filtres",
|
||||
"sidebar.filter.status": "Statut",
|
||||
"sidebar.filter.language": "Langue",
|
||||
"sidebar.filter.dateRange": "Plage de dates",
|
||||
"sidebar.filter.from": "Depuis (AAAA-MM-JJ)",
|
||||
"sidebar.filter.to": "Jusqu'au (AAAA-MM-JJ)",
|
||||
"editor.insertLink": "Insérer un lien",
|
||||
"editor.insertMedia": "Insérer un média",
|
||||
"editor.gallery": "Galerie",
|
||||
|
||||
@@ -196,6 +196,11 @@
|
||||
"sidebar.filter.search": "Cerca...",
|
||||
"sidebar.filter.toggle": "Filtri",
|
||||
"sidebar.filter.clearAll": "Cancella tutti i filtri",
|
||||
"sidebar.filter.status": "Stato",
|
||||
"sidebar.filter.language": "Lingua",
|
||||
"sidebar.filter.dateRange": "Intervallo date",
|
||||
"sidebar.filter.from": "Da (AAAA-MM-GG)",
|
||||
"sidebar.filter.to": "A (AAAA-MM-GG)",
|
||||
"editor.insertLink": "Inserisci link",
|
||||
"editor.insertMedia": "Inserisci media",
|
||||
"editor.gallery": "Galleria",
|
||||
|
||||
Reference in New Issue
Block a user