authenticator/widgets/providers/
dialog.rs1use 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 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: >k::SearchEntry) {
222 let text = entry.text().to_string();
223 self.search(text);
224 }
225
226 #[template_callback]
227 fn on_search_started(&self, _entry: >k::SearchEntry) {
228 self.imp().search_btn.set_active(true);
229 }
230 #[template_callback]
231 fn on_search_stopped(&self, _entry: >k::SearchEntry) {
232 self.imp().search_btn.set_active(false);
233 }
234
235 #[template_callback]
236 fn on_search_btn_toggled(&self, btn: >k::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}