diff --git a/index.html b/index.html index 190e1ed..fa33f10 100644 --- a/index.html +++ b/index.html @@ -47,59 +47,94 @@ diff --git a/src/i18n.ts b/src/i18n.ts index d2be233..0361241 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -66,6 +66,16 @@ const dict: Record> = { exportImage: "Copy as image", imageCopied: "Image copied", imageFailed: "Could not copy image", + checkUpdate: "Check for updates", + upToDate: "You're on the latest version", + updateCheckFailed: "Could not check for updates", + autoUpdate: "Automatically check for updates", + dateformat: "Date format", + sysDefault: "System", + tabGeneral: "General", + tabFormat: "Format", + tabData: "Data", + tabUpdates: "Updates", }, ru: { settings: "Настройки", @@ -132,6 +142,16 @@ const dict: Record> = { exportImage: "Скопировать картинкой", imageCopied: "Картинка скопирована", imageFailed: "Не удалось скопировать картинку", + checkUpdate: "Проверить обновления", + upToDate: "У вас последняя версия", + updateCheckFailed: "Не удалось проверить обновления", + autoUpdate: "Автоматически проверять обновления", + dateformat: "Формат даты", + sysDefault: "Системный", + tabGeneral: "Общие", + tabFormat: "Формат", + tabData: "Данные", + tabUpdates: "Обновления", }, }; diff --git a/src/main.ts b/src/main.ts index 7f51e69..dc73edb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -197,6 +197,7 @@ function applySettings(): void { precision: settings.precision, groupSeparator: settings.groupSeparator, decimalSeparator: settings.decimalSeparator, + dateFormat: settings.dateFormat, }); editor?.refresh(); syncTitleField(); @@ -208,6 +209,7 @@ function bindSettingsUI(): void { const precision = $("#set-precision"); const groupSep = $("#set-groupsep"); const decimalSep = $("#set-decimalsep"); + const dateFmt = $("#set-dateformat"); const langSel = $("#set-lang"); const hotkey = $("#set-hotkey"); const autostart = $("#set-autostart"); @@ -219,6 +221,7 @@ function bindSettingsUI(): void { precision.value = String(settings.precision); groupSep.value = settings.groupSeparator; decimalSep.value = settings.decimalSeparator; + dateFmt.value = settings.dateFormat; langSel.value = settings.language; hotkey.value = settings.hotkey; autostart.checked = settings.autostart; @@ -236,13 +239,14 @@ function bindSettingsUI(): void { settings.groupSeparator = settings.decimalSeparator === "," ? " " : ","; groupSep.value = settings.groupSeparator; } + settings.dateFormat = dateFmt.value as SettingsData["dateFormat"]; settings.language = langSel.value; settings.fontSize = Math.max(10, Math.min(32, Number(fontSize.value) || 15)); settings.resultsWidth = Math.max(20, Math.min(60, Number(resultsWidth.value) || 42)); applySettings(); void saveSettings(settings); }; - for (const el of [themeSel, precision, groupSep, decimalSep, langSel, fontSize]) { + for (const el of [themeSel, precision, groupSep, decimalSep, dateFmt, langSel, fontSize]) { el.addEventListener("change", save); } resultsWidth.addEventListener("input", save); // live while sliding @@ -346,6 +350,22 @@ function bindSettingsUI(): void { toast(t("folderChanged")); }); + const autoUpdate = $("#set-autoupdate"); + autoUpdate.checked = settings.autoUpdateEnabled; + autoUpdate.addEventListener("change", () => { + settings.autoUpdateEnabled = autoUpdate.checked; + void saveSettings(settings); + }); + + const tabs = document.querySelectorAll("#settings-tabs .tab"); + const tabPanels = document.querySelectorAll(".settings-tabpanel"); + for (const tab of tabs) { + tab.addEventListener("click", () => { + for (const other of tabs) other.classList.toggle("active", other === tab); + for (const panel of tabPanels) panel.classList.toggle("hidden", panel.dataset.tabpanel !== tab.dataset.tab); + }); + } + $("#open-settings").addEventListener("click", () => { $("#settings-panel").classList.toggle("hidden"); }); @@ -354,6 +374,7 @@ function bindSettingsUI(): void { }); $("#open-extensions").addEventListener("click", () => void openExtensionsFolder()); $("#open-backups").addEventListener("click", () => void openBackupsFolder(settings.dataDir)); + $("#check-update").addEventListener("click", () => void checkUpdate(true)); matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (settings.theme === "system") applySettings(); @@ -591,7 +612,12 @@ async function boot(): Promise { localStorage.setItem("summarum.langInit", "1"); } - engine = new SumEngine({ precision: settings.precision, groupSeparator: settings.groupSeparator }); + engine = new SumEngine({ + precision: settings.precision, + groupSeparator: settings.groupSeparator, + decimalSeparator: settings.decimalSeparator, + dateFormat: settings.dateFormat, + }); // snapshots run before the app writes anything setDataDir(settings.dataDir); @@ -852,17 +878,30 @@ async function boot(): Promise { } } - void checkUpdate(); + if (settings.autoUpdateEnabled) void checkUpdate(); + // a tray app can stay running for weeks without a cold restart, and the + // update check above only ever runs once at boot — recheck periodically + // so a long-lived process still notices new releases; re-read the setting + // on every tick since the user can flip it while the app is running + setInterval(() => { if (settings.autoUpdateEnabled) void checkUpdate(); }, 6 * 60 * 60 * 1000); } -async function checkUpdate(): Promise { - const update = await checkForUpdate(); - if (!update) return; - const choice = await askModal(t("updateAvailable").replace("{}", update.version), t("updateInstall"), t("updateLater")); +/** manual = true when the user clicked "Check for updates" — only then report "up to date" / "check failed" */ +async function checkUpdate(manual = false): Promise { + const result = await checkForUpdate(); + if (result === "error") { + if (manual) toast(t("updateCheckFailed")); + return; + } + if (!result) { + if (manual) toast(t("upToDate")); + return; + } + const choice = await askModal(t("updateAvailable").replace("{}", result.version), t("updateInstall"), t("updateLater")); if (choice !== "a") return; toast(t("updateInstalling")); try { - await update.install(); + await result.install(); } catch { toast(t("updateFailed")); } diff --git a/src/storage.ts b/src/storage.ts index b9caf3a..ce99608 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -32,6 +32,10 @@ export interface SettingsData { deletedRetentionDays: number; /** keep the window above others */ alwaysOnTop: boolean; + /** date display layout; "system" also picks day/month order for ambiguous input like 01/02/2024 */ + dateFormat: "system" | "iso" | "dmy" | "mdy"; + /** background update checks at boot + every few hours; manual "Check for updates" always works regardless */ + autoUpdateEnabled: boolean; } export const defaultSettingsData: SettingsData = { @@ -48,6 +52,8 @@ export const defaultSettingsData: SettingsData = { dataDir: "", deletedRetentionDays: 14, alwaysOnTop: false, + dateFormat: "system", + autoUpdateEnabled: true, }; /** "" -> undefined for the Rust side (default app data dir) */ diff --git a/src/ui/app.css b/src/ui/app.css index 12144ce..9ee1862 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -360,6 +360,38 @@ body { } #settings-panel.hidden { display: none; } #settings-panel h2 { margin: 0 0 10px; font-size: 14px; } +#settings-tabs { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border); +} +#settings-tabs .tab { + border: none; + background: none; + color: var(--fg-muted); + font-family: var(--font); + font-size: 12px; + padding: 5px 10px; + border-radius: 6px; + cursor: pointer; +} +#settings-tabs .tab:hover { background: var(--hover); color: var(--fg); } +#settings-tabs .tab.active { background: var(--sel); color: var(--fg); } +.settings-tabpanel.hidden { display: none; } +.settings-tabpanel-buttons { display: flex; gap: 6px; margin-top: 10px; } +.settings-tabpanel-buttons button { + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + border-radius: 6px; + padding: 5px 12px; + cursor: pointer; + font-size: 12.5px; +} +.settings-tabpanel-buttons button:hover { background: var(--hover); } #settings-panel label { display: flex; justify-content: space-between; @@ -394,7 +426,7 @@ body { } .path-btn:hover { background: var(--hover); } -.settings-footer { display: flex; justify-content: space-between; gap: 6px; margin-top: 12px; } +.settings-footer { display: flex; justify-content: flex-end; gap: 6px; margin-top: 12px; } .settings-footer button { border: 1px solid var(--border); background: var(--bg); diff --git a/src/updater.ts b/src/updater.ts index fbce4da..835841f 100644 --- a/src/updater.ts +++ b/src/updater.ts @@ -5,13 +5,24 @@ export interface AvailableUpdate { install(): Promise; } -/** null if no update, offline, or running outside Tauri (`vite dev`). */ -export async function checkForUpdate(): Promise { +/** + * null = no update available (checked successfully); "error" = the check + * itself failed (offline, updater/signature error); running outside Tauri + * (`vite dev`) also reports null. + */ +export async function checkForUpdate(): Promise { if (!isTauri()) return null; try { const { check } = await import("@tauri-apps/plugin-updater"); const update = await check(); - if (!update) return null; + if (!update) { + // visible in devtools console — the fastest way to tell "already on + // the latest published release" apart from "the check silently failed" + const { getVersion } = await import("@tauri-apps/api/app"); + console.info(`no update: running ${await getVersion()}, that's the latest published release`); + return null; + } + console.info(`update available: ${update.currentVersion} -> ${update.version}`); return { version: update.version, async install() { @@ -20,7 +31,8 @@ export async function checkForUpdate(): Promise { await relaunch(); }, }; - } catch { - return null; + } catch (e) { + console.warn("update check failed", e); + return "error"; } }