| 1 | //! The app's own settings: how it starts, what closing the window does, |
| 2 | //! how it tells the person something arrived, the menu bar, zoom and |
| 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}; |
| 10 | use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; |
| 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, |
| 39 | /// The menu bar under the title (Windows and Linux; macOS has its own at the top). |
| 40 | pub menu_bar: bool, |
| 41 | /// The site's zoom, 1 for as it is. |
| 42 | pub zoom: f64, |
| 43 | /// Updates download by themselves; a person can still check from the menu. |
| 44 | pub auto_update: bool, |
| 45 | } |
| 46 | |
| 47 | impl Default for Settings { |
| 48 | fn default() -> Self { |
| 49 | Self { start_hidden: true, close_to_tray: true, notifications: true, flash: true, badge: true, menu_bar: true, zoom: 1.0, auto_update: true } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | impl Settings { |
| 54 | /// A zoom the app offers, nearest to the one asked for. |
| 55 | pub fn clamp_zoom(zoom: f64) -> f64 { |
| 56 | ZOOMS.iter().copied().min_by(|a, b| (a - zoom).abs().total_cmp(&(b - zoom).abs())).unwrap_or(1.0) |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | /// What the settings window shows: the settings, and what the app and the |
| 61 | /// system know. |
| 62 | #[derive(Serialize)] |
| 63 | #[serde(rename_all = "camelCase")] |
| 64 | pub struct View { |
| 65 | #[serde(flatten)] |
| 66 | pub settings: Settings, |
| 67 | pub open_at_login: bool, |
| 68 | pub version: String, |
| 69 | pub platform: String, |
| 70 | pub server: String, |
| 71 | pub update_ready: Option<String>, |
| 72 | } |
| 73 | |
| 74 | pub fn view(app: &AppHandle) -> View { |
| 75 | View { |
| 76 | settings: config::settings(app), |
| 77 | open_at_login: app.autolaunch().is_enabled().unwrap_or(false), |
| 78 | version: env!("CARGO_PKG_VERSION").into(), |
| 79 | platform: std::env::consts::OS.into(), |
| 80 | server: config::server(app).to_string(), |
| 81 | update_ready: crate::updates::ready(app), |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | /// Keeps new settings and puts them to work at once. |
| 86 | pub fn apply(app: &AppHandle, mut settings: Settings) -> Result<(), String> { |
| 87 | settings.zoom = Settings::clamp_zoom(settings.zoom); |
| 88 | let before = config::settings(app); |
| 89 | config::set_settings(app, settings.clone())?; |
| 90 | if let Some(window) = app.get_webview_window(WINDOW) { |
| 91 | if before.menu_bar != settings.menu_bar { |
| 92 | let _ = if settings.menu_bar { window.show_menu() } else { window.hide_menu() }; |
| 93 | } |
| 94 | if before.zoom != settings.zoom { |
| 95 | let _ = window.set_zoom(settings.zoom); |
| 96 | } |
| 97 | } |
| 98 | if before.badge != settings.badge { |
| 99 | crate::badge::refresh(app); |
| 100 | } |
| 101 | Ok(()) |
| 102 | } |
| 103 | |
| 104 | /// Turns opening at sign-in on or off, with the system. |
| 105 | pub fn open_at_login(app: &AppHandle, on: bool) -> Result<(), String> { |
| 106 | let autolaunch = app.autolaunch(); |
| 107 | let result = if on { autolaunch.enable() } else { autolaunch.disable() }; |
| 108 | result.map_err(|error| error.to_string()) |
| 109 | } |
| 110 | |
| 111 | /// Steps the zoom in (1), out (-1), or back to as it is (0). |
| 112 | pub fn zoom(app: &AppHandle, step: i32) { |
| 113 | let mut settings = config::settings(app); |
| 114 | let at = ZOOMS.iter().position(|zoom| *zoom == Settings::clamp_zoom(settings.zoom)).unwrap_or(4); |
| 115 | settings.zoom = match step { |
| 116 | 0 => 1.0, |
| 117 | 1 => ZOOMS[(at + 1).min(ZOOMS.len() - 1)], |
| 118 | _ => ZOOMS[at.saturating_sub(1)], |
| 119 | }; |
| 120 | let _ = apply(app, settings); |
| 121 | } |
| 122 | |
| 123 | /// Whether this launch is the system's, at sign-in, and g1t should wait in the tray. |
| 124 | pub fn starts_hidden(app: &AppHandle) -> bool { |
| 125 | std::env::args().any(|arg| arg == AT_LOGIN) && config::settings(app).start_hidden |
| 126 | } |
| 127 | |
| 128 | /// Opens the settings window, or brings it forward. |
| 129 | pub fn open(app: &AppHandle) { |
| 130 | if let Some(window) = app.get_webview_window(SETTINGS) { |
| 131 | let _ = window.unminimize(); |
| 132 | let _ = window.show(); |
| 133 | let _ = window.set_focus(); |
| 134 | return; |
| 135 | } |
| 136 | let built = WebviewWindowBuilder::new(app, SETTINGS, WebviewUrl::App("settings.html".into())) |
| 137 | .title("g1t settings") |
| 138 | .inner_size(560.0, 680.0) |
| 139 | .min_inner_size(440.0, 420.0) |
| 140 | .maximizable(false) |
| 141 | .minimizable(false) |
| 142 | .center() |
| 143 | // The page is bundled with the app; its links go to the browser (settings.js). |
| 144 | .on_navigation(config::is_launcher) |
| 145 | .build(); |
| 146 | match built { |
| 147 | Ok(window) => { |
| 148 | let _ = window.set_focus(); |
| 149 | } |
| 150 | Err(error) => eprintln!("g1t: the settings window could not open: {error}"), |
| 151 | } |
| 152 | } |
| 153 | |
| 154 | #[cfg(test)] |
| 155 | mod tests { |
| 156 | use super::Settings; |
| 157 | |
| 158 | #[test] |
| 159 | fn a_missing_setting_is_its_default() { |
| 160 | let settings: Settings = serde_json::from_str(r#"{"closeToTray":false}"#).unwrap(); |
| 161 | assert!(!settings.close_to_tray); |
| 162 | assert!(settings.notifications); |
| 163 | assert_eq!(settings.zoom, 1.0); |
| 164 | } |
| 165 | |
| 166 | #[test] |
| 167 | fn zoom_is_one_the_app_offers() { |
| 168 | assert_eq!(Settings::clamp_zoom(1.07), 1.1); |
| 169 | assert_eq!(Settings::clamp_zoom(9.0), 2.0); |
| 170 | assert_eq!(Settings::clamp_zoom(0.1), 0.67); |
| 171 | } |
| 172 | } |