Skip to main content

cadmus_core/view/settings_editor/kinds/
dictionary.rs

1//! Setting kinds for the Dictionaries category.
2
3use super::{SettingData, SettingIdentity, SettingKind, WidgetKind};
4use crate::fl;
5use crate::settings::Settings;
6use crate::view::{EntryId, EntryKind, Event};
7
8/// Represents a single monolingual dictionary row in the Dictionaries settings category.
9///
10/// Each row shows a lang code as the label and "Installed" or "Download" as the
11/// value. Installed dictionaries show a sub-menu with "Re-download" and "Delete"
12/// options; uninstalled ones show an `ActionLabel` that fires the download event on tap.
13/// When an update is available, the value shows "Update Available" and the submenu
14/// includes an "Update" option above "Re-download". When a download is in progress,
15/// the value shows "Downloading" and no action widget is offered.
16pub struct DictionaryInfo {
17    /// ISO 639-1 language code, e.g. `"en"` or `"fr"`.
18    pub lang: String,
19    /// Whether this dictionary is currently installed on the device.
20    pub is_installed: bool,
21    /// Whether a newer version is available on the server.
22    pub update_available: bool,
23    /// Whether a download/install is currently in progress for this language.
24    pub is_installing: bool,
25}
26
27impl SettingKind for DictionaryInfo {
28    fn identity(&self) -> SettingIdentity {
29        SettingIdentity::DictionaryInfo(self.lang.clone())
30    }
31
32    fn label(&self, _settings: &Settings) -> String {
33        self.lang.clone()
34    }
35
36    fn handle(
37        &self,
38        evt: &Event,
39        _settings: &mut Settings,
40        _bus: &mut crate::view::Bus,
41    ) -> (Option<String>, bool) {
42        match evt {
43            Event::Select(entry) => match entry {
44                EntryId::DownloadDictionary(lang) | EntryId::RedownloadDictionary(lang)
45                    if lang == &self.lang =>
46                {
47                    (Some(fl!("settings-dictionaries-downloading")), false)
48                }
49                _ => (None, false),
50            },
51            _ => (None, false),
52        }
53    }
54
55    fn fetch(&self, _settings: &Settings) -> SettingData {
56        if self.is_installing {
57            return SettingData {
58                value: fl!("settings-dictionaries-downloading"),
59                widget: WidgetKind::None,
60            };
61        }
62
63        if self.is_installed {
64            let mut entries = Vec::new();
65
66            if self.update_available {
67                entries.push(EntryKind::Command(
68                    fl!("settings-dictionaries-update"),
69                    EntryId::RedownloadDictionary(self.lang.clone()),
70                ));
71            } else {
72                entries.push(EntryKind::Command(
73                    fl!("settings-dictionaries-re-download"),
74                    EntryId::RedownloadDictionary(self.lang.clone()),
75                ));
76            }
77
78            entries.push(EntryKind::Command(
79                fl!("settings-dictionaries-delete"),
80                EntryId::DeleteDictionary(self.lang.clone()),
81            ));
82
83            let value = if self.update_available {
84                fl!("settings-dictionaries-update-available")
85            } else {
86                fl!("settings-dictionaries-installed")
87            };
88
89            SettingData {
90                value,
91                widget: WidgetKind::SubMenu(entries),
92            }
93        } else {
94            SettingData {
95                value: fl!("settings-dictionaries-download"),
96                widget: WidgetKind::ActionLabel(Event::Select(EntryId::DownloadDictionary(
97                    self.lang.clone(),
98                ))),
99            }
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::settings::Settings;
108    use crate::view::Bus;
109    use std::collections::VecDeque;
110
111    fn make_settings() -> Settings {
112        Settings::default()
113    }
114
115    mod fetch {
116        use super::*;
117
118        #[test]
119        fn uninstalled_yields_action_label_with_download_event() {
120            let info = DictionaryInfo {
121                lang: "en".to_string(),
122                is_installed: false,
123                update_available: false,
124                is_installing: false,
125            };
126            let data = info.fetch(&make_settings());
127
128            assert!(matches!(
129                data.widget,
130                WidgetKind::ActionLabel(Event::Select(EntryId::DownloadDictionary(ref l)))
131                    if l == "en"
132            ));
133        }
134
135        #[test]
136        fn installed_yields_submenu_with_redownload_and_delete() {
137            let info = DictionaryInfo {
138                lang: "fr".to_string(),
139                is_installed: true,
140                update_available: false,
141                is_installing: false,
142            };
143            let data = info.fetch(&make_settings());
144
145            let WidgetKind::SubMenu(entries) = data.widget else {
146                panic!("expected SubMenu");
147            };
148            assert_eq!(entries.len(), 2);
149            assert!(matches!(
150                &entries[0],
151                EntryKind::Command(_, EntryId::RedownloadDictionary(l)) if l == "fr"
152            ));
153            assert!(matches!(
154                &entries[1],
155                EntryKind::Command(_, EntryId::DeleteDictionary(l)) if l == "fr"
156            ));
157        }
158
159        #[test]
160        fn update_available_yields_submenu_with_update_first() {
161            let info = DictionaryInfo {
162                lang: "de".to_string(),
163                is_installed: true,
164                update_available: true,
165                is_installing: false,
166            };
167            let data = info.fetch(&make_settings());
168
169            let WidgetKind::SubMenu(entries) = data.widget else {
170                panic!("expected SubMenu");
171            };
172            assert_eq!(entries.len(), 2);
173            assert!(matches!(
174                &entries[0],
175                EntryKind::Command(label, EntryId::RedownloadDictionary(l))
176                    if l == "de" && label == "Update"
177            ));
178            assert!(matches!(
179                &entries[1],
180                EntryKind::Command(_, EntryId::DeleteDictionary(l)) if l == "de"
181            ));
182            assert_eq!(data.value, "Update Available");
183        }
184
185        #[test]
186        fn is_installing_yields_none_widget() {
187            let info = DictionaryInfo {
188                lang: "es".to_string(),
189                is_installed: false,
190                update_available: false,
191                is_installing: true,
192            };
193            let data = info.fetch(&make_settings());
194
195            assert!(matches!(data.widget, WidgetKind::None));
196        }
197
198        #[test]
199        fn is_installing_takes_priority_over_installed() {
200            let info = DictionaryInfo {
201                lang: "es".to_string(),
202                is_installed: true,
203                update_available: true,
204                is_installing: true,
205            };
206            let data = info.fetch(&make_settings());
207
208            assert!(matches!(data.widget, WidgetKind::None));
209        }
210    }
211
212    mod handle {
213        use super::*;
214
215        #[test]
216        fn download_event_returns_downloading_string() {
217            let info = DictionaryInfo {
218                lang: "en".to_string(),
219                is_installed: false,
220                update_available: false,
221                is_installing: false,
222            };
223            let mut settings = make_settings();
224            let mut bus: Bus = VecDeque::new();
225            let event = Event::Select(EntryId::DownloadDictionary("en".to_string()));
226
227            let (display, consumed) = info.handle(&event, &mut settings, &mut bus);
228
229            assert!(display.is_some());
230            assert!(!consumed);
231        }
232
233        #[test]
234        fn redownload_event_returns_downloading_string() {
235            let info = DictionaryInfo {
236                lang: "en".to_string(),
237                is_installed: true,
238                update_available: false,
239                is_installing: false,
240            };
241            let mut settings = make_settings();
242            let mut bus: Bus = VecDeque::new();
243            let event = Event::Select(EntryId::RedownloadDictionary("en".to_string()));
244
245            let (display, consumed) = info.handle(&event, &mut settings, &mut bus);
246
247            assert!(display.is_some());
248            assert!(!consumed);
249        }
250
251        #[test]
252        fn event_for_different_lang_returns_none() {
253            let info = DictionaryInfo {
254                lang: "en".to_string(),
255                is_installed: false,
256                update_available: false,
257                is_installing: false,
258            };
259            let mut settings = make_settings();
260            let mut bus: Bus = VecDeque::new();
261            let event = Event::Select(EntryId::DownloadDictionary("fr".to_string()));
262
263            let (display, _) = info.handle(&event, &mut settings, &mut bus);
264
265            assert!(display.is_none());
266        }
267
268        #[test]
269        fn unrelated_event_returns_none() {
270            let info = DictionaryInfo {
271                lang: "en".to_string(),
272                is_installed: false,
273                update_available: false,
274                is_installing: false,
275            };
276            let mut settings = make_settings();
277            let mut bus: Bus = VecDeque::new();
278
279            let (display, _) = info.handle(&Event::Select(EntryId::About), &mut settings, &mut bus);
280
281            assert!(display.is_none());
282        }
283    }
284}