From 1e06281f5effec06cda0f0ee5c69ff5ec616ef60 Mon Sep 17 00:00:00 2001
From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
Date: Fri, 3 Jul 2026 12:00:39 +0300
Subject: [PATCH] settings: tabbed panel, date format, update controls
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The flat settings list had grown to 14 fields plus footer buttons —
split it into General / Format / Data / Updates tabs (same pill style
as the pinned-doc sidebar, active tab gets the selection color).
Wired the date-format setting into the engine, and reworked update
checks per an actual bug report: a boot-only check is easy to miss on
a tray app that can run for weeks without a cold restart, and every
failure was swallowed silently with no logging at all. Now: failures
are logged (console.warn) and successful "no update" checks say why
(console.info, current version vs. latest published release) so the
next report has something to go on; checks also rerun every 6 hours;
Settings has both a manual "Check for updates" button (reports back
via toast) and a toggle to turn automatic checking off entirely —
manual checks keep working either way.
---
index.html | 139 +++++++++++++++++++++++++++++++------------------
src/i18n.ts | 20 +++++++
src/main.ts | 55 ++++++++++++++++---
src/storage.ts | 6 +++
src/ui/app.css | 34 +++++++++++-
src/updater.ts | 22 ++++++--
6 files changed, 210 insertions(+), 66 deletions(-)
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";
}
}