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 requests a download 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) if lang == &self.lang => {
45                    (Some(fl!("settings-dictionaries-downloading")), false)
46                }
47                _ => (None, false),
48            },
49            _ => (None, false),
50        }
51    }
52
53    fn fetch(&self, _settings: &Settings) -> SettingData {
54        if self.is_installing {
55            return SettingData {
56                value: fl!("settings-dictionaries-downloading"),
57                widget: WidgetKind::None,
58            };
59        }
60
61        if self.is_installed {
62            let mut entries = Vec::new();
63
64            if self.update_available {
65                entries.push(EntryKind::Command(
66                    fl!("settings-dictionaries-update"),
67                    EntryId::RequestDictionaryDownload(self.lang.clone()),
68                ));
69            } else {
70                entries.push(EntryKind::Command(
71                    fl!("settings-dictionaries-re-download"),
72                    EntryId::RequestDictionaryDownload(self.lang.clone()),
73                ));
74            }
75
76            entries.push(EntryKind::Command(
77                fl!("settings-dictionaries-delete"),
78                EntryId::DeleteDictionary(self.lang.clone()),
79            ));
80
81            let value = if self.update_available {
82                fl!("settings-dictionaries-update-available")
83            } else {
84                fl!("settings-dictionaries-installed")
85            };
86
87            SettingData {
88                value,
89                widget: WidgetKind::SubMenu(entries),
90            }
91        } else {
92            SettingData {
93                value: fl!("settings-dictionaries-download"),
94                widget: WidgetKind::ActionLabel(Event::Select(EntryId::RequestDictionaryDownload(
95                    self.lang.clone(),
96                ))),
97            }
98        }
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::settings::Settings;
106    use crate::view::Bus;
107    use std::collections::VecDeque;
108
109    fn make_settings() -> Settings {
110        Settings::default()
111    }
112
113    mod fetch {
114        use super::*;
115
116        #[test]
117        fn uninstalled_yields_action_label_with_request_event() {
118            let info = DictionaryInfo {
119                lang: "en".to_string(),
120                is_installed: false,
121                update_available: false,
122                is_installing: false,
123            };
124            let data = info.fetch(&make_settings());
125
126            assert!(matches!(
127                data.widget,
128                WidgetKind::ActionLabel(Event::Select(EntryId::RequestDictionaryDownload(ref l)))
129                    if l == "en"
130            ));
131        }
132
133        #[test]
134        fn installed_yields_submenu_with_redownload_and_delete() {
135            let info = DictionaryInfo {
136                lang: "fr".to_string(),
137                is_installed: true,
138                update_available: false,
139                is_installing: false,
140            };
141            let data = info.fetch(&make_settings());
142
143            let WidgetKind::SubMenu(entries) = data.widget else {
144                panic!("expected SubMenu");
145            };
146            assert_eq!(entries.len(), 2);
147            assert!(matches!(
148                &entries[0],
149                EntryKind::Command(_, EntryId::RequestDictionaryDownload(l)) if l == "fr"
150            ));
151            assert!(matches!(
152                &entries[1],
153                EntryKind::Command(_, EntryId::DeleteDictionary(l)) if l == "fr"
154            ));
155        }
156
157        #[test]
158        fn update_available_yields_submenu_with_update_first() {
159            let info = DictionaryInfo {
160                lang: "de".to_string(),
161                is_installed: true,
162                update_available: true,
163                is_installing: false,
164            };
165            let data = info.fetch(&make_settings());
166
167            let WidgetKind::SubMenu(entries) = data.widget else {
168                panic!("expected SubMenu");
169            };
170            assert_eq!(entries.len(), 2);
171            assert!(matches!(
172                &entries[0],
173                EntryKind::Command(label, EntryId::RequestDictionaryDownload(l))
174                    if l == "de" && label == "Update"
175            ));
176            assert!(matches!(
177                &entries[1],
178                EntryKind::Command(_, EntryId::DeleteDictionary(l)) if l == "de"
179            ));
180            assert_eq!(data.value, "Update Available");
181        }
182
183        #[test]
184        fn is_installing_yields_none_widget() {
185            let info = DictionaryInfo {
186                lang: "es".to_string(),
187                is_installed: false,
188                update_available: false,
189                is_installing: true,
190            };
191            let data = info.fetch(&make_settings());
192
193            assert!(matches!(data.widget, WidgetKind::None));
194        }
195
196        #[test]
197        fn is_installing_takes_priority_over_installed() {
198            let info = DictionaryInfo {
199                lang: "es".to_string(),
200                is_installed: true,
201                update_available: true,
202                is_installing: true,
203            };
204            let data = info.fetch(&make_settings());
205
206            assert!(matches!(data.widget, WidgetKind::None));
207        }
208    }
209
210    mod handle {
211        use super::*;
212
213        #[test]
214        fn download_event_returns_downloading_string() {
215            let info = DictionaryInfo {
216                lang: "en".to_string(),
217                is_installed: false,
218                update_available: false,
219                is_installing: false,
220            };
221            let mut settings = make_settings();
222            let mut bus: Bus = VecDeque::new();
223            let event = Event::Select(EntryId::DownloadDictionary("en".to_string()));
224
225            let (display, consumed) = info.handle(&event, &mut settings, &mut bus);
226
227            assert!(display.is_some());
228            assert!(!consumed);
229        }
230
231        #[test]
232        fn request_event_returns_none() {
233            let info = DictionaryInfo {
234                lang: "en".to_string(),
235                is_installed: true,
236                update_available: false,
237                is_installing: false,
238            };
239            let mut settings = make_settings();
240            let mut bus: Bus = VecDeque::new();
241            let event = Event::Select(EntryId::RequestDictionaryDownload("en".to_string()));
242
243            let (display, consumed) = info.handle(&event, &mut settings, &mut bus);
244
245            assert!(display.is_none());
246            assert!(!consumed);
247        }
248
249        #[test]
250        fn event_for_different_lang_returns_none() {
251            let info = DictionaryInfo {
252                lang: "en".to_string(),
253                is_installed: false,
254                update_available: false,
255                is_installing: false,
256            };
257            let mut settings = make_settings();
258            let mut bus: Bus = VecDeque::new();
259            let event = Event::Select(EntryId::DownloadDictionary("fr".to_string()));
260
261            let (display, _) = info.handle(&event, &mut settings, &mut bus);
262
263            assert!(display.is_none());
264        }
265
266        #[test]
267        fn unrelated_event_returns_none() {
268            let info = DictionaryInfo {
269                lang: "en".to_string(),
270                is_installed: false,
271                update_available: false,
272                is_installing: false,
273            };
274            let mut settings = make_settings();
275            let mut bus: Bus = VecDeque::new();
276
277            let (display, _) = info.handle(&Event::Select(EntryId::About), &mut settings, &mut bus);
278
279            assert!(display.is_none());
280        }
281    }
282}