a lightweight, interval-based utility to combat digital strain through "Ma" (intentional pauses) for the eyes and body.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: wire profile level editor callbacks

+144 -30
+144 -30
src/settings/mod.rs
··· 1 - use slint::{ComponentHandle, ModelRc, SharedString, VecModel}; 1 + use std::rc::Rc; 2 + 3 + use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; 2 4 3 5 use crate::autostart; 4 - use crate::config::{AppConfig, BreakModeConfig}; 5 - use crate::generated::SettingsWindow; 6 + use crate::config::{AppConfig, BreakLevelConfig, BreakModeConfig, LongBreakConfig}; 7 + use crate::generated::{LevelEntry, SettingsWindow}; 6 8 7 9 pub struct SettingsManager { 8 10 window: SettingsWindow, 11 + levels_model: Rc<VecModel<LevelEntry>>, 9 12 } 10 13 11 14 impl SettingsManager { 12 15 pub fn new(cfg: &AppConfig) -> anyhow::Result<Self> { 13 16 let window = SettingsWindow::new()?; 14 - populate(&window, cfg); 15 - // Hide immediately — shown only when user opens settings from tray. 17 + let levels_model: Rc<VecModel<LevelEntry>> = Rc::new(VecModel::default()); 18 + window.set_levels(ModelRc::new(Rc::clone(&levels_model))); 19 + 20 + populate(&window, &levels_model, cfg); 21 + 22 + // Level editor callbacks 23 + { 24 + let model = Rc::clone(&levels_model); 25 + window.on_level_changed(move |idx, entry| { 26 + model.set_row_data(idx as usize, entry); 27 + }); 28 + } 29 + { 30 + let model = Rc::clone(&levels_model); 31 + window.on_level_removed(move |idx| { 32 + let i = idx as usize; 33 + if i < model.row_count() { 34 + model.remove(i); 35 + } 36 + }); 37 + } 38 + { 39 + let model = Rc::clone(&levels_model); 40 + window.on_level_added(move || { 41 + model.push(LevelEntry { 42 + work_mins: 20, 43 + break_mins: 1, 44 + break_extra_secs: 0, 45 + label: "Break".into(), 46 + }); 47 + }); 48 + } 49 + 50 + // Profile switch: re-populate level editor from saved config 51 + { 52 + let cfg_clone = cfg.clone(); 53 + let model = Rc::clone(&levels_model); 54 + let win2 = window.clone_strong(); 55 + window.on_profile_changed(move |name| { 56 + populate_for_profile(&win2, &model, &cfg_clone, name.as_str()); 57 + }); 58 + } 59 + 16 60 window.hide().unwrap_or_default(); 17 - Ok(Self { window }) 61 + Ok(Self { window, levels_model }) 18 62 } 19 63 20 64 pub fn window(&self) -> &SettingsWindow { 21 65 &self.window 22 66 } 23 67 24 - #[allow(dead_code)] 25 68 pub fn read_into(&self, cfg: &mut AppConfig) { 69 + // Resolve active profile key from display name 70 + let active_name = self.window.get_active_profile().to_string(); 71 + let active_key = cfg 72 + .profiles 73 + .iter() 74 + .find(|(_, p)| p.name == active_name) 75 + .map(|(k, _)| k.clone()); 76 + 77 + if let Some(key) = active_key { 78 + cfg.app.active_profile = key.clone(); 79 + 80 + if let Some(prof) = cfg.profiles.get_mut(&key) { 81 + // Levels from model 82 + prof.levels = (0..self.levels_model.row_count()) 83 + .filter_map(|i| self.levels_model.row_data(i)) 84 + .map(|e| BreakLevelConfig { 85 + work_secs: e.work_mins as u64 * 60, 86 + break_secs: e.break_mins as u64 * 60 + e.break_extra_secs as u64, 87 + label: e.label.to_string(), 88 + }) 89 + .collect(); 90 + 91 + // Long break 92 + prof.long_break = if self.window.get_long_break_enabled() { 93 + Some(LongBreakConfig { 94 + after_cycles: self.window.get_long_break_after_cycles() as u32, 95 + max_cycle_gap_secs: self.window.get_long_break_gap_mins() as u64 * 60, 96 + break_secs: self.window.get_long_break_duration_mins() as u64 * 60, 97 + label: self.window.get_long_break_label().to_string(), 98 + }) 99 + } else { 100 + None 101 + }; 102 + 103 + prof.idle_detection_enabled = self.window.get_idle_detection_enabled(); 104 + prof.idle_threshold_secs = 105 + self.window.get_idle_threshold_mins() as u64 * 60; 106 + prof.mode = if self.window.get_enforced_mode() { 107 + BreakModeConfig::Enforced 108 + } else { 109 + BreakModeConfig::Reminder 110 + }; 111 + } 112 + } 113 + 26 114 cfg.appearance.sound_enabled = self.window.get_sound_enabled(); 27 115 cfg.appearance.sound_volume = self.window.get_sound_volume(); 28 116 cfg.app.autostart = self.window.get_autostart(); 29 - 30 - if let Some(prof) = cfg.profiles.get_mut(&cfg.app.active_profile) { 31 - prof.idle_detection_enabled = self.window.get_idle_detection_enabled(); 32 - prof.idle_threshold_secs = (self.window.get_idle_threshold_mins() as u64) * 60; 33 - prof.mode = if self.window.get_enforced_mode() { 34 - BreakModeConfig::Enforced 35 - } else { 36 - BreakModeConfig::Reminder 37 - }; 38 - } 39 117 } 40 118 } 41 119 42 - fn populate(window: &SettingsWindow, cfg: &AppConfig) { 43 - let active = cfg 120 + fn populate(window: &SettingsWindow, levels_model: &VecModel<LevelEntry>, cfg: &AppConfig) { 121 + let active_name = cfg 44 122 .profiles 45 123 .get(&cfg.app.active_profile) 46 124 .map(|p| p.name.as_str()) 47 125 .unwrap_or("") 48 126 .to_string(); 49 - window.set_active_profile(SharedString::from(active.as_str())); 127 + window.set_active_profile(SharedString::from(active_name.as_str())); 50 128 51 - let names: Vec<SharedString> = cfg 129 + let mut names: Vec<SharedString> = cfg 52 130 .profiles 53 131 .values() 54 132 .map(|p| SharedString::from(p.name.as_str())) 55 133 .collect(); 134 + names.sort(); 56 135 window.set_profile_names(ModelRc::new(VecModel::from(names))); 57 136 58 - let enforced = cfg 59 - .profiles 60 - .get(&cfg.app.active_profile) 61 - .map_or(false, |p| p.mode == BreakModeConfig::Enforced); 62 - window.set_enforced_mode(enforced); 63 137 window.set_sound_enabled(cfg.appearance.sound_enabled); 64 138 window.set_sound_volume(cfg.appearance.sound_volume); 65 - // Reflect actual OS state rather than last-saved config value. 66 139 window.set_autostart(autostart::is_enabled()); 67 140 68 - let idle_prof = cfg.profiles.get(&cfg.app.active_profile); 69 - window.set_idle_detection_enabled(idle_prof.map_or(true, |p| p.idle_detection_enabled)); 141 + populate_for_profile(window, levels_model, cfg, &active_name); 142 + } 143 + 144 + fn populate_for_profile( 145 + window: &SettingsWindow, 146 + levels_model: &VecModel<LevelEntry>, 147 + cfg: &AppConfig, 148 + profile_name: &str, 149 + ) { 150 + let prof = cfg.profiles.values().find(|p| p.name == profile_name); 151 + 152 + window.set_enforced_mode(prof.map_or(false, |p| p.mode == BreakModeConfig::Enforced)); 153 + window.set_idle_detection_enabled(prof.map_or(true, |p| p.idle_detection_enabled)); 70 154 window.set_idle_threshold_mins( 71 - idle_prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32), 155 + prof.map_or(5, |p| (p.idle_threshold_secs / 60).max(1) as i32), 72 156 ); 157 + 158 + // Replace level rows 159 + let entries: Vec<LevelEntry> = prof 160 + .map(|p| { 161 + p.levels 162 + .iter() 163 + .map(|l| LevelEntry { 164 + work_mins: (l.work_secs / 60) as i32, 165 + break_mins: (l.break_secs / 60) as i32, 166 + break_extra_secs: (l.break_secs % 60) as i32, 167 + label: l.label.as_str().into(), 168 + }) 169 + .collect() 170 + }) 171 + .unwrap_or_default(); 172 + 173 + for i in (0..levels_model.row_count()).rev() { 174 + levels_model.remove(i); 175 + } 176 + for entry in entries { 177 + levels_model.push(entry); 178 + } 179 + 180 + // Long break 181 + let lb = prof.and_then(|p| p.long_break.as_ref()); 182 + window.set_long_break_enabled(lb.is_some()); 183 + window.set_long_break_after_cycles(lb.map_or(4, |l| l.after_cycles as i32)); 184 + window.set_long_break_duration_mins(lb.map_or(15, |l| (l.break_secs / 60) as i32)); 185 + window.set_long_break_gap_mins(lb.map_or(30, |l| (l.max_cycle_gap_secs / 60) as i32)); 186 + window.set_long_break_label(lb.map_or("Long break", |l| l.label.as_str()).into()); 73 187 }