fix: more work on dashboard
This commit is contained in:
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chrono::Datelike;
|
||||||
use iced::{Element, Subscription, Task};
|
use iced::{Element, Subscription, Task};
|
||||||
|
|
||||||
use bds_core::db::Database;
|
use bds_core::db::Database;
|
||||||
@@ -345,12 +346,13 @@ fn format_bytes(size: i64) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{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 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::views::media_editor::{MediaEditorMsg, MediaEditorState};
|
use crate::views::media_editor::{MediaEditorMsg, MediaEditorState};
|
||||||
use crate::views::post_editor::PostEditorState;
|
use crate::views::post_editor::PostEditorState;
|
||||||
use crate::views::script_editor::ScriptEditorState;
|
use crate::views::script_editor::ScriptEditorState;
|
||||||
use crate::views::settings_view::SettingsViewState;
|
use crate::views::settings_view::SettingsViewState;
|
||||||
use crate::views::template_editor::TemplateEditorState;
|
use crate::views::template_editor::TemplateEditorState;
|
||||||
|
use chrono::Datelike;
|
||||||
use bds_core::db::fts::ensure_fts_tables;
|
use bds_core::db::fts::ensure_fts_tables;
|
||||||
use bds_core::db::queries::project::insert_project;
|
use bds_core::db::queries::project::insert_project;
|
||||||
use bds_core::db::Database;
|
use bds_core::db::Database;
|
||||||
@@ -803,13 +805,17 @@ mod tests {
|
|||||||
let _ = app.refresh_counts();
|
let _ = app.refresh_counts();
|
||||||
|
|
||||||
let dash = app.dashboard_state.expect("dashboard state should be set");
|
let dash = app.dashboard_state.expect("dashboard state should be set");
|
||||||
|
let now = chrono::Utc::now();
|
||||||
assert_eq!(dash.stats.total_posts, 2);
|
assert_eq!(dash.stats.total_posts, 2);
|
||||||
assert_eq!(dash.stats.published_count, 1);
|
assert_eq!(dash.stats.published_count, 1);
|
||||||
assert_eq!(dash.stats.media_count, 1);
|
assert_eq!(dash.stats.media_count, 1);
|
||||||
assert_eq!(dash.stats.tag_count, 2);
|
assert_eq!(dash.stats.tag_count, 2);
|
||||||
|
assert_eq!(dash.timeline.len(), 12);
|
||||||
|
assert_eq!(dash.timeline.last().map(|month| month.year), Some(now.year()));
|
||||||
|
assert_eq!(dash.timeline.last().map(|month| month.label.clone()), Some(month_abbreviation(now.month())));
|
||||||
|
assert_eq!(dash.timeline.iter().map(|month| month.count).sum::<usize>(), 2);
|
||||||
assert_eq!(dash.recent_posts.len(), 2);
|
assert_eq!(dash.recent_posts.len(), 2);
|
||||||
assert!(!dash.category_cloud.is_empty());
|
assert!(!dash.category_cloud.is_empty());
|
||||||
assert!(!dash.timeline.is_empty());
|
|
||||||
assert_eq!(dash.recent_posts[0].title, "Second");
|
assert_eq!(dash.recent_posts[0].title, "Second");
|
||||||
assert_eq!(first.title, "First");
|
assert_eq!(first.title, "First");
|
||||||
}
|
}
|
||||||
@@ -2757,17 +2763,27 @@ impl BdsApp {
|
|||||||
let image_count = media.iter().filter(|item| item.mime_type.starts_with("image/")).count();
|
let image_count = media.iter().filter(|item| item.mime_type.starts_with("image/")).count();
|
||||||
let total_media_size = media.iter().map(|item| item.size).sum::<i64>();
|
let total_media_size = media.iter().map(|item| item.size).sum::<i64>();
|
||||||
|
|
||||||
let mut timeline = monthly_counts
|
let now = chrono::Utc::now();
|
||||||
.into_iter()
|
let current_year = now.year();
|
||||||
|
let current_month = now.month() as i32;
|
||||||
|
let timeline = (0..12)
|
||||||
.rev()
|
.rev()
|
||||||
.take(12)
|
.map(|offset| {
|
||||||
.map(|((year, month), count)| DashboardTimelineMonth {
|
let total_month_index = current_year * 12 + (current_month - 1) - offset;
|
||||||
label: month_abbreviation(month),
|
let year = total_month_index.div_euclid(12);
|
||||||
|
let month = total_month_index.rem_euclid(12) + 1;
|
||||||
|
let count = monthly_counts
|
||||||
|
.get(&(year, month as u32))
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
DashboardTimelineMonth {
|
||||||
|
label: month_abbreviation(month as u32),
|
||||||
year,
|
year,
|
||||||
count,
|
count,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
timeline.reverse();
|
|
||||||
|
|
||||||
let mut tag_cloud = tags
|
let mut tag_cloud = tags
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use iced::widget::{button, column, container, row, scrollable, text, Space};
|
use iced::widget::{button, column, container, row, scrollable, text, tooltip, Space};
|
||||||
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
@@ -191,20 +191,17 @@ fn stat_card<'a>(label: &str, value: String, details: Vec<String>) -> Element<'a
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn timeline_chart<'a>(months: &'a [DashboardTimelineMonth], locale: UiLocale) -> Element<'a, Message> {
|
fn timeline_chart<'a>(months: &'a [DashboardTimelineMonth], _locale: UiLocale) -> Element<'a, Message> {
|
||||||
if months.is_empty() {
|
|
||||||
return text(t(locale, "dashboard.noTimelineData"))
|
|
||||||
.size(13)
|
|
||||||
.color(Color::from_rgb(0.6, 0.62, 0.68))
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
let max_count = months.iter().map(|month| month.count).max().unwrap_or(1).max(1);
|
let max_count = months.iter().map(|month| month.count).max().unwrap_or(1).max(1);
|
||||||
row(
|
row(
|
||||||
months
|
months
|
||||||
.iter()
|
.iter()
|
||||||
.map(|month| {
|
.map(|month| {
|
||||||
let height = 24.0 + (month.count as f32 / max_count as f32) * 96.0;
|
let height = if month.count == 0 {
|
||||||
|
8.0
|
||||||
|
} else {
|
||||||
|
24.0 + (month.count as f32 / max_count as f32) * 96.0
|
||||||
|
};
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
text(month.count.to_string()).size(12).color(Color::WHITE),
|
text(month.count.to_string()).size(12).color(Color::WHITE),
|
||||||
@@ -231,16 +228,21 @@ fn timeline_chart<'a>(months: &'a [DashboardTimelineMonth], locale: UiLocale) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn tag_cloud<'a>(tags: &'a [DashboardTag], overflow_count: usize, locale: UiLocale) -> Element<'a, Message> {
|
fn tag_cloud<'a>(tags: &'a [DashboardTag], overflow_count: usize, locale: UiLocale) -> Element<'a, Message> {
|
||||||
let items = tags.iter().fold(column![inputs::section_header(&t(locale, "dashboard.tagCloud"))].spacing(8), |column, tag| {
|
let cloud = if tags.is_empty() {
|
||||||
let bg = parse_color(tag.color.as_deref()).unwrap_or(Color::from_rgb(0.18, 0.21, 0.28));
|
row![text(t(locale, "dashboard.noTags")).size(13).color(Color::from_rgb(0.6, 0.62, 0.68))]
|
||||||
column.push(
|
|
||||||
container(
|
|
||||||
row![
|
|
||||||
text(tag.name.clone()).size(scale_font(tag.count)).color(contrast_color(bg)),
|
|
||||||
text(tag.count.to_string()).size(12).color(contrast_color(bg)),
|
|
||||||
]
|
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.align_y(Alignment::Center),
|
.wrap()
|
||||||
|
} else {
|
||||||
|
let words = tags
|
||||||
|
.iter()
|
||||||
|
.map(|tag| {
|
||||||
|
let bg = parse_color(tag.color.as_deref()).unwrap_or(Color::from_rgb(0.18, 0.21, 0.28));
|
||||||
|
let fg = contrast_color(bg);
|
||||||
|
tooltip(
|
||||||
|
container(
|
||||||
|
text(tag.name.clone())
|
||||||
|
.size(scale_font(tag.count))
|
||||||
|
.color(fg),
|
||||||
)
|
)
|
||||||
.padding([6, 10])
|
.padding([6, 10])
|
||||||
.style(move |_: &Theme| container::Style {
|
.style(move |_: &Theme| container::Style {
|
||||||
@@ -248,16 +250,29 @@ fn tag_cloud<'a>(tags: &'a [DashboardTag], overflow_count: usize, locale: UiLoca
|
|||||||
border: iced::Border { radius: 999.0.into(), ..iced::Border::default() },
|
border: iced::Border { radius: 999.0.into(), ..iced::Border::default() },
|
||||||
..container::Style::default()
|
..container::Style::default()
|
||||||
}),
|
}),
|
||||||
|
text(format!("{} {}", tag.name, tag.count)).size(12),
|
||||||
|
tooltip::Position::Top,
|
||||||
)
|
)
|
||||||
});
|
.into()
|
||||||
|
})
|
||||||
let with_overflow = if overflow_count > 0 {
|
.collect::<Vec<_>>();
|
||||||
items.push(text(format!("{} {}", t(locale, "dashboard.and"), overflow_count)).size(12).color(Color::from_rgb(0.6, 0.62, 0.68)))
|
row(words).spacing(8).wrap()
|
||||||
} else {
|
|
||||||
items
|
|
||||||
};
|
};
|
||||||
|
|
||||||
container(with_overflow).width(Length::FillPortion(1)).into()
|
let content = if overflow_count > 0 {
|
||||||
|
column![
|
||||||
|
inputs::section_header(&t(locale, "dashboard.tagCloud")),
|
||||||
|
cloud,
|
||||||
|
text(format!("{} {}", t(locale, "dashboard.and"), overflow_count))
|
||||||
|
.size(12)
|
||||||
|
.color(Color::from_rgb(0.6, 0.62, 0.68)),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
} else {
|
||||||
|
column![inputs::section_header(&t(locale, "dashboard.tagCloud")), cloud].spacing(8)
|
||||||
|
};
|
||||||
|
|
||||||
|
container(content).width(Length::FillPortion(1)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn category_cloud<'a>(categories: &'a [DashboardCategory], locale: UiLocale) -> Element<'a, Message> {
|
fn category_cloud<'a>(categories: &'a [DashboardCategory], locale: UiLocale) -> Element<'a, Message> {
|
||||||
|
|||||||
@@ -337,6 +337,7 @@
|
|||||||
"dashboard.timeline": "Zeitachse",
|
"dashboard.timeline": "Zeitachse",
|
||||||
"dashboard.noTimelineData": "Noch keine Zeitachsendaten",
|
"dashboard.noTimelineData": "Noch keine Zeitachsendaten",
|
||||||
"dashboard.tagCloud": "Tag-Wolke",
|
"dashboard.tagCloud": "Tag-Wolke",
|
||||||
|
"dashboard.noTags": "Noch keine Tags",
|
||||||
"dashboard.categoryCloud": "Kategorie-Wolke",
|
"dashboard.categoryCloud": "Kategorie-Wolke",
|
||||||
"dashboard.recentPosts": "Letzte Beiträge",
|
"dashboard.recentPosts": "Letzte Beiträge",
|
||||||
"dashboard.noRecentPosts": "Keine aktuellen Beiträge",
|
"dashboard.noRecentPosts": "Keine aktuellen Beiträge",
|
||||||
|
|||||||
@@ -337,6 +337,7 @@
|
|||||||
"dashboard.timeline": "Timeline",
|
"dashboard.timeline": "Timeline",
|
||||||
"dashboard.noTimelineData": "No timeline data yet",
|
"dashboard.noTimelineData": "No timeline data yet",
|
||||||
"dashboard.tagCloud": "Tag Cloud",
|
"dashboard.tagCloud": "Tag Cloud",
|
||||||
|
"dashboard.noTags": "No tags yet",
|
||||||
"dashboard.categoryCloud": "Category Cloud",
|
"dashboard.categoryCloud": "Category Cloud",
|
||||||
"dashboard.recentPosts": "Recent Posts",
|
"dashboard.recentPosts": "Recent Posts",
|
||||||
"dashboard.noRecentPosts": "No recent posts",
|
"dashboard.noRecentPosts": "No recent posts",
|
||||||
|
|||||||
@@ -337,6 +337,7 @@
|
|||||||
"dashboard.timeline": "Cronología",
|
"dashboard.timeline": "Cronología",
|
||||||
"dashboard.noTimelineData": "Todavía no hay datos de cronología",
|
"dashboard.noTimelineData": "Todavía no hay datos de cronología",
|
||||||
"dashboard.tagCloud": "Nube de etiquetas",
|
"dashboard.tagCloud": "Nube de etiquetas",
|
||||||
|
"dashboard.noTags": "Todavía no hay etiquetas",
|
||||||
"dashboard.categoryCloud": "Nube de categorías",
|
"dashboard.categoryCloud": "Nube de categorías",
|
||||||
"dashboard.recentPosts": "Publicaciones recientes",
|
"dashboard.recentPosts": "Publicaciones recientes",
|
||||||
"dashboard.noRecentPosts": "No hay publicaciones recientes",
|
"dashboard.noRecentPosts": "No hay publicaciones recientes",
|
||||||
|
|||||||
@@ -337,6 +337,7 @@
|
|||||||
"dashboard.timeline": "Chronologie",
|
"dashboard.timeline": "Chronologie",
|
||||||
"dashboard.noTimelineData": "Aucune donnée de chronologie",
|
"dashboard.noTimelineData": "Aucune donnée de chronologie",
|
||||||
"dashboard.tagCloud": "Nuage de tags",
|
"dashboard.tagCloud": "Nuage de tags",
|
||||||
|
"dashboard.noTags": "Aucun tag pour le moment",
|
||||||
"dashboard.categoryCloud": "Nuage de catégories",
|
"dashboard.categoryCloud": "Nuage de catégories",
|
||||||
"dashboard.recentPosts": "Articles récents",
|
"dashboard.recentPosts": "Articles récents",
|
||||||
"dashboard.noRecentPosts": "Aucun article récent",
|
"dashboard.noRecentPosts": "Aucun article récent",
|
||||||
|
|||||||
@@ -337,6 +337,7 @@
|
|||||||
"dashboard.timeline": "Cronologia",
|
"dashboard.timeline": "Cronologia",
|
||||||
"dashboard.noTimelineData": "Nessun dato cronologico disponibile",
|
"dashboard.noTimelineData": "Nessun dato cronologico disponibile",
|
||||||
"dashboard.tagCloud": "Nuvola di tag",
|
"dashboard.tagCloud": "Nuvola di tag",
|
||||||
|
"dashboard.noTags": "Nessun tag per ora",
|
||||||
"dashboard.categoryCloud": "Nuvola di categorie",
|
"dashboard.categoryCloud": "Nuvola di categorie",
|
||||||
"dashboard.recentPosts": "Post recenti",
|
"dashboard.recentPosts": "Post recenti",
|
||||||
"dashboard.noRecentPosts": "Nessun post recente",
|
"dashboard.noRecentPosts": "Nessun post recente",
|
||||||
|
|||||||
Reference in New Issue
Block a user