From 4fbf9b9e9dbb590fe4dce974eeea8c286db74a8d Mon Sep 17 00:00:00 2001 From: tsukinaha Date: Sat, 21 Dec 2024 22:54:39 +0800 Subject: [PATCH] feat: impl filter panel Signed-off-by: tsukinaha --- resources/resources.gresource.xml | 2 + resources/ui/filter.ui | 136 ++++++++---- resources/ui/filter_row.ui | 79 +------ resources/ui/filter_search_page.ui | 24 ++ resources/ui/filters_row.ui | 70 ++++++ resources/ui/identify_dialog.ui | 2 +- resources/ui/missing_episodes.ui | 5 +- resources/ui/server_row.ui | 2 +- resources/ui/window.ui | 6 +- src/arg.rs | 4 +- src/client/emby_client.rs | 17 ++ src/client/structs.rs | 2 +- src/macros.rs | 16 +- src/ui/widgets/filter_panel/dialog.rs | 18 +- src/ui/widgets/filter_panel/filter_label.rs | 24 +- src/ui/widgets/filter_panel/filter_row.rs | 77 ++++--- src/ui/widgets/filter_panel/filters_row.rs | 153 +++++++++++++ src/ui/widgets/filter_panel/mod.rs | 4 + src/ui/widgets/filter_panel/search_page.rs | 233 ++++++++++++++++++++ src/ui/widgets/identify/dialog.rs | 8 +- src/ui/widgets/search.rs | 5 +- src/ui/widgets/single_grid.rs | 5 +- src/ui/widgets/tu_item/action.rs | 11 +- 23 files changed, 724 insertions(+), 179 deletions(-) create mode 100644 resources/ui/filter_search_page.ui create mode 100644 resources/ui/filters_row.ui create mode 100644 src/ui/widgets/filter_panel/filters_row.rs create mode 100644 src/ui/widgets/filter_panel/search_page.rs diff --git a/resources/resources.gresource.xml b/resources/resources.gresource.xml index 17c752ec..d4361b73 100644 --- a/resources/resources.gresource.xml +++ b/resources/resources.gresource.xml @@ -88,6 +88,8 @@ ui/theme_switcher.ui ui/filter.ui ui/filter_row.ui + ui/filters_row.ui ui/filter_label.ui + ui/filter_search_page.ui diff --git a/resources/ui/filter.ui b/resources/ui/filter.ui index 5cfb57ce..4d5e8ed2 100644 --- a/resources/ui/filter.ui +++ b/resources/ui/filter.ui @@ -7,25 +7,25 @@ - + + crossfade - - main - Filter Panel + + view + Filter Panel - - - - - + - - crossfade - - - view - Filter Panel - + + main + Filter Panel + + + + + + + never @@ -43,6 +43,7 @@ Playback Status + True @@ -59,7 +60,7 @@ Favourite check - + @@ -151,30 +189,30 @@ - + - - - - loading - Loading - - - True - center - center - 32 - 32 - - - - + + + + loading + Loading + + + True + center + center + 32 + 32 + + + + diff --git a/resources/ui/filter_row.ui b/resources/ui/filter_row.ui index 13d13769..456b4ae8 100644 --- a/resources/ui/filter_row.ui +++ b/resources/ui/filter_row.ui @@ -1,73 +1,16 @@ - + diff --git a/resources/ui/filter_search_page.ui b/resources/ui/filter_search_page.ui new file mode 100644 index 00000000..3dba6e88 --- /dev/null +++ b/resources/ui/filter_search_page.ui @@ -0,0 +1,24 @@ + + + + diff --git a/resources/ui/filters_row.ui b/resources/ui/filters_row.ui new file mode 100644 index 00000000..55b7deb2 --- /dev/null +++ b/resources/ui/filters_row.ui @@ -0,0 +1,70 @@ + + + + \ No newline at end of file diff --git a/resources/ui/identify_dialog.ui b/resources/ui/identify_dialog.ui index 520f6ddf..16f1f02c 100644 --- a/resources/ui/identify_dialog.ui +++ b/resources/ui/identify_dialog.ui @@ -5,7 +5,7 @@ 1000 Identify - + diff --git a/resources/ui/missing_episodes.ui b/resources/ui/missing_episodes.ui index d2bd143c..842c05c8 100644 --- a/resources/ui/missing_episodes.ui +++ b/resources/ui/missing_episodes.ui @@ -44,7 +44,10 @@ never - + + none + true + diff --git a/resources/ui/server_row.ui b/resources/ui/server_row.ui index 44abfcd1..26b92a3d 100644 --- a/resources/ui/server_row.ui +++ b/resources/ui/server_row.ui @@ -32,7 +32,7 @@ end 90 fill - 9 + 3 diff --git a/resources/ui/window.ui b/resources/ui/window.ui index 4df798cf..09f0b03e 100644 --- a/resources/ui/window.ui +++ b/resources/ui/window.ui @@ -299,7 +299,7 @@ end 90 fill - 9 + 3 Home @@ -345,7 +345,7 @@ end 90 fill - 9 + 3 Liked @@ -391,7 +391,7 @@ end 90 fill - 9 + 3 Search diff --git a/src/arg.rs b/src/arg.rs index fa65337c..df47f47f 100644 --- a/src/arg.rs +++ b/src/arg.rs @@ -15,7 +15,9 @@ use tracing_subscriber::fmt::time::ChronoLocal; use crate::dyn_event; -const DEFAULT_RENDERER: &str = "gl"; +/// gl renderer will glitch on fractional scaling +/// vulkan renderer has poor performance +const DEFAULT_RENDERER: &str = "ngl"; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] diff --git a/src/client/emby_client.rs b/src/client/emby_client.rs index 7fd0fefe..dcf36a14 100644 --- a/src/client/emby_client.rs +++ b/src/client/emby_client.rs @@ -45,6 +45,7 @@ use super::{ Back, DeleteInfo, ExternalIdInfo, + FilterList, ImageItem, ImageSearchResult, List, @@ -1315,6 +1316,22 @@ impl EmbyClient { self.post("items/metadata/reset", &[], json!({"Ids": ids})) .await } + + pub async fn filters(&self, type_: &str) -> Result { + let params = [ + ("SortBy", "SortName"), + ("SortOrder", "Ascending"), + ("Recursive", "true"), + ("EnableImages", "false"), + ("EnableUserData", "false"), + ( + "IncludeItemTypes", + "Movie,Series,Episode,BoxSet,Person,MusicAlbum,Audio,Video", + ), + ("userId", &self.user_id()), + ]; + self.request(type_, ¶ms).await + } } #[cfg(test)] diff --git a/src/client/structs.rs b/src/client/structs.rs index 3bac640a..847b24f7 100644 --- a/src/client/structs.rs +++ b/src/client/structs.rs @@ -582,7 +582,7 @@ pub struct FilterList { pub total_record_count: Option, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq)] pub struct FilterItem { #[serde(rename = "Name")] pub name: String, diff --git a/src/macros.rs b/src/macros.rs index dd035d61..4d42ece1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -3,7 +3,11 @@ macro_rules! _add_toast { ($widget:expr, $toast:expr) => {{ use gtk::prelude::WidgetExt; - use $crate::ui::widgets::image_dialog::ImageDialog; + use $crate::ui::widgets::{ + filter_panel::FilterPanelDialog, + identify::IdentifyDialog, + image_dialog::ImageDialog, + }; if let Some(dialog) = $widget .ancestor(adw::PreferencesDialog::static_type()) .and_downcast::() @@ -15,6 +19,16 @@ macro_rules! _add_toast { .and_downcast::() { overlay.add_toast($toast); + } else if let Some(dialog) = $widget + .ancestor(FilterPanelDialog::static_type()) + .and_downcast::() + { + dialog.add_toast($toast); + } else if let Some(dialog) = $widget + .ancestor(IdentifyDialog::static_type()) + .and_downcast::() + { + dialog.add_toast($toast); } else if let Some(dialog) = $widget .ancestor(ImageDialog::static_type()) .and_downcast::() diff --git a/src/ui/widgets/filter_panel/dialog.rs b/src/ui/widgets/filter_panel/dialog.rs index 8f8cb1fe..8756bafe 100644 --- a/src/ui/widgets/filter_panel/dialog.rs +++ b/src/ui/widgets/filter_panel/dialog.rs @@ -1,4 +1,8 @@ -use adw::subclass::prelude::*; +use super::FiltersRow; +use adw::{ + prelude::*, + subclass::prelude::*, +}; use gtk::{ glib, template_callbacks, @@ -13,7 +17,6 @@ mod imp { }; use super::*; - use crate::ui::widgets::filter_panel::FilterRow; #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/moe/tsuna/tsukimi/ui/filter.ui")] @@ -24,6 +27,8 @@ mod imp { pub toast_overlay: TemplateChild, #[template_child] pub apply_button_row: TemplateChild, + #[template_child] + pub navigation_view: TemplateChild, } #[glib::object_subclass] @@ -33,7 +38,7 @@ mod imp { type ParentType = adw::Dialog; fn class_init(klass: &mut Self::Class) { - FilterRow::ensure_type(); + FiltersRow::ensure_type(); klass.bind_template(); klass.bind_template_instance_callbacks(); } @@ -89,4 +94,11 @@ impl FilterPanelDialog { pub fn connect_applied(&self, f: F) -> glib::SignalHandlerId { self.imp().apply_button_row.connect_activated(f) } + + pub fn push_page

