Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 1 | //! The app's own settings: how it starts, what closing the window does, |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 2 | //! how it tells the person something arrived, its title bar, zoom and |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 3 | //! updates. They live in their own window (`launcher/settings.html`), |
| 4 | //! opened from the menu, the tray, or the site's account menu, and are | |
| 5 | //! kept with the server in `desktop.json` (config.rs). Whether g1t opens | |
| 6 | //! when someone signs in to their computer is kept by the system itself, | |
| 7 | //! through the autostart plugin, and read back from it. | |
| 8 | ||
| 9 | use serde::{Deserialize, Serialize}; | |
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 10 | use tauri::{AppHandle, Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 11 | use tauri_plugin_autostart::ManagerExt; |
| 12 | ||
| 13 | use crate::{WINDOW, config}; | |
| 14 | ||
| 15 | /// The settings window's label. | |
| 16 | pub const SETTINGS: &str = "settings"; | |
| 17 | ||
| 18 | /// The argument the system starts g1t with at sign-in, so it can tell a | |
| 19 | /// start of its own from someone opening it. | |
| 20 | pub const AT_LOGIN: &str = "--at-login"; | |
| 21 | ||
| 22 | /// The zoom levels the View menu steps through. | |
| 23 | pub const ZOOMS: &[f64] = &[0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0]; | |
| 24 | ||
| 25 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] | |
| 26 | #[serde(default, rename_all = "camelCase")] | |
| 27 | pub struct Settings { | |
| 28 | /// Opened at sign-in, g1t waits in the tray instead of opening its window. | |
| 29 | pub start_hidden: bool, | |
| 30 | /// Closing the window leaves g1t running in the tray, for notifications; | |
| 31 | /// otherwise closing it quits. | |
| 32 | pub close_to_tray: bool, | |
| 33 | /// Notifications from the system, for what the site says needs the person. | |
| 34 | pub notifications: bool, | |
| 35 | /// The taskbar icon flashes, or the dock icon bounces, with a notification. | |
| 36 | pub flash: bool, | |
| 37 | /// What needs the person, counted on the icon. | |
| 38 | pub badge: bool, | |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 39 | /// The system's title bar, and on Windows and Linux its menu bar, in |
| 40 | /// place of g1t's own (frame.rs). Takes effect when g1t restarts. | |
| 41 | pub system_title_bar: bool, | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 42 | /// The site's zoom, 1 for as it is. |
| 43 | pub zoom: f64, | |
| 44 | /// Updates download by themselves; a person can still check from the menu. | |
| 45 | pub auto_update: bool, | |
| 46 | } | |
| 47 | ||
| 48 | impl Default for Settings { | |
| 49 | fn default() -> Self { | |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 50 | Self { start_hidden: true, close_to_tray: true, notifications: true, flash: true, badge: true, system_title_bar: false, zoom: 1.0, auto_update: true } |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 51 | } |
| 52 | } | |
| 53 | ||
| 54 | impl Settings { | |
| 55 | /// A zoom the app offers, nearest to the one asked for. | |
| 56 | pub fn clamp_zoom(zoom: f64) -> f64 { | |
| 57 | ZOOMS.iter().copied().min_by(|a, b| (a - zoom).abs().total_cmp(&(b - zoom).abs())).unwrap_or(1.0) | |
| 58 | } | |
| 59 | } | |
| 60 | ||
| 61 | /// What the settings window shows: the settings, and what the app and the | |
| 62 | /// system know. | |
| 63 | #[derive(Serialize)] | |
| 64 | #[serde(rename_all = "camelCase")] | |
| 65 | pub struct View { | |
| 66 | #[serde(flatten)] | |
| 67 | pub settings: Settings, | |
| 68 | pub open_at_login: bool, | |
| 69 | pub version: String, | |
| 70 | pub platform: String, | |
| 71 | pub server: String, | |
| 72 | pub update_ready: Option<String>, | |
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 73 | /// When the app last asked for updates and heard back, in milliseconds since 1970. |
| 74 | pub checked_at: Option<u64>, | |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 75 | /// The title bar setting differs from the one the windows have: a restart applies it. |
| 76 | pub title_bar_changed: bool, | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 77 | } |
| 78 | ||
| 79 | pub fn view(app: &AppHandle) -> View { | |
| 80 | View { | |
| 81 | settings: config::settings(app), | |
| 82 | open_at_login: app.autolaunch().is_enabled().unwrap_or(false), | |
| 83 | version: env!("CARGO_PKG_VERSION").into(), | |
| 84 | platform: std::env::consts::OS.into(), | |
| 85 | server: config::server(app).to_string(), | |
| 86 | update_ready: crate::updates::ready(app), | |
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 87 | checked_at: crate::updates::checked_at(app), |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 88 | title_bar_changed: crate::frame::started(app) != crate::frame::custom(app), |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 89 | } |
| 90 | } | |
| 91 | ||
| 92 | /// Keeps new settings and puts them to work at once. | |
| 93 | pub fn apply(app: &AppHandle, mut settings: Settings) -> Result<(), String> { | |
| 94 | settings.zoom = Settings::clamp_zoom(settings.zoom); | |
| 95 | let before = config::settings(app); | |
| 96 | config::set_settings(app, settings.clone())?; | |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 97 | if let Some(window) = app.get_webview_window(WINDOW).filter(|_| before.zoom != settings.zoom) { |
| 98 | let _ = window.set_zoom(settings.zoom); | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 99 | } |
| 100 | if before.badge != settings.badge { | |
| 101 | crate::badge::refresh(app); | |
| 102 | } | |
| 103 | Ok(()) | |
| 104 | } | |
| 105 | ||
| 106 | /// Turns opening at sign-in on or off, with the system. | |
| 107 | pub fn open_at_login(app: &AppHandle, on: bool) -> Result<(), String> { | |
| 108 | let autolaunch = app.autolaunch(); | |
| 109 | let result = if on { autolaunch.enable() } else { autolaunch.disable() }; | |
| 110 | result.map_err(|error| error.to_string()) | |
| 111 | } | |
| 112 | ||
| 113 | /// Steps the zoom in (1), out (-1), or back to as it is (0). | |
| 114 | pub fn zoom(app: &AppHandle, step: i32) { | |
| 115 | let mut settings = config::settings(app); | |
| 116 | let at = ZOOMS.iter().position(|zoom| *zoom == Settings::clamp_zoom(settings.zoom)).unwrap_or(4); | |
| 117 | settings.zoom = match step { | |
| 118 | 0 => 1.0, | |
| 119 | 1 => ZOOMS[(at + 1).min(ZOOMS.len() - 1)], | |
| 120 | _ => ZOOMS[at.saturating_sub(1)], | |
| 121 | }; | |
| 122 | let _ = apply(app, settings); | |
| 123 | } | |
| 124 | ||
| 125 | /// Whether this launch is the system's, at sign-in, and g1t should wait in the tray. | |
| 126 | pub fn starts_hidden(app: &AppHandle) -> bool { | |
| 127 | std::env::args().any(|arg| arg == AT_LOGIN) && config::settings(app).start_hidden | |
| 128 | } | |
| 129 | ||
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 130 | /// What the settings window listens for: check for updates now. |
| 131 | pub const CHECK_EVENT: &str = "g1t-check-updates"; | |
| 132 | /// What it listens for: an update is downloaded, with its version. | |
| 133 | pub const READY_EVENT: &str = "g1t-update-ready"; | |
| 134 | ||
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 135 | /// Opens the settings window, or brings it forward. |
| 136 | pub fn open(app: &AppHandle) { | |
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 137 | show(app, false); |
| 138 | } | |
| 139 | ||
| 140 | /// "Check for updates…" in a menu: the settings window opens and checks, | |
| 141 | /// and says what it found, the download included, in its Updates section. | |
| 142 | pub fn open_and_check(app: &AppHandle) { | |
| 143 | show(app, true); | |
| 144 | } | |
| 145 | ||
| 146 | fn show(app: &AppHandle, check: bool) { | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 147 | if let Some(window) = app.get_webview_window(SETTINGS) { |
| 148 | let _ = window.unminimize(); | |
| 149 | let _ = window.show(); | |
| 150 | let _ = window.set_focus(); | |
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 151 | if check { |
| 152 | let _ = app.emit_to(SETTINGS, CHECK_EVENT, ()); | |
| 153 | } | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 154 | return; |
| 155 | } | |
| Checking for updates in the desktop app answers in its settings window, with a restart beside a downloaded version. | 156 | // A window still loading has no listener yet: the address asks instead. |
| 157 | let page = if check { "settings.html?check" } else { "settings.html" }; | |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 158 | let custom = crate::frame::started(app); |
| 159 | let builder = WebviewWindowBuilder::new(app, SETTINGS, WebviewUrl::App(page.into())) | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 160 | .title("g1t settings") |
| The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again. | 161 | .initialization_script(crate::bridge::script(SETTINGS, custom)); |
| 162 | let built = crate::frame::style(builder, custom) | |
| The desktop app has settings of its own and a menu, and it is for using g1t, not browsing it. | 163 | .inner_size(560.0, 680.0) |
| 164 | .min_inner_size(440.0, 420.0) | |
| 165 | .maximizable(false) | |
| 166 | .minimizable(false) | |
| 167 | .center() | |
| 168 | // The page is bundled with the app; its links go to the browser (settings.js). | |
| 169 | .on_navigation(config::is_launcher) | |
| 170 | .build(); | |
| 171 | match built { | |
| 172 | Ok(window) => { | |
| 173 | let _ = window.set_focus(); | |
| 174 | } | |
| 175 | Err(error) => eprintln!("g1t: the settings window could not open: {error}"), | |
| 176 | } | |
| 177 | } | |
| 178 | ||
| 179 | #[cfg(test)] | |
| 180 | mod tests { | |
| 181 | use super::Settings; | |
| 182 | ||
| 183 | #[test] | |
| 184 | fn a_missing_setting_is_its_default() { | |
| 185 | let settings: Settings = serde_json::from_str(r#"{"closeToTray":false}"#).unwrap(); | |
| 186 | assert!(!settings.close_to_tray); | |
| 187 | assert!(settings.notifications); | |
| 188 | assert_eq!(settings.zoom, 1.0); | |
| 189 | } | |
| 190 | ||
| 191 | #[test] | |
| 192 | fn zoom_is_one_the_app_offers() { | |
| 193 | assert_eq!(Settings::clamp_zoom(1.07), 1.1); | |
| 194 | assert_eq!(Settings::clamp_zoom(9.0), 2.0); | |
| 195 | assert_eq!(Settings::clamp_zoom(0.1), 0.67); | |
| 196 | } | |
| 197 | } |