authenticator/widgets/providers/
dialog.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::glib::{self, clone};
4
5use super::{ProviderPage, dialog_row::ProviderActionRow};
6use crate::models::{Provider, ProvidersModel};
7
8enum View {
9    List,
10    Form,
11    Placeholder,
12}
13
14mod imp {
15    use std::{cell::OnceCell, sync::LazyLock};
16
17    use glib::subclass::Signal;
18
19    use super::*;
20    use crate::config;
21
22    #[derive(Default, gtk::CompositeTemplate, glib::Properties)]
23    #[template(resource = "/com/belmoussaoui/Authenticator/providers_dialog.ui")]
24    #[properties(wrapper_type = super::ProvidersDialog)]
25    pub struct ProvidersDialog {
26        #[property(get, set, construct_only)]
27        pub model: OnceCell<ProvidersModel>,
28        #[template_child]
29        pub page: TemplateChild<ProviderPage>,
30        pub filter_model: gtk::FilterListModel,
31        #[template_child]
32        pub providers_list: TemplateChild<gtk::ListBox>,
33        #[template_child]
34        pub deck: TemplateChild<adw::NavigationSplitView>,
35        #[template_child]
36        pub search_entry: TemplateChild<gtk::SearchEntry>,
37        #[template_child]
38        pub search_bar: TemplateChild<gtk::SearchBar>,
39        #[template_child]
40        pub search_btn: TemplateChild<gtk::ToggleButton>,
41        #[template_child]
42        pub search_stack: TemplateChild<gtk::Stack>,
43        #[template_child]
44        pub stack: TemplateChild<gtk::Stack>,
45        #[template_child]
46        pub placeholder_page: TemplateChild<adw::StatusPage>,
47        #[template_child]
48        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
49        pub(super) sort_model: gtk::SortListModel,
50    }
51
52    #[glib::object_subclass]
53    impl ObjectSubclass for ProvidersDialog {
54        const NAME: &'static str = "ProvidersDialog";
55        type Type = super::ProvidersDialog;
56        type ParentType = adw::Dialog;
57
58        fn class_init(klass: &mut Self::Class) {
59            klass.bind_template();
60            klass.bind_template_instance_callbacks();
61
62            klass.install_action("providers.back", None, |dialog, _, _| {
63                dialog.set_view(View::List);
64            });
65
66            klass.install_action("providers.add", None, |dialog, _, _| {
67                dialog.add_provider();
68            });
69
70            klass.install_action("providers.search", None, |dialog, _, _| {
71                let search_btn = &*dialog.imp().search_btn;
72                search_btn.set_active(!search_btn.is_active());
73            });
74        }
75
76        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
77            obj.init_template();
78        }
79    }
80
81    #[glib::derived_properties]
82    impl ObjectImpl for ProvidersDialog {
83        fn signals() -> &'static [Signal] {
84            static SIGNALS: LazyLock<Vec<Signal>> =
85                LazyLock::new(|| vec![Signal::builder("changed").build()]);
86            SIGNALS.as_ref()
87        }
88
89        fn constructed(&self) {
90            self.parent_constructed();
91            let obj = self.obj();
92            self.placeholder_page.set_icon_name(Some(config::APP_ID));
93            self.filter_model.set_model(Some(&obj.model()));
94            self.filter_model.connect_items_changed(clone!(
95                #[weak(rename_to = dialog)]
96                obj,
97                move |model, _, _, _| {
98                    if model.n_items() == 0 {
99                        dialog
100                            .imp()
101                            .search_stack
102                            .set_visible_child_name("no-results");
103                    } else {
104                        dialog.imp().search_stack.set_visible_child_name("results");
105                    }
106                }
107            ));
108
109            let sorter = gtk::StringSorter::builder()
110                .ignore_case(true)
111                .expression(Provider::this_expression("name"))
112                .build();
113            self.sort_model.set_model(Some(&self.filter_model));
114            self.sort_model.set_sorter(Some(&sorter));
115
116            let selection_model = gtk::NoSelection::new(Some(self.sort_model.clone()));
117            self.providers_list
118                .bind_model(Some(&selection_model), move |obj| {
119                    let provider = obj.downcast_ref::<Provider>().unwrap();
120                    let row = ProviderActionRow::new(provider);
121                    row.upcast::<gtk::Widget>()
122                });
123
124            obj.set_view(View::Placeholder);
125        }
126    }
127    impl WidgetImpl for ProvidersDialog {}
128    impl AdwDialogImpl for ProvidersDialog {}
129}
130glib::wrapper! {
131    pub struct ProvidersDialog(ObjectSubclass<imp::ProvidersDialog>)
132        @extends gtk::Widget, adw::Dialog,
133        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
134}
135
136#[gtk::template_callbacks]
137impl ProvidersDialog {
138    pub fn new(model: &ProvidersModel) -> Self {
139        glib::Object::builder().property("model", model).build()
140    }
141
142    pub fn connect_changed<F>(&self, callback: F) -> glib::SignalHandlerId
143    where
144        F: Fn(&Self) + 'static,
145    {
146        self.connect_local(
147            "changed",
148            false,
149            clone!(
150                #[weak(rename_to = dialog)]
151                self,
152                #[upgrade_or]
153                None,
154                move |_| {
155                    callback(&dialog);
156                    None
157                }
158            ),
159        )
160    }
161
162    fn search(&self, text: String) {
163        let providers_filter = gtk::CustomFilter::new(move |object| {
164            let provider = object.downcast_ref::<Provider>().unwrap();
165            provider
166                .name()
167                .to_ascii_lowercase()
168                .contains(&text.to_ascii_lowercase())
169        });
170        self.imp().filter_model.set_filter(Some(&providers_filter));
171    }
172
173    fn add_provider(&self) {
174        self.set_view(View::Form);
175        // By not setting the current provider we implicitly say it's for creating a new
176        // one
177        self.imp().page.set_provider(None);
178    }
179
180    fn edit_provider(&self, provider: Provider) {
181        self.set_view(View::Form);
182        let imp = self.imp();
183        let model = &imp.sort_model;
184
185        let mut index = -1;
186        for pos in 0..model.n_items() {
187            let other_provider = model.item(pos).and_downcast::<Provider>().unwrap();
188            if provider.id() == other_provider.id() {
189                index = pos as i32;
190                break;
191            }
192        }
193
194        imp.page.set_provider(Some(provider));
195        let row = imp.providers_list.row_at_index(index);
196        imp.providers_list.select_row(row.as_ref());
197    }
198
199    fn set_view(&self, view: View) {
200        let imp = self.imp();
201        match view {
202            View::Form => {
203                imp.deck.set_show_content(true);
204                imp.stack.set_visible_child_name("provider");
205                imp.search_bar.set_key_capture_widget(gtk::Widget::NONE);
206                imp.search_entry.emit_stop_search();
207            }
208            View::List => {
209                imp.deck.set_show_content(false);
210                imp.search_bar.set_key_capture_widget(Some(self));
211            }
212            View::Placeholder => {
213                imp.deck.set_show_content(false);
214                imp.stack.set_visible_child_name("placeholder");
215                imp.search_bar.set_key_capture_widget(Some(self));
216            }
217        }
218    }
219
220    #[template_callback]
221    fn on_search_changed(&self, entry: &gtk::SearchEntry) {
222        let text = entry.text().to_string();
223        self.search(text);
224    }
225
226    #[template_callback]
227    fn on_search_started(&self, _entry: &gtk::SearchEntry) {
228        self.imp().search_btn.set_active(true);
229    }
230    #[template_callback]
231    fn on_search_stopped(&self, _entry: &gtk::SearchEntry) {
232        self.imp().search_btn.set_active(false);
233    }
234
235    #[template_callback]
236    fn on_search_btn_toggled(&self, btn: &gtk::ToggleButton) {
237        let imp = self.imp();
238        if btn.is_active() {
239            imp.search_entry.grab_focus();
240        } else {
241            imp.search_entry.set_text("");
242        }
243    }
244    #[template_callback]
245    fn on_row_activated(&self, row: ProviderActionRow, _list: gtk::ListBox) {
246        let provider = row.provider();
247        self.edit_provider(provider);
248    }
249
250    #[template_callback]
251    fn on_provider_created(&self, provider: Provider, _page: ProviderPage) {
252        let model = self
253            .imp()
254            .filter_model
255            .model()
256            .and_downcast::<ProvidersModel>()
257            .unwrap();
258        model.append(&provider);
259        self.emit_by_name::<()>("changed", &[]);
260        self.imp()
261            .toast_overlay
262            .add_toast(adw::Toast::new(&gettext("Provider created successfully")));
263        self.set_view(View::Placeholder);
264    }
265
266    #[template_callback]
267    fn on_provider_updated(&self, _provider: Provider, _page: ProviderPage) {
268        self.set_view(View::List);
269        self.emit_by_name::<()>("changed", &[]);
270        self.imp()
271            .toast_overlay
272            .add_toast(adw::Toast::new(&gettext("Provider updated successfully")));
273    }
274
275    #[template_callback]
276    fn on_provider_deleted(&self, provider: Provider, _page: ProviderPage) {
277        let model = self
278            .imp()
279            .filter_model
280            .model()
281            .and_downcast::<ProvidersModel>()
282            .unwrap();
283        model.delete_provider(&provider);
284        self.set_view(View::Placeholder);
285        self.emit_by_name::<()>("changed", &[]);
286        self.imp()
287            .toast_overlay
288            .add_toast(adw::Toast::new(&gettext("Provider removed successfully")));
289    }
290}