diff --git a/package.json b/package.json index 43fd3cc..8806558 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@tauri-apps/plugin-dialog": "2.0.0-alpha.3", "@tauri-apps/plugin-log": "2.0.0-alpha.3", "@tauri-apps/plugin-notification": "2.0.0-alpha.3", + "@tauri-apps/plugin-os": "2.0.0-alpha.6", "@tauri-apps/plugin-process": "2.0.0-alpha.3", "@tauri-apps/plugin-stronghold": "2.0.0-alpha.4", "@tauri-apps/plugin-updater": "2.0.0-alpha.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c50b8c..d3ab48a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ dependencies: '@tauri-apps/plugin-notification': specifier: 2.0.0-alpha.3 version: 2.0.0-alpha.3 + '@tauri-apps/plugin-os': + specifier: 2.0.0-alpha.6 + version: 2.0.0-alpha.6 '@tauri-apps/plugin-process': specifier: 2.0.0-alpha.3 version: 2.0.0-alpha.3 @@ -3516,6 +3519,11 @@ packages: engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} dev: false + /@tauri-apps/api@2.0.0-alpha.13: + resolution: {integrity: sha512-sGgCkFahF3OZAHoGN5Ozt9WK7wJlbVZSgWpPQKNag4nSOX1+Py6VDRTEWriiJHDiV+gg31CWHnNXRy6TFoZmdA==} + engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'} + dev: false + /@tauri-apps/cli-darwin-arm64@2.0.0-alpha.16: resolution: {integrity: sha512-T/yu8+m4XrI1Ja5aVnsv4v5aGqIvwz1egHarMgh4LXrlMioJ60BoxDPfenaUokO6NVee212woFSmH6p4S7V8PA==} engines: {node: '>= 10'} @@ -3659,6 +3667,12 @@ packages: '@tauri-apps/api': 2.0.0-alpha.11 dev: false + /@tauri-apps/plugin-os@2.0.0-alpha.6: + resolution: {integrity: sha512-ld5p56TiWxnGHCysEfHLmRH5+Lz7rzDRC5H5WzofVNPiqp4ra1O5y1lNOWklE01Rm5P1c0t1PbUVdNGpeYA3GQ==} + dependencies: + '@tauri-apps/api': 2.0.0-alpha.13 + dev: false + /@tauri-apps/plugin-process@2.0.0-alpha.3: resolution: {integrity: sha512-XfDBtjW5s584/OJvpaWo3EGtB/lRYltWUmdRotEs4CMWZRRidDfIWf39BWLEXOnik7aMZOA9LVURaTU2QHBy+g==} dependencies: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 254bce3..62a2225 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-log", + "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-updater", "url", @@ -1399,6 +1400,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2436,6 +2447,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -3502,6 +3524,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -3734,6 +3765,23 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-plugin-os" +version = "2.0.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cfaf07f8dcbfd4b2ce6156c4158d9d1419850ffe4e8146b6e890b5381e6906" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "thiserror", +] + [[package]] name = "tauri-plugin-process" version = "2.0.0-alpha.6" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 09a211c..d1eaed0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri = { version = "2.0.0-alpha", features = ["devtools", "tray-icon"] } tauri-plugin-updater = "2.0.0-alpha" tauri-plugin-process = "2.0.0-alpha" tauri-plugin-log = "2.0.0-alpha" +tauri-plugin-os = "2.0.0-alpha" reqwest = { version = "0.11.22", features = ["json"] } url = "2.4.1" chrono = "0.4" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 837d409..05de965 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -32,6 +32,7 @@ pub fn run() { let mut app = tauri::Builder::default() .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_os::init()) // Add logging plugin .plugin( tauri_plugin_log::Builder::default() @@ -39,11 +40,11 @@ pub fn run() { .targets([ Target::new(TargetKind::Webview), Target::new(TargetKind::LogDir { - file_name: Some("webview.log".into()), + file_name: Some("webview".into()), }) .filter(|metadata| metadata.target() == WEBVIEW_TARGET), Target::new(TargetKind::LogDir { - file_name: Some("rust.log".into()), + file_name: Some("rust".into()), }) .filter(|metadata| metadata.target() != WEBVIEW_TARGET), ]) @@ -90,7 +91,9 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ close_splashscreen, updater::check_for_updates, - updater::install_update + updater::download_update, + updater::install_update, + updater::clear_update_cache ]) .build(context) .expect("error while running tauri application"); diff --git a/src-tauri/src/updater.rs b/src-tauri/src/updater.rs index c6e7b4b..ab28ffb 100644 --- a/src-tauri/src/updater.rs +++ b/src-tauri/src/updater.rs @@ -33,14 +33,24 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi return; } + match window.emit("CHECKING_FOR_UPDATE", Some(serde_json::json!({}))) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to emit update checking event: {:?}", e); + } + } + tauri::async_runtime::spawn(async move { - println!("Searching for update file on github."); + println!("[Updater] Searching for update file on github."); // Custom configure the updater. let github_releases_endpoint = "https://api.github.com/repos/spacebarchat/client/releases"; let github_releases_endpoint = match Url::parse(github_releases_endpoint) { Ok(url) => url, Err(e) => { - println!("Failed to parse url: {:?}. Failed to check for updates", e); + println!( + "[Updater] Failed to parse url: {:?}. Failed to check for updates", + e + ); return; } }; @@ -54,7 +64,7 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi Ok(response) => response, Err(e) => { println!( - "Failed to send request: {:?}. Failed to check for updates", + "[Updater] Failed to send request: {:?}. Failed to check for updates", e ); return; @@ -63,7 +73,7 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi if response.status() != reqwest::StatusCode::OK { println!( - "Non OK status code: {:?}. Failed to check for updates", + "[Updater] Non OK status code: {:?}. Failed to check for updates", response.status() ); return; @@ -72,7 +82,7 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi Ok(releases) => releases, Err(e) => { println!( - "Failed to parse response: {:?}. Failed to check for updates", + "[Updater] Failed to parse response: {:?}. Failed to check for updates", e ); return; @@ -81,7 +91,7 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi // check if there are any releases if releases.len() == 0 { - println!("No releases found. Failed to check for updates"); + println!("[Updater] No releases found. Failed to check for updates"); return; } @@ -102,7 +112,7 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi let tauri_release_asset = match tauri_release_asset { Some(tauri_release_asset) => tauri_release_asset, None => { - println!("Failed to find latest.json asset. Failed to check for updates\n\nFound Assets are:"); + println!("[Updater] Failed to find latest.json asset. Failed to check for updates\n\nFound Assets are:"); // Print a list of the assets found for asset in latest_release.assets.iter() { println!(" {:?}", asset.name); @@ -114,23 +124,29 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi let tauri_release_endpoint = match Url::parse(&tauri_release_asset.browser_download_url) { Ok(url) => url, Err(e) => { - println!("Failed to parse url: {:?}. Failed to check for updates", e); + println!( + "[Updater] Failed to parse url: {:?}. Failed to check for updates", + e + ); return; } }; let updater_builder = match handle .updater_builder() .version_comparator(|current_version, latest_version| { - println!("Current version: {}", current_version); - println!("Latest version: {}", latest_version.version.clone()); + println!("[Updater] Current version: {}", current_version); + println!( + "[Updater] Latest version: {}", + latest_version.version.clone() + ); if latest_version.version > current_version { - println!("Latest version is greater than current version. "); + println!("[Updater] Latest version is greater than current version. "); return true; } if latest_version.version < current_version { - println!("Latest version is lower than current version. "); + println!("[Updater] Latest version is lower than current version. "); return false; } @@ -142,7 +158,7 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi Ok(updater_builder) => updater_builder, Err(e) => { println!( - "Failed to build updater builder: {:?}. Failed to check for updates", + "[Updater] Failed to build updater builder: {:?}. Failed to check for updates", e ); return; @@ -153,18 +169,18 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi Ok(updater) => updater, Err(e) => { println!( - "Failed to build updater: {:?}. Failed to check for updates", + "[Updater] Failed to build updater: {:?}. Failed to check for updates", e ); return; } }; - println!("Checking for updates"); + println!("[Updater] Checking for updates"); let response = updater.check().await; - println!("Update check response: {:?}", response); + println!("[Updater] Update check response: {:?}", response); match response { Ok(Some(update)) => { @@ -174,40 +190,198 @@ pub fn check_for_updates(ignore_prereleases: bool, window: tauri::Wi // } UPDATE_INFO.lock().unwrap().replace(update.clone()); - match window.emit( - "UPDATE_AVAILABLE", - Some(UpdateAvailable { - version: update.version, - body: update.body, - }), - ) { + let package_path = handle + .path() + .app_local_data_dir() + .unwrap() + .join("update.sbcup"); + + // if we already have an update package, emit the update downloaded event + if package_path.exists() { + match window.emit( + "UPDATE_DOWNLOADED", + Some(UpdateAvailable { + version: update.version.clone(), + body: update.body.clone(), + }), + ) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to emit update downloaded event: {:?}", e); + } + } + return; + } + + // otherwise emit the update available event + match window.emit("UPDATE_AVAILABLE", Some({})) { Ok(_) => {} Err(e) => { - println!("Failed to emit update available event: {:?}", e); + println!("[Updater] Failed to emit update available event: {:?}", e); + } + } + + return download_update(window).await; + } + Ok(None) => { + println!("[Updater] No update available"); + match window.emit("UPDATE_NOT_AVAILABLE", Some({})) { + Ok(_) => {} + Err(e) => { + println!( + "[Updater] Failed to emit update not available event: {:?}", + e + ); } } } - _ => {} + Err(e) => { + println!("[Updater] Failed to check for updates: {:?}.", e); + } } }); } #[tauri::command] -pub async fn install_update(_window: tauri::Window) { - println!("Downloading and installing update!"); +pub async fn download_update(window: tauri::Window) { + println!("[Updater] Downloading update package"); let update = match UPDATE_INFO.lock().unwrap().clone() { Some(update) => update, None => { - println!("No update found to install"); + println!("[Updater] No update found to download"); return; } }; - let install_response = update.download_and_install(|_, _| {}, || {}).await; - if let Err(e) = install_response { - println!("Failed to install update: {:?}", e); + // emit UPDATE_DOWNLOADING + match window.emit("UPDATE_DOWNLOADING", Some({})) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to emit update downloading event: {:?}", e); + } + } + + let on_chunk = |size: usize, progress: Option| { + println!( + "[Updater] Received chunk: size={}, progress={:?}", + size, progress + ); + }; + + let on_download_finish = || { + println!("[Updater] Download finished!"); + }; + + let download_response = update.download(on_chunk, on_download_finish).await; + + if let Err(e) = download_response { + println!("[Updater] Failed to download update: {:?}", e); } else { - println!("Update installed"); + println!("[Updater] Update downloaded"); + + let handle = window.app_handle().clone(); + let package_path = handle + .path() + .app_local_data_dir() + .unwrap() + .join("update.sbcup"); + println!("[Updater] Saving update package to {:?}", package_path); + + // store download_response bytes to a file + match std::fs::write(package_path.clone(), download_response.unwrap()) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to save update package: {:?}", e); + } + } + } + + match window.emit( + "UPDATE_DOWNLOADED", + Some(UpdateAvailable { + version: update.version.clone(), + body: update.body.clone(), + }), + ) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to emit update downloaded event: {:?}", e); + } + } +} + +#[tauri::command] +pub async fn install_update(window: tauri::Window) { + println!("[Updater] Installing update package"); + + let update = match UPDATE_INFO.lock().unwrap().clone() { + Some(update) => update, + None => { + println!("[Updater] No update found to install"); + return; + } + }; + + let handle = window.app_handle().clone(); + let package_path = handle + .path() + .app_local_data_dir() + .unwrap() + .join("update.sbcup"); + + // check if the update package exists + if !package_path.exists() { + println!("[Updater] No pending update found to install"); + return; + } + + // read in the update package bytes + let bytes = match std::fs::read(package_path.clone()) { + Ok(bytes) => bytes, + Err(e) => { + println!("[Updater] Failed to read update package: {:?}", e); + return; + } + }; + + let install_response = update.install(bytes); + + if let Err(e) = install_response { + println!("[Updater] Failed to install update: {:?}", e); + } else { + println!("[Updater] Update installed"); + + // remove the update package + match std::fs::remove_file(package_path) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to remove update package: {:?}", e); + } + } + } +} + +#[tauri::command] +pub fn clear_update_cache(window: tauri::Window) { + let handle = window.app_handle().clone(); + let package_path = handle + .path() + .app_local_data_dir() + .unwrap() + .join("update.sbcup"); + + // check if the update package exists + if !package_path.exists() { + println!("[Updater] No pending update found to clear"); + return; + } + + // remove the update package + match std::fs::remove_file(package_path) { + Ok(_) => {} + Err(e) => { + println!("[Updater] Failed to remove update package: {:?}", e); + } } } diff --git a/src/App.tsx b/src/App.tsx index d8a0f10..af2d184 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,7 +52,7 @@ function App() { ); Globals.load(); - app.loadToken(); + app.loadSettings(); logger.debug("Loading complete"); app.setAppLoading(false); diff --git a/src/components/UserPanel.tsx b/src/components/UserPanel.tsx index ff31ed3..4c76bc6 100644 --- a/src/components/UserPanel.tsx +++ b/src/components/UserPanel.tsx @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { modalController } from "../controllers/modals"; import { useAppStore } from "../stores/AppStore"; import User from "../stores/objects/User"; import Avatar from "./Avatar"; @@ -70,7 +71,11 @@ const ActionsWrapper = styled.div` function UserPanel() { const app = useAppStore(); - const openSettingsModal = () => {}; + const openSettingsModal = () => { + modalController.push({ + type: "settings", + }); + }; return ( ` - color: ${(props) => (props.$active ? "#ffffff" : "var(--text-secondary)")}; + color: ${(props) => (props.$active ? "var(--text)" : "var(--text-secondary)")}; &:hover { - color: var(--text); + color: ${(props) => (props.$active ? "var(--text-secondary)" : "var(--text)")}; + cursor: pointer; } `; @@ -115,9 +112,11 @@ interface ActionItemProps { ariaLabel?: string; tooltip: string; onClick?: () => void; + disabled?: boolean; + color?: string; } -function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemProps) { +function ActionItem({ icon, active, ariaLabel, tooltip, disabled, color, onClick }: ActionItemProps) { const logger = useLogger("ChatHeader.tsx:ActionItem"); return ( @@ -131,7 +130,13 @@ function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemPro - + @@ -143,7 +148,7 @@ function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemPro * Top header for channel messages section */ function ChatHeader({ channel }: Props) { - const { memberListVisible, toggleMemberList } = useAppStore(); + const { memberListVisible, toggleMemberList, updaterStore } = useAppStore(); return ( @@ -153,6 +158,36 @@ function ChatHeader({ channel }: Props) { {/* Action Items */} + {updaterStore?.checkingForUpdates && ( + + )} + {updaterStore?.updateAvailable && ( + + )} + {updaterStore?.updateDownloading && ( + + )} + {updaterStore?.updateDownloaded && ( + { + updaterStore.quitAndInstall(); + }} + /> + )} {/* */} Search diff --git a/src/components/modals/SettingsModal.tsx b/src/components/modals/SettingsModal.tsx index ac6c06d..d6be585 100644 --- a/src/components/modals/SettingsModal.tsx +++ b/src/components/modals/SettingsModal.tsx @@ -1,3 +1,143 @@ -export function SettingsModal() { - return null; +import { FormControlLabel, FormGroup, Switch } from "@mui/material"; +import { getTauriVersion, getVersion } from "@tauri-apps/api/app"; +import { arch, locale, platform, version } from "@tauri-apps/plugin-os"; +import { observer } from "mobx-react-lite"; +import React from "react"; +import styled from "styled-components"; +import { ModalProps } from "../../controllers/modals"; +import { useAppStore } from "../../stores/AppStore"; +import { isTauri } from "../../utils/Utils"; +import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison"; +import Button from "../Button"; +import Link from "../Link"; +import { Modal } from "./ModalComponents"; + +const Wrapper = styled.div` + padding: 16px 0; + gap: 8px; + display: flex; + flex-direction: column; +`; + +const ActionWrapper = styled.div` + margin-top: 20px; + gap: 8px; + display: flex; +`; + +const VersionWrapper = styled.div` + display: flex; + flex-direction: column; + user-select: text; + + & > span { + color: var(--text-secondary); + } +`; + +interface VersionInfo { + tauri: string; + app: string; + platform: { + name: string; + arch: string; + version: string; + locale: string | null; + }; } + +export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => { + const app = useAppStore(); + const [versionInfo, setVersionInfo] = React.useState(undefined); + + const getVersionInfo = React.useMemo( + () => async () => { + const [tauriVersion, appVersion, platformName, platformArch, platformVersion, platformLocale] = + await Promise.all([getTauriVersion(), getVersion(), platform(), arch(), version(), locale()]); + + setVersionInfo({ + tauri: tauriVersion, + app: appVersion, + platform: { + name: platformName, + arch: platformArch, + version: platformVersion, + locale: platformLocale, + }, + }); + }, + [], + ); + + React.useEffect(() => { + if (isTauri) { + getVersionInfo(); + } + }, [getVersionInfo]); + + return ( + + + + app.setFpsShown(e.target.checked)} />} + label="Show FPS Graph" + /> + + + {isTauri && ( + + app.updaterStore?.setEnabled(e.target.checked)} + /> + } + label="Enabled auto updater" + /> + + )} + + + + Client Version:{" "} + + {GIT_REVISION.substring(0, 7)} + + {` `} + + ({GIT_BRANCH}) + + + + {isTauri && ( + <> + App Version: {versionInfo?.app ?? "Fetching version information..."} + Tauri Version: {versionInfo?.tauri ?? "Fetching version information..."} + Platform: {versionInfo?.platform.name} + Arch: {versionInfo?.platform.arch} + OS Version: {versionInfo?.platform.version} + Locale: {versionInfo?.platform.locale ?? "Unknown"} + + )} + + + + + + + + ); +}); diff --git a/src/controllers/modals/ModalController.tsx b/src/controllers/modals/ModalController.tsx index df99e80..da5392d 100644 --- a/src/controllers/modals/ModalController.tsx +++ b/src/controllers/modals/ModalController.tsx @@ -12,6 +12,7 @@ import { JoinServerModal, KickMemberModal, LeaveServerModal, + SettingsModal, } from "../../components/modals"; import { Modal } from "./types"; @@ -184,4 +185,5 @@ export const modalController = new ModalControllerExtended({ // report_success: ReportSuccess, // modify_displayname: ModifyDisplayname, // changelog_usernames: ChangelogUsernames, + settings: SettingsModal, }); diff --git a/src/controllers/modals/types.ts b/src/controllers/modals/types.ts index 688b544..1e6f5b2 100644 --- a/src/controllers/modals/types.ts +++ b/src/controllers/modals/types.ts @@ -10,7 +10,7 @@ export type Modal = { key?: string; } & ( | { - type: "add_server" | "create_server" | "join_server"; + type: "add_server" | "create_server" | "join_server" | "settings"; } | { type: "error"; diff --git a/src/stores/AppStore.ts b/src/stores/AppStore.ts index 7ca3ad6..48a87d1 100644 --- a/src/stores/AppStore.ts +++ b/src/stores/AppStore.ts @@ -3,6 +3,7 @@ import { action, computed, makeAutoObservable, observable } from "mobx"; import secureLocalStorage from "react-secure-storage"; import Logger from "../utils/Logger"; import REST from "../utils/REST"; +import { isTauri } from "../utils/Utils"; import AccountStore from "./AccountStore"; import ChannelStore from "./ChannelStore"; import ExperimentsStore from "./ExperimentsStore"; @@ -13,6 +14,7 @@ import PresenceStore from "./PresenceStore"; import PrivateChannelStore from "./PrivateChannelStore"; import RoleStore from "./RoleStore"; import ThemeStore from "./ThemeStore"; +import UpdaterStore from "./UpdaterStore"; import UserStore from "./UserStore"; import Channel from "./objects/Channel"; import Guild from "./objects/Guild"; @@ -46,6 +48,8 @@ export default class AppStore { @observable experiments = new ExperimentsStore(); @observable presences = new PresenceStore(this); @observable queue = new MessageQueue(this); + @observable updaterStore: UpdaterStore | null = null; + @observable activeGuild: Guild | null = null; @observable activeGuildId: Snowflake | undefined | "@me" = "@me"; @observable activeChannel: Channel | null = null; @@ -55,6 +59,10 @@ export default class AppStore { constructor() { makeAutoObservable(this); + if (isTauri) { + this.updaterStore = new UpdaterStore(this); + } + // bind this in toggleMemberList this.toggleMemberList = this.toggleMemberList.bind(this); // bind this in windowToggleFps @@ -77,43 +85,11 @@ export default class AppStore { this.isAppLoading = value; } - @action - setToken(token: string, save = false) { - this.token = token; - this.tokenLoaded = true; - if (save) { - secureLocalStorage.setItem("token", token); - this.logger.info("Token saved to storage"); - } - } - @action setUser(user: APIUser) { this.account = new AccountStore(user); } - @action - loadToken() { - const token = secureLocalStorage.getItem("token") as string | null; - - this.tokenLoaded = true; - - if (token) { - this.logger.debug("Loaded token from storage."); - this.setToken(token); - } else { - this.logger.debug("No token found in storage."); - this.setGatewayReady(true); - } - } - - @action - logout() { - this.token = null; - this.tokenLoaded = false; - secureLocalStorage.removeItem("token"); - } - @action setNetworkConnected(value: boolean) { this.isNetworkConnected = value; @@ -143,11 +119,6 @@ export default class AppStore { this.activeChannel = (id ? this.channels.get(id) : null) ?? null; } - @action - setFpsShown(value: boolean) { - this.fpsShown = value; - } - @action toggleMemberList() { this.memberListVisible = !this.memberListVisible; @@ -157,6 +128,73 @@ export default class AppStore { windowToggleFps() { this.setFpsShown(!this.fpsShown); } + + // stuff mainly for settings, really anything that uses local storage + + @action + setToken(token: string, save = false) { + this.token = token; + this.tokenLoaded = true; + if (save) { + secureLocalStorage.setItem("token", token); + this.logger.info("Token saved to storage"); + } + } + + @action + loadToken() { + const token = secureLocalStorage.getItem("token") as string | null; + + this.tokenLoaded = true; + + if (token) { + this.logger.debug("Loaded token from storage."); + this.setToken(token); + } else { + this.logger.debug("No token found in storage."); + this.setGatewayReady(true); + } + } + + @action + logout() { + this.token = null; + this.tokenLoaded = false; + this.isAppLoading = false; + this.isGatewayReady = true; + secureLocalStorage.clear(); + } + + @action + setFpsShown(value: boolean) { + this.fpsShown = value; + + secureLocalStorage.setItem("fpsShown", value); + } + + @action + loadFpsShown() { + this.fpsShown = (secureLocalStorage.getItem("fpsShown") as boolean | null) ?? false; + } + + @action + setUpdaterEnabled(value: boolean) { + this.updaterStore?.setEnabled(value); + + secureLocalStorage.setItem("updaterEnabled", value); + } + + @action + loadUpdaterEnabled() { + this.updaterStore?.setEnabled((secureLocalStorage.getItem("updaterEnabled") as boolean | null) ?? true); + } + + @action + loadSettings() { + this.loadFpsShown(); + this.loadToken(); + this.loadUpdaterEnabled(); + } } export const appStore = new AppStore(); diff --git a/src/stores/UpdaterStore.ts b/src/stores/UpdaterStore.ts new file mode 100644 index 0000000..990c2d4 --- /dev/null +++ b/src/stores/UpdaterStore.ts @@ -0,0 +1,182 @@ +import { listen } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/primitives"; +import { action, makeAutoObservable, observable } from "mobx"; +import useLogger from "../hooks/useLogger"; +import Logger from "../utils/Logger"; +import AppStore from "./AppStore"; + +export default class UpdaterStore { + private readonly logger: Logger = useLogger("UpdaterStore"); + @observable initialized: boolean = false; + @observable enabled: boolean = true; + @observable checkingForUpdates: boolean = false; + @observable updateDownloading: boolean = false; + @observable updateDownloaded: boolean = false; + @observable updateAvailable: boolean = false; + @observable timer: NodeJS.Timeout | null = null; + + constructor(private readonly app: AppStore) { + this.logger.info("Initializing UpdaterStore"); + makeAutoObservable(this); + + const setupListeners = async () => { + await listen("CHECKING_FOR_UPDATE", () => { + this.logger.debug("Checking for updates"); + this.setCheckingForUpdates(true); + this.setUpdateAvailable(false); + this.setUpdateDownloading(false); + this.setUpdateDownloaded(false); + }); + + await listen("UPDATE_AVAILABLE", () => { + this.logger.debug("Update available"); + this.setCheckingForUpdates(false); + this.setUpdateAvailable(true); + this.setUpdateDownloading(false); + this.setUpdateDownloaded(false); + }); + + await listen("UPDATE_NOT_AVAILABLE", () => { + this.logger.debug("Update not available"); + this.setCheckingForUpdates(false); + this.setUpdateAvailable(false); + this.setUpdateDownloading(false); + this.setUpdateDownloaded(false); + }); + + await listen("UPDATE_DOWNLOADING", () => { + this.logger.debug("Update downloading"); + this.setCheckingForUpdates(false); + this.setUpdateAvailable(false); + this.setUpdateDownloading(true); + this.setUpdateDownloaded(false); + }); + + await listen("UPDATE_DOWNLOADED", () => { + this.logger.debug("Update downloaded"); + this.setCheckingForUpdates(false); + this.setUpdateAvailable(false); + this.setUpdateDownloading(false); + this.setUpdateDownloaded(true); + }); + }; + + setupListeners(); + + // @ts-expect-error - expose updater to window, don't use use this though + window.updater = { + setUpdateAvailable: this.setUpdateAvailable.bind(this), + setUpdateDownloading: this.setUpdateDownloading.bind(this), + setUpdateDownloaded: this.setUpdateDownloaded.bind(this), + setCheckingForUpdates: this.setCheckingForUpdates.bind(this), + checkForUpdates: this.checkForUpdates.bind(this), + downloadUpdate: this.downloadUpdate.bind(this), + quitAndInstall: this.quitAndInstall.bind(this), + clearUpdateCache: this.clearCache.bind(this), + }; + } + + @action + setCheckingForUpdates(value: boolean) { + this.checkingForUpdates = value; + } + + @action + setUpdateAvailable(value: boolean) { + this.updateAvailable = value; + } + + @action + setUpdateDownloading(value: boolean) { + this.updateDownloading = value; + } + + @action + setUpdateDownloaded(value: boolean) { + this.updateDownloaded = value; + } + + async checkForUpdates() { + if (this.checkingForUpdates) { + this.logger.warn("Already checking for updates, skipping check"); + return; + } + + // if (this.app.settings.ignoreUpdates) { + // this.logger.warn("Ignoring updates, skipping check"); + // return; + // } + this.logger.debug("Invoking update check"); + await invoke("check_for_updates", { ignorePrereleases: false }); + } + + async downloadUpdate() { + if (this.updateDownloading) { + this.logger.warn("Already downloading an update"); + return; + } + + if (this.updateDownloaded) { + this.logger.warn("An update is already pending installation"); + return; + } + + this.logger.debug("Invoking update download"); + await invoke("download_update"); + } + + async quitAndInstall() { + if (!this.updateAvailable) { + this.logger.warn("No update is pending installation"); + return; + } + + this.logger.debug("Invoking update install"); + await invoke("install_update"); + } + + @action + setEnabled(value: boolean) { + this.enabled = value; + + if (value) { + this.enable(); + } else { + this.disable(); + } + } + + @action + async enable() { + this.logger.debug("Enabling updater"); + + if (this.initialized) { + this.logger.debug("Updater already initialized, skipping init"); + return; + } + + // initial update check + await this.checkForUpdates(); + + // start an update timer + this.timer = setInterval(async () => { + this.logger.debug("[UpdateTimer] Checking for updates"); + await this.checkForUpdates(); + }, 36e5); // 1 hour + + this.initialized = true; + } + + @action + disable() { + this.logger.debug("Disabling updater"); + if (this.timer) { + clearInterval(this.timer); + } + } + + async clearCache() { + this.logger.debug("Clearing update cache"); + await invoke("clear_update_cache"); + } +}