(&self, page: &P) + where + P: IsA, + { + self.imp().navigation_view.push(page); + } } diff --git a/src/ui/widgets/filter_panel/filter_label.rs b/src/ui/widgets/filter_panel/filter_label.rs index 5c29bcf0..4ff3bcad 100644 --- a/src/ui/widgets/filter_panel/filter_label.rs +++ b/src/ui/widgets/filter_panel/filter_label.rs @@ -6,6 +6,10 @@ use gtk::{ CompositeTemplate, }; +use crate::client::structs::FilterItem; + +use super::FiltersRow; + mod imp { use std::cell::RefCell; @@ -19,6 +23,10 @@ mod imp { pub struct FilterLabel { #[property(get, set, nullable)] pub label: RefCell>, + #[property(get, set)] + pub name: RefCell, + #[property(get, set, nullable)] + pub id: RefCell>, #[property(get, set, nullable)] pub icon_name: RefCell>, } @@ -73,14 +81,20 @@ impl FilterLabel { #[template_callback] fn on_delete_button_clicked(&self) { - let Some(flowbox) = self - .parent() - .and_then(|p| p.parent()) - .and_downcast::() + let Some(fillter_row) = self + .ancestor(FiltersRow::static_type()) + .and_downcast::() else { return; }; - flowbox.remove(self); + fillter_row.remove_filter(self.filter_item()); + } + + pub fn filter_item(&self) -> FilterItem { + FilterItem { + name: self.name(), + id: self.id(), + } } } diff --git a/src/ui/widgets/filter_panel/filter_row.rs b/src/ui/widgets/filter_panel/filter_row.rs index 9322ea22..35bea5b1 100644 --- a/src/ui/widgets/filter_panel/filter_row.rs +++ b/src/ui/widgets/filter_panel/filter_row.rs @@ -1,39 +1,43 @@ use adw::subclass::prelude::*; use gtk::{ glib, - template_callbacks, + prelude::*, CompositeTemplate, }; +use crate::client::structs::FilterItem; + +use super::FilterDialogSearchPage; + mod imp { use std::cell::RefCell; - use glib::subclass::InitializingObject; + use glib::{ + subclass::InitializingObject, + Properties, + }; use gtk::prelude::*; - use crate::client::structs::FilterItem; - use super::*; - #[derive(Default, CompositeTemplate, glib::Properties)] + #[derive(Debug, Default, CompositeTemplate, Properties)] #[template(resource = "/moe/tsuna/tsukimi/ui/filter_row.ui")] #[properties(wrapper_type = super::FilterRow)] pub struct FilterRow { + #[property(get, set)] + pub name: RefCell, #[property(get, set, nullable)] - pub title: RefCell>, - #[property(get, set, nullable)] - pub icon_name: RefCell>, - #[template_child] - pub flowbox: TemplateChild, + pub id: RefCell>, - pub filter_list: RefCell>, + #[template_child] + pub check: TemplateChild, } #[glib::object_subclass] impl ObjectSubclass for FilterRow { const NAME: &'static str = "FilterRow"; type Type = super::FilterRow; - type ParentType = adw::PreferencesRow; + type ParentType = adw::ActionRow; fn class_init(klass: &mut Self::Class) { Self::bind_template(klass); @@ -49,34 +53,49 @@ mod imp { impl ObjectImpl for FilterRow {} impl WidgetImpl for FilterRow {} - impl ListBoxRowImpl for FilterRow {} - impl PreferencesRowImpl for FilterRow {} + impl ActionRowImpl for FilterRow {} } glib::wrapper! { pub struct FilterRow(ObjectSubclass) - @extends gtk::Widget, gtk::ListBoxRow, adw::ActionRow, adw::PreferencesRow, @implements gtk::Actionable, gtk::Accessible; -} - -impl Default for FilterRow { - fn default() -> Self { - Self::new() - } + @extends gtk::Widget, gtk::ListBoxRow, adw::ActionRow, adw::PreferencesRow, @implements gtk::Accessible; } -#[template_callbacks] +#[gtk::template_callbacks] impl FilterRow { - pub fn new() -> Self { - glib::Object::new() + pub fn new(name: &str, id: Option) -> Self { + glib::Object::builder() + .property("name", name) + .property("id", id) + .build() } #[template_callback] - fn on_add_button_clicked(&self) { - let label = super::FilterLabel::new(); - label.set_label(Some("Test")); - label.set_icon_name(self.icon_name()); - self.imp().flowbox.append(&label); + fn on_check_toggled(&self, check_button: >k::CheckButton) { + let binding = self.ancestor(FilterDialogSearchPage::static_type()); + let Some(search_page) = binding.and_downcast_ref::() else { + return; + }; + + let filter = FilterItem { + id: self.id(), + name: self.name(), + }; + match check_button.is_active() { + true => { + search_page.add_active_rows(self); + search_page.add_filter(filter) + } + false => { + search_page.remove_active_rows(self); + search_page.remove_filter(filter); + } + } + } + + pub fn set_active(&self, active: bool) { + self.imp().check.set_active(active); } } diff --git a/src/ui/widgets/filter_panel/filters_row.rs b/src/ui/widgets/filter_panel/filters_row.rs new file mode 100644 index 00000000..2abb8076 --- /dev/null +++ b/src/ui/widgets/filter_panel/filters_row.rs @@ -0,0 +1,153 @@ +use adw::subclass::prelude::*; +use gst::prelude::{ + CastNone, + StaticType, +}; +use gtk::{ + glib, + prelude::*, + template_callbacks, + CompositeTemplate, +}; + +use crate::client::structs::FilterItem; + +use super::{ + FilterDialogSearchPage, + FilterPanelDialog, +}; + +mod imp { + use std::cell::{ + OnceCell, + RefCell, + }; + + use glib::subclass::InitializingObject; + use gtk::prelude::*; + + use crate::ui::widgets::filter_panel::FilterDialogSearchPage; + + use super::*; + + #[derive(Default, CompositeTemplate, glib::Properties)] + #[template(resource = "/moe/tsuna/tsukimi/ui/filters_row.ui")] + #[properties(wrapper_type = super::FiltersRow)] + pub struct FiltersRow { + #[property(get, set, nullable)] + pub title: RefCell>, + #[property(get, set, nullable)] + pub filter_type: RefCell>, + #[property(get, set, nullable)] + pub icon_name: RefCell>, + + #[template_child] + pub flowbox: TemplateChild, + + pub search_page: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for FiltersRow { + const NAME: &'static str = "FiltersRow"; + type Type = super::FiltersRow; + type ParentType = adw::PreferencesRow; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for FiltersRow { + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for FiltersRow {} + + impl ListBoxRowImpl for FiltersRow {} + + impl PreferencesRowImpl for FiltersRow {} +} + +glib::wrapper! { + pub struct FiltersRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::ActionRow, adw::PreferencesRow, @implements gtk::Actionable, gtk::Accessible; +} + +impl Default for FiltersRow { + fn default() -> Self { + Self::new() + } +} + +#[template_callbacks] +impl FiltersRow { + pub fn new() -> Self { + glib::Object::new() + } + + #[template_callback] + fn on_add_button_clicked(&self) { + let filter_type = self.filter_type(); + + let page = self.imp().search_page.get_or_init(|| { + let page = FilterDialogSearchPage::new(); + page.set_filter_row(self); + page.push_filter(filter_type.unwrap_or_default()); + page.connect_filters_changed(glib::clone!( + #[weak(rename_to = obj)] + self, + move |_| { + obj.on_filter_changed(); + } + )); + page + }); + + if let Some(dialog) = self + .ancestor(FilterPanelDialog::static_type()) + .and_downcast_ref::() + { + dialog.push_page(page) + } + } + + pub fn on_filter_changed(&self) { + let flowbox = self.imp().flowbox.get(); + + flowbox.remove_all(); + + let Some(search_page) = self.imp().search_page.get() else { + return; + }; + + let Ok(filter_list) = search_page.imp().filter_list.lock() else { + return; + }; + + for filter in filter_list.iter() { + let label = super::FilterLabel::new(); + label.set_label(Some(filter.name.clone().replace("&", "&"))); + label.set_name(filter.name.clone()); + label.set_id(filter.id.clone()); + label.set_icon_name(self.icon_name()); + flowbox.append(&label); + } + } + + pub fn remove_filter(&self, filter: FilterItem) { + let Some(search_page) = self.imp().search_page.get() else { + return; + }; + + search_page.remove_filter(filter); + } +} diff --git a/src/ui/widgets/filter_panel/mod.rs b/src/ui/widgets/filter_panel/mod.rs index 6899633b..3260d291 100644 --- a/src/ui/widgets/filter_panel/mod.rs +++ b/src/ui/widgets/filter_panel/mod.rs @@ -1,7 +1,11 @@ mod dialog; mod filter_label; mod filter_row; +mod filters_row; +mod search_page; pub use dialog::FilterPanelDialog; pub use filter_label::FilterLabel; pub use filter_row::FilterRow; +pub use filters_row::FiltersRow; +pub use search_page::FilterDialogSearchPage; diff --git a/src/ui/widgets/filter_panel/search_page.rs b/src/ui/widgets/filter_panel/search_page.rs new file mode 100644 index 00000000..50ee061b --- /dev/null +++ b/src/ui/widgets/filter_panel/search_page.rs @@ -0,0 +1,233 @@ +use adw::{ + prelude::*, + subclass::prelude::*, +}; +use gtk::{ + glib, + CompositeTemplate, +}; + +use gtk::template_callbacks; + +use crate::{ + client::{ + emby_client::EMBY_CLIENT, + error::UserFacingError, + structs::FilterItem, + }, + toast, + utils::{ + spawn, + spawn_tokio, + }, +}; + +mod imp { + use std::{ + cell::RefCell, + sync::{ + LazyLock, + Mutex, + }, + }; + + use glib::{ + subclass::{ + InitializingObject, + Signal, + }, + WeakRef, + }; + + use super::*; + + #[derive(Default, CompositeTemplate)] + #[template(resource = "/moe/tsuna/tsukimi/ui/filter_search_page.ui")] + pub struct FilterDialogSearchPage { + pub filter_list: Mutex>, + + #[template_child] + pub listbox: TemplateChild, + + pub filter_row_ref: WeakRef, + pub active_filter_rows: RefCell>>, + } + + #[glib::object_subclass] + impl ObjectSubclass for FilterDialogSearchPage { + const NAME: &'static str = "FilterDialogSearchPage"; + type Type = super::FilterDialogSearchPage; + type ParentType = adw::NavigationPage; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for FilterDialogSearchPage { + fn signals() -> &'static [Signal] { + static SIGNALS: LazyLock> = + LazyLock::new(|| vec![Signal::builder("filters-changed").build()]); + SIGNALS.as_ref() + } + + fn constructed(&self) { + self.parent_constructed(); + self.obj().connect_filters_changed(|obj| { + obj.listbox_retain_filters(); + }); + } + } + + impl WidgetImpl for FilterDialogSearchPage {} + + impl NavigationPageImpl for FilterDialogSearchPage {} +} + +glib::wrapper! { + pub struct FilterDialogSearchPage(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage, @implements gtk::Accessible; +} + +use super::{ + FilterPanelDialog, + FilterRow, + FiltersRow, +}; + +impl Default for FilterDialogSearchPage { + fn default() -> Self { + Self::new() + } +} + +#[template_callbacks] +impl FilterDialogSearchPage { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn push_filter(&self, filter_type: String) { + spawn(glib::clone!( + #[weak(rename_to = obj)] + self, + #[strong] + filter_type, + async move { + let binding = obj.ancestor(FilterPanelDialog::static_type()); + let Some(dialog) = binding.and_downcast_ref::() else { + return; + }; + + dialog.loading_page(); + + let filters = + match spawn_tokio(async move { EMBY_CLIENT.filters(&filter_type).await }).await + { + Ok(filters) => filters, + Err(e) => { + toast!(obj, e.to_user_facing()); + return; + } + }; + + dialog.view_page(); + + let listbox = &obj.imp().listbox; + filters.items.iter().for_each(|filter| { + let filter_row = FilterRow::new(&filter.name, filter.id.clone()); + filter_row.set_title(&filter.name.clone().replace("&", "&")); + + listbox.append(&filter_row); + }); + } + )); + } + + pub fn set_filter_row(&self, filter_row: &FiltersRow) { + self.imp().filter_row_ref.set(Some(filter_row)); + } + + pub fn clear_filters(&self) { + let _ = self + .imp() + .filter_list + .lock() + .map(|mut filters| filters.clear()); + self.emit_by_name::<()>("filters-changed", &[]); + } + + pub fn add_filter(&self, filter: FilterItem) { + let _ = self + .imp() + .filter_list + .lock() + .map(|mut filters| filters.push(filter)); + self.emit_by_name::<()>("filters-changed", &[]); + } + + pub fn remove_filter(&self, filter: FilterItem) { + let _ = self + .imp() + .filter_list + .lock() + .map(|mut filters| filters.retain(|f| *f != filter)); + self.emit_by_name::<()>("filters-changed", &[]); + } + + pub fn remove_active_rows(&self, row: &FilterRow) { + self.imp().active_filter_rows.borrow_mut().retain(|c| { + if let Some(c) = c.upgrade() { + c != *row + } else { + false + } + }); + } + + pub fn clear_active_rows(&self) { + self.imp().active_filter_rows.borrow_mut().clear(); + } + + pub fn add_active_rows(&self, row: &FilterRow) { + self.imp() + .active_filter_rows + .borrow_mut() + .push(row.downgrade()); + } + + pub fn listbox_retain_filters(&self) { + for row in self.imp().active_filter_rows.borrow().iter() { + let Some(filter_row) = row.upgrade() else { + continue; + }; + + let filter = FilterItem { + id: filter_row.id(), + name: filter_row.name(), + }; + + let Ok(filters) = self.imp().filter_list.lock() else { + return; + }; + + let is_active = filters.iter().any(|f| *f == filter); + filter_row.set_active(is_active); + } + } + + pub fn connect_filters_changed(&self, f: F) -> glib::SignalHandlerId { + self.connect_closure( + "filters-changed", + true, + glib::closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } +} diff --git a/src/ui/widgets/identify/dialog.rs b/src/ui/widgets/identify/dialog.rs index 5725f04d..4c97bb4e 100644 --- a/src/ui/widgets/identify/dialog.rs +++ b/src/ui/widgets/identify/dialog.rs @@ -61,6 +61,8 @@ mod imp { #[template_child] pub navigation_view: TemplateChild, + #[template_child] + pub toast_overlay: TemplateChild, } #[glib::object_subclass] @@ -130,7 +132,7 @@ impl IdentifyDialog { Ok(item) => { self.imp() .path_row - .set_subtitle(&item.path.unwrap_or_default()); + .set_subtitle(&item.path.unwrap_or_default().replace("&", "&")); } Err(_) => { self.imp() @@ -165,6 +167,10 @@ impl IdentifyDialog { } } + pub fn add_toast(&self, toast: adw::Toast) { + self.imp().toast_overlay.add_toast(toast); + } + #[template_callback] async fn on_search_button_clicked(&self) { let imp = self.imp(); diff --git a/src/ui/widgets/search.rs b/src/ui/widgets/search.rs index a3e68f56..a0a22eeb 100644 --- a/src/ui/widgets/search.rs +++ b/src/ui/widgets/search.rs @@ -291,10 +291,7 @@ impl SearchPage { #[template_callback] fn filter_panel_cb(&self, _btn: >k::Button) { - let panel = self - .imp() - .filter_panel - .get_or_init(FilterPanelDialog::new); + let panel = self.imp().filter_panel.get_or_init(FilterPanelDialog::new); panel.present(Some(self)); } } diff --git a/src/ui/widgets/single_grid.rs b/src/ui/widgets/single_grid.rs index 3108d919..d1f66b14 100644 --- a/src/ui/widgets/single_grid.rs +++ b/src/ui/widgets/single_grid.rs @@ -355,10 +355,7 @@ impl SingleGrid { #[template_callback] fn filter_panel_cb(&self, _btn: >k::Button) { - let panel = self - .imp() - .filter_panel - .get_or_init(FilterPanelDialog::new); + let panel = self.imp().filter_panel.get_or_init(FilterPanelDialog::new); panel.present(Some(self)); } diff --git a/src/ui/widgets/tu_item/action.rs b/src/ui/widgets/tu_item/action.rs index 68ac0348..2509c5e8 100644 --- a/src/ui/widgets/tu_item/action.rs +++ b/src/ui/widgets/tu_item/action.rs @@ -45,9 +45,6 @@ pub enum Action { } pub trait TuItemAction { - const DELETE_TITLE: &str = "Delete Item"; - const DELETE_BODY: &str = "Deleting this item will delete it from both the file system and your media library.\nThe following files and folders will be deleted:"; - const DELETE_CONFIRM: &str = "Are you sure you wish to continue?"; async fn perform_action_inner(id: &str, action: &Action) -> Result<()>; async fn perform_action(&self, action: Action); @@ -461,13 +458,13 @@ where }; let alert_dialog = adw::AlertDialog::builder() - .heading(Self::DELETE_TITLE) - .title(Self::DELETE_TITLE) + .heading(gettext("Delete Item")) + .title("Delete Item") .body(format!( "{}\n{}\n{}", - gettext(Self::DELETE_BODY), + gettext("Deleting this item will delete it from both the file system and your media library.\nThe following files and folders will be deleted:"), delete_info.paths.join("\n"), - gettext(Self::DELETE_CONFIRM) + gettext("Are you sure you wish to continue?") )) .build();