feat: sidebar filtering
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use iced::widget::{button, column, container, scrollable, text, Space};
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||
|
||||
@@ -8,6 +8,7 @@ use bds_core::model::{Media, Post, Script, Template};
|
||||
use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::SidebarView;
|
||||
use crate::state::sidebar_filter::{CalendarYear, MediaFilter, PostFilter};
|
||||
use crate::state::tabs::{Tab, TabType};
|
||||
|
||||
/// Sidebar container style — dark background.
|
||||
@@ -118,12 +119,366 @@ fn format_post_date(unix_ms: i64) -> String {
|
||||
dt.format("%b %d, %Y").to_string()
|
||||
}
|
||||
|
||||
/// Search input style.
|
||||
fn search_input_style(_theme: &Theme, _status: text_input::Status) -> text_input::Style {
|
||||
text_input::Style {
|
||||
background: Background::Color(Color::from_rgb(0.12, 0.12, 0.15)),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||
width: 1.0,
|
||||
radius: 3.0.into(),
|
||||
},
|
||||
icon: Color::from_rgb(0.50, 0.50, 0.55),
|
||||
placeholder: Color::from_rgb(0.45, 0.45, 0.50),
|
||||
value: Color::from_rgb(0.85, 0.85, 0.90),
|
||||
selection: Color::from_rgba(0.35, 0.55, 0.85, 0.4),
|
||||
}
|
||||
}
|
||||
|
||||
/// Style for filter toggle / clear buttons in the header.
|
||||
fn filter_button_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.25, 0.25, 0.30),
|
||||
_ => Color::TRANSPARENT,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.60, 0.60, 0.65),
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Active filter toggle button.
|
||||
fn filter_button_active_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.25, 0.30, 0.40),
|
||||
_ => Color::from_rgb(0.20, 0.25, 0.35),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.55, 0.70, 0.95),
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tag/category chip style (unselected).
|
||||
fn chip_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.24, 0.24, 0.30),
|
||||
_ => Color::from_rgb(0.18, 0.18, 0.22),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.70, 0.70, 0.75),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.30, 0.30, 0.35),
|
||||
width: 1.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tag/category chip style (selected).
|
||||
fn chip_selected_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.25, 0.35, 0.55),
|
||||
_ => Color::from_rgb(0.20, 0.30, 0.50),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.85, 0.90, 1.0),
|
||||
border: Border {
|
||||
color: Color::from_rgb(0.40, 0.55, 0.80),
|
||||
width: 1.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calendar year/month button style.
|
||||
fn calendar_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.22, 0.22, 0.27),
|
||||
_ => Color::TRANSPARENT,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.70, 0.70, 0.75),
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Calendar year/month button style (selected).
|
||||
fn calendar_selected_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let bg = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.25, 0.30, 0.40),
|
||||
_ => Color::from_rgb(0.20, 0.25, 0.35),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(bg)),
|
||||
text_color: Color::from_rgb(0.55, 0.70, 0.95),
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// "Clear All Filters" button style.
|
||||
fn clear_filters_style(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
let color = match status {
|
||||
button::Status::Hovered => Color::from_rgb(0.90, 0.50, 0.50),
|
||||
_ => Color::from_rgb(0.75, 0.45, 0.45),
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: color,
|
||||
border: Border::default(),
|
||||
..button::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Month name abbreviation for calendar display.
|
||||
fn month_abbr(month: u32) -> &'static str {
|
||||
match month {
|
||||
1 => "Jan", 2 => "Feb", 3 => "Mar", 4 => "Apr",
|
||||
5 => "May", 6 => "Jun", 7 => "Jul", 8 => "Aug",
|
||||
9 => "Sep", 10 => "Oct", 11 => "Nov", 12 => "Dec",
|
||||
_ => "???",
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the calendar archive tree widget.
|
||||
fn calendar_widget(
|
||||
years: &[CalendarYear],
|
||||
selected_year: Option<i32>,
|
||||
selected_month: Option<u32>,
|
||||
on_year: impl Fn(Option<i32>) -> Message + 'static + Clone,
|
||||
on_month: impl Fn(Option<u32>) -> 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();
|
||||
|
||||
let section_label = text(t(locale, "sidebar.filter.calendar"))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted);
|
||||
items.push(section_label.into());
|
||||
|
||||
for cy in years {
|
||||
let year_selected = selected_year == Some(cy.year);
|
||||
let total: usize = cy.months.iter().map(|m| m.count).sum();
|
||||
let label = format!("{} ({})", cy.year, total);
|
||||
let year_val = cy.year;
|
||||
let on_year_clone = on_year.clone();
|
||||
let style_fn = if year_selected { calendar_selected_style } else { calendar_style };
|
||||
let year_btn = button(
|
||||
text(label).size(11).shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(if year_selected {
|
||||
on_year_clone(None)
|
||||
} else {
|
||||
on_year_clone(Some(year_val))
|
||||
})
|
||||
.padding([2, 4])
|
||||
.style(style_fn);
|
||||
items.push(year_btn.into());
|
||||
|
||||
// Show months only when this year is selected
|
||||
if year_selected {
|
||||
for cm in &cy.months {
|
||||
let month_selected = selected_month == Some(cm.month);
|
||||
let label = format!(" {} ({})", month_abbr(cm.month), cm.count);
|
||||
let month_val = cm.month;
|
||||
let on_month_clone = on_month.clone();
|
||||
let style_fn = if month_selected { calendar_selected_style } else { calendar_style };
|
||||
let month_btn = button(
|
||||
text(label).size(10).shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(if month_selected {
|
||||
on_month_clone(None)
|
||||
} else {
|
||||
on_month_clone(Some(month_val))
|
||||
})
|
||||
.padding([1, 4])
|
||||
.style(style_fn);
|
||||
items.push(month_btn.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(items).spacing(1).into()
|
||||
}
|
||||
|
||||
/// Build chip selector for tags or categories.
|
||||
fn chip_selector(
|
||||
label: &str,
|
||||
available: &[String],
|
||||
selected: &[String],
|
||||
on_toggle: impl Fn(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();
|
||||
|
||||
let section_label = text(t(locale, label))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted);
|
||||
items.push(section_label.into());
|
||||
|
||||
// Wrap chips in rows
|
||||
let mut chip_row: Vec<Element<'static, Message>> = Vec::new();
|
||||
for tag in available {
|
||||
let is_selected = selected.contains(tag);
|
||||
let tag_clone = tag.clone();
|
||||
let on_toggle_clone = on_toggle.clone();
|
||||
let style_fn = if is_selected { chip_selected_style } else { chip_style };
|
||||
let chip = button(
|
||||
text(tag.clone()).size(10).shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(on_toggle_clone(tag_clone))
|
||||
.padding([2, 6])
|
||||
.style(style_fn);
|
||||
chip_row.push(chip.into());
|
||||
}
|
||||
|
||||
if !chip_row.is_empty() {
|
||||
let mut collected: Vec<Element<'static, Message>> = chip_row.into_iter().collect();
|
||||
while !collected.is_empty() {
|
||||
let chunk_size = 3.min(collected.len());
|
||||
let chunk: Vec<Element<'static, Message>> = collected.drain(..chunk_size).collect();
|
||||
items.push(
|
||||
iced::widget::Row::with_children(chunk).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,
|
||||
is_pages: bool,
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let mut sections: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
// Calendar archive
|
||||
if !filter.calendar_years.is_empty() {
|
||||
sections.push(calendar_widget(
|
||||
&filter.calendar_years,
|
||||
filter.calendar.selected_year,
|
||||
filter.calendar.selected_month,
|
||||
|y| Message::SetPostCalendarYear(y),
|
||||
|m| Message::SetPostCalendarMonth(m),
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
// Tag chips
|
||||
if !filter.available_tags.is_empty() {
|
||||
sections.push(chip_selector(
|
||||
"sidebar.filter.tags",
|
||||
&filter.available_tags,
|
||||
&filter.tag_filter,
|
||||
|tag| Message::TogglePostTagFilter(tag),
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
// Category chips (not shown for Pages since Pages IS a category filter)
|
||||
if !is_pages && !filter.available_categories.is_empty() {
|
||||
sections.push(chip_selector(
|
||||
"sidebar.filter.categories",
|
||||
&filter.available_categories,
|
||||
&filter.category_filter,
|
||||
|cat| Message::TogglePostCategoryFilter(cat),
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
// Clear all filters button
|
||||
if filter.has_active_filters() {
|
||||
sections.push(
|
||||
button(
|
||||
text(t(locale, "sidebar.filter.clearAll"))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(Message::ClearPostFilters)
|
||||
.padding([2, 4])
|
||||
.style(clear_filters_style)
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(sections).spacing(2).into()
|
||||
}
|
||||
|
||||
/// Build the filter panel for Media view.
|
||||
fn media_filter_panel(
|
||||
filter: &MediaFilter,
|
||||
locale: UiLocale,
|
||||
) -> Element<'static, Message> {
|
||||
let mut sections: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
if !filter.calendar_years.is_empty() {
|
||||
sections.push(calendar_widget(
|
||||
&filter.calendar_years,
|
||||
filter.calendar.selected_year,
|
||||
filter.calendar.selected_month,
|
||||
|y| Message::SetMediaCalendarYear(y),
|
||||
|m| Message::SetMediaCalendarMonth(m),
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
if !filter.available_tags.is_empty() {
|
||||
sections.push(chip_selector(
|
||||
"sidebar.filter.tags",
|
||||
&filter.available_tags,
|
||||
&filter.tag_filter,
|
||||
|tag| Message::ToggleMediaTagFilter(tag),
|
||||
locale,
|
||||
));
|
||||
sections.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
if filter.has_active_filters() {
|
||||
sections.push(
|
||||
button(
|
||||
text(t(locale, "sidebar.filter.clearAll"))
|
||||
.size(10)
|
||||
.shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(Message::ClearMediaFilters)
|
||||
.padding([2, 4])
|
||||
.style(clear_filters_style)
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(sections).spacing(2).into()
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
sidebar_view: SidebarView,
|
||||
posts: &[Post],
|
||||
media: &[Media],
|
||||
scripts: &[Script],
|
||||
templates: &[Template],
|
||||
post_filter: &PostFilter,
|
||||
media_filter: &MediaFilter,
|
||||
width: f32,
|
||||
active_tab: Option<&str>,
|
||||
locale: UiLocale,
|
||||
@@ -137,13 +492,66 @@ pub fn view(
|
||||
.color(Color::from_rgb(0.85, 0.85, 0.90));
|
||||
|
||||
let body: Element<'static, Message> = match sidebar_view {
|
||||
SidebarView::Posts => {
|
||||
SidebarView::Posts | SidebarView::Pages => {
|
||||
let is_pages = sidebar_view == SidebarView::Pages;
|
||||
let filter = post_filter;
|
||||
|
||||
let mut top_items: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
// Search input + filter toggle row
|
||||
let search = text_input(
|
||||
&t(locale, "sidebar.filter.search"),
|
||||
&filter.search_query,
|
||||
)
|
||||
.on_input(Message::PostSearchChanged)
|
||||
.size(11)
|
||||
.padding([4, 6])
|
||||
.width(Length::Fill)
|
||||
.style(search_input_style);
|
||||
|
||||
let has_filters = filter.has_active_filters();
|
||||
let toggle_style = if filter.filter_panel_visible || has_filters {
|
||||
filter_button_active_style
|
||||
} else {
|
||||
filter_button_style
|
||||
};
|
||||
let toggle_label = if has_filters {
|
||||
"\u{2B50}" // ⭐ indicates active filters
|
||||
} else {
|
||||
"\u{25BC}" // ▼ toggle icon
|
||||
};
|
||||
let filter_toggle = button(
|
||||
text(toggle_label).size(11).shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(Message::TogglePostFilterPanel)
|
||||
.padding([4, 6])
|
||||
.style(toggle_style);
|
||||
|
||||
top_items.push(
|
||||
row![search, filter_toggle].spacing(4).into()
|
||||
);
|
||||
|
||||
// Filter panel (collapsible)
|
||||
if filter.filter_panel_visible {
|
||||
top_items.push(Space::with_height(4.0).into());
|
||||
top_items.push(post_filter_panel(filter, is_pages, locale));
|
||||
top_items.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
// Post list
|
||||
if posts.is_empty() {
|
||||
text(t(locale, placeholder_key(sidebar_view)))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted)
|
||||
.into()
|
||||
let key = if has_filters {
|
||||
"sidebar.filter.noResults"
|
||||
} else {
|
||||
placeholder_key(sidebar_view)
|
||||
};
|
||||
top_items.push(
|
||||
text(t(locale, key))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted)
|
||||
.into()
|
||||
);
|
||||
} else {
|
||||
let section_header = |label: &str| -> Element<'static, Message> {
|
||||
text(label.to_string())
|
||||
@@ -155,18 +563,14 @@ pub fn view(
|
||||
|
||||
let make_post_item = |p: &Post| -> Element<'static, Message> {
|
||||
let is_active = active_tab == Some(p.id.as_str());
|
||||
// Per sidebar_views.allium PostTypeIcon
|
||||
let icon = post_type_icon(&p.categories);
|
||||
let status_indicator = match p.status {
|
||||
bds_core::model::PostStatus::Draft => "\u{25CB}",
|
||||
bds_core::model::PostStatus::Published => "\u{25CF}",
|
||||
bds_core::model::PostStatus::Archived => "\u{25A1}",
|
||||
};
|
||||
// Per sidebar_views.allium PostDateFormat
|
||||
let date = format_post_date(p.created_at);
|
||||
// Truncate title to fit sidebar width, accounting for
|
||||
// padding and the status/icon prefix.
|
||||
let prefix_chars: usize = 5; // icon + space + status + space
|
||||
let prefix_chars: usize = 5;
|
||||
let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX
|
||||
- (prefix_chars as f32 * AVG_CHAR_WIDTH_PX);
|
||||
let display_title = truncate_to_fit(&p.title, text_px);
|
||||
@@ -198,62 +602,106 @@ pub fn view(
|
||||
.into()
|
||||
};
|
||||
|
||||
let mut sections: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
// Draft section
|
||||
let drafts: Vec<&Post> = posts.iter().filter(|p| p.status == bds_core::model::PostStatus::Draft).collect();
|
||||
if !drafts.is_empty() {
|
||||
sections.push(section_header(&t(locale, "sidebar.drafts")));
|
||||
top_items.push(section_header(&t(locale, "sidebar.drafts")));
|
||||
for p in &drafts {
|
||||
sections.push(make_post_item(p));
|
||||
top_items.push(make_post_item(p));
|
||||
}
|
||||
sections.push(Space::with_height(6.0).into());
|
||||
top_items.push(Space::with_height(6.0).into());
|
||||
}
|
||||
|
||||
// Published section
|
||||
let published: Vec<&Post> = posts.iter().filter(|p| p.status == bds_core::model::PostStatus::Published).collect();
|
||||
if !published.is_empty() {
|
||||
sections.push(section_header(&t(locale, "sidebar.published")));
|
||||
top_items.push(section_header(&t(locale, "sidebar.published")));
|
||||
for p in &published {
|
||||
sections.push(make_post_item(p));
|
||||
top_items.push(make_post_item(p));
|
||||
}
|
||||
sections.push(Space::with_height(6.0).into());
|
||||
top_items.push(Space::with_height(6.0).into());
|
||||
}
|
||||
|
||||
// Archived section
|
||||
let archived: Vec<&Post> = posts.iter().filter(|p| p.status == bds_core::model::PostStatus::Archived).collect();
|
||||
if !archived.is_empty() {
|
||||
sections.push(section_header(&t(locale, "sidebar.archived")));
|
||||
top_items.push(section_header(&t(locale, "sidebar.archived")));
|
||||
for p in &archived {
|
||||
sections.push(make_post_item(p));
|
||||
top_items.push(make_post_item(p));
|
||||
}
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(sections)
|
||||
.spacing(1)
|
||||
.into()
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(top_items)
|
||||
.spacing(1)
|
||||
.into()
|
||||
}
|
||||
SidebarView::Media => {
|
||||
let filter = media_filter;
|
||||
let mut top_items: Vec<Element<'static, Message>> = Vec::new();
|
||||
|
||||
// Search input + filter toggle row
|
||||
let search = text_input(
|
||||
&t(locale, "sidebar.filter.search"),
|
||||
&filter.search_query,
|
||||
)
|
||||
.on_input(Message::MediaSearchChanged)
|
||||
.size(11)
|
||||
.padding([4, 6])
|
||||
.width(Length::Fill)
|
||||
.style(search_input_style);
|
||||
|
||||
let has_filters = filter.has_active_filters();
|
||||
let toggle_style = if filter.filter_panel_visible || has_filters {
|
||||
filter_button_active_style
|
||||
} else {
|
||||
filter_button_style
|
||||
};
|
||||
let toggle_label = if has_filters {
|
||||
"\u{2B50}" // ⭐ indicates active filters
|
||||
} else {
|
||||
"\u{25BC}" // ▼ toggle icon
|
||||
};
|
||||
let filter_toggle = button(
|
||||
text(toggle_label).size(11).shaping(Shaping::Advanced)
|
||||
)
|
||||
.on_press(Message::ToggleMediaFilterPanel)
|
||||
.padding([4, 6])
|
||||
.style(toggle_style);
|
||||
|
||||
top_items.push(
|
||||
row![search, filter_toggle].spacing(4).into()
|
||||
);
|
||||
|
||||
// Filter panel (collapsible)
|
||||
if filter.filter_panel_visible {
|
||||
top_items.push(Space::with_height(4.0).into());
|
||||
top_items.push(media_filter_panel(filter, locale));
|
||||
top_items.push(Space::with_height(4.0).into());
|
||||
}
|
||||
|
||||
if media.is_empty() {
|
||||
text(t(locale, placeholder_key(sidebar_view)))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted)
|
||||
.into()
|
||||
let key = if has_filters {
|
||||
"sidebar.filter.noResults"
|
||||
} else {
|
||||
placeholder_key(sidebar_view)
|
||||
};
|
||||
top_items.push(
|
||||
text(t(locale, key))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(muted)
|
||||
.into()
|
||||
);
|
||||
} else {
|
||||
let items: Vec<Element<'static, Message>> = media
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let is_active = active_tab == Some(m.id.as_str());
|
||||
// Per sidebar_views.allium MediaGridItem: title truncated to 60 chars + "..."
|
||||
// if over limit; fallback originalName (no truncation).
|
||||
// Additionally truncate to fit sidebar width.
|
||||
let display_name = match m.title.as_deref() {
|
||||
Some(title) => truncate_media_title(title),
|
||||
None => m.original_name.clone(),
|
||||
};
|
||||
// Emoji prefix "🖼 " is ~2 chars wide
|
||||
let text_px = width - SIDEBAR_TEXT_OVERHEAD_PX - 16.0;
|
||||
let display_name = truncate_to_fit(&display_name, text_px);
|
||||
let label = format!("\u{1F5BC} {display_name}");
|
||||
@@ -280,10 +728,12 @@ pub fn view(
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
iced::widget::Column::with_children(items)
|
||||
.spacing(1)
|
||||
.into()
|
||||
top_items.extend(items);
|
||||
}
|
||||
|
||||
iced::widget::Column::with_children(top_items)
|
||||
.spacing(1)
|
||||
.into()
|
||||
}
|
||||
SidebarView::Scripts => {
|
||||
if scripts.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user