mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 02:12:00 +03:00
settings: tabbed panel, date format, update controls
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.
This commit is contained in:
parent
5af7c66c3c
commit
1e06281f5e
6 changed files with 210 additions and 66 deletions
79
index.html
79
index.html
|
|
@ -47,6 +47,14 @@
|
|||
</main>
|
||||
<div id="settings-panel" class="hidden">
|
||||
<h2 data-i18n="settings">Settings</h2>
|
||||
<div id="settings-tabs" role="tablist">
|
||||
<button class="tab active" data-tab="general" data-i18n="tabGeneral">General</button>
|
||||
<button class="tab" data-tab="format" data-i18n="tabFormat">Format</button>
|
||||
<button class="tab" data-tab="data" data-i18n="tabData">Data</button>
|
||||
<button class="tab" data-tab="updates" data-i18n="tabUpdates">Updates</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-tabpanel" data-tabpanel="general">
|
||||
<label><span data-i18n="theme">Theme</span>
|
||||
<select id="set-theme">
|
||||
<option value="system" data-i18n="system">System</option>
|
||||
|
|
@ -54,6 +62,30 @@
|
|||
<option value="dark" data-i18n="dark">Dark</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span data-i18n="language">Language</span>
|
||||
<select id="set-lang">
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Русский</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span data-i18n="fontsize">Font size</span>
|
||||
<input id="set-fontsize" type="number" min="10" max="32" />
|
||||
</label>
|
||||
<label><span data-i18n="resultswidth">Results column</span>
|
||||
<input id="set-resultswidth" type="range" min="20" max="60" step="1" />
|
||||
</label>
|
||||
<label class="row"><input id="set-alwaysontop" type="checkbox" />
|
||||
<span data-i18n="alwaysontop">Always on top</span>
|
||||
</label>
|
||||
<label><span data-i18n="hotkey">Global hotkey</span>
|
||||
<input id="set-hotkey" type="text" placeholder="Ctrl+Alt+N" />
|
||||
</label>
|
||||
<label class="row"><input id="set-autostart" type="checkbox" />
|
||||
<span data-i18n="autostart">Launch at startup</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-tabpanel hidden" data-tabpanel="format">
|
||||
<label><span data-i18n="precision">Decimal places</span>
|
||||
<input id="set-precision" type="number" min="0" max="15" />
|
||||
</label>
|
||||
|
|
@ -70,36 +102,39 @@
|
|||
<option value=",">0,5</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span data-i18n="language">Language</span>
|
||||
<select id="set-lang">
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Русский</option>
|
||||
<label><span data-i18n="dateformat">Date format</span>
|
||||
<select id="set-dateformat">
|
||||
<option value="system" data-i18n="sysDefault">System</option>
|
||||
<option value="dmy">31.12.2024</option>
|
||||
<option value="mdy">12/31/2024</option>
|
||||
<option value="iso">2024-12-31</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span data-i18n="hotkey">Global hotkey</span>
|
||||
<input id="set-hotkey" type="text" placeholder="Ctrl+Alt+N" />
|
||||
</label>
|
||||
<label class="row"><input id="set-autostart" type="checkbox" />
|
||||
<span data-i18n="autostart">Launch at startup</span>
|
||||
</label>
|
||||
<label class="row"><input id="set-alwaysontop" type="checkbox" />
|
||||
<span data-i18n="alwaysontop">Always on top</span>
|
||||
</label>
|
||||
<label><span data-i18n="fontsize">Font size</span>
|
||||
<input id="set-fontsize" type="number" min="10" max="32" />
|
||||
</label>
|
||||
<label><span data-i18n="resultswidth">Results column</span>
|
||||
<input id="set-resultswidth" type="range" min="20" max="60" step="1" />
|
||||
</div>
|
||||
|
||||
<div class="settings-tabpanel hidden" data-tabpanel="data">
|
||||
<label><span data-i18n="datadir">Data folder</span>
|
||||
<button id="set-datadir" class="path-btn" title=""></button>
|
||||
</label>
|
||||
<label><span data-i18n="bindays">Bin retention (days)</span>
|
||||
<input id="set-bindays" type="number" min="1" max="365" step="1" />
|
||||
</label>
|
||||
<label><span data-i18n="datadir">Data folder</span>
|
||||
<button id="set-datadir" class="path-btn" title=""></button>
|
||||
</label>
|
||||
<div class="settings-footer">
|
||||
<div class="settings-tabpanel-buttons">
|
||||
<button id="open-extensions" data-i18n="extensions">Extensions folder</button>
|
||||
<button id="open-backups" data-i18n="backups">Backups</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-tabpanel hidden" data-tabpanel="updates">
|
||||
<label class="row"><input id="set-autoupdate" type="checkbox" />
|
||||
<span data-i18n="autoUpdate">Automatically check for updates</span>
|
||||
</label>
|
||||
<div class="settings-tabpanel-buttons">
|
||||
<button id="check-update" data-i18n="checkUpdate">Check for updates</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-footer">
|
||||
<button id="close-settings" data-i18n="done">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
20
src/i18n.ts
20
src/i18n.ts
|
|
@ -66,6 +66,16 @@ const dict: Record<string, Record<string, string>> = {
|
|||
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<string, Record<string, string>> = {
|
|||
exportImage: "Скопировать картинкой",
|
||||
imageCopied: "Картинка скопирована",
|
||||
imageFailed: "Не удалось скопировать картинку",
|
||||
checkUpdate: "Проверить обновления",
|
||||
upToDate: "У вас последняя версия",
|
||||
updateCheckFailed: "Не удалось проверить обновления",
|
||||
autoUpdate: "Автоматически проверять обновления",
|
||||
dateformat: "Формат даты",
|
||||
sysDefault: "Системный",
|
||||
tabGeneral: "Общие",
|
||||
tabFormat: "Формат",
|
||||
tabData: "Данные",
|
||||
tabUpdates: "Обновления",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
55
src/main.ts
55
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 = $<HTMLInputElement>("#set-precision");
|
||||
const groupSep = $<HTMLSelectElement>("#set-groupsep");
|
||||
const decimalSep = $<HTMLSelectElement>("#set-decimalsep");
|
||||
const dateFmt = $<HTMLSelectElement>("#set-dateformat");
|
||||
const langSel = $<HTMLSelectElement>("#set-lang");
|
||||
const hotkey = $<HTMLInputElement>("#set-hotkey");
|
||||
const autostart = $<HTMLInputElement>("#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 = $<HTMLInputElement>("#set-autoupdate");
|
||||
autoUpdate.checked = settings.autoUpdateEnabled;
|
||||
autoUpdate.addEventListener("change", () => {
|
||||
settings.autoUpdateEnabled = autoUpdate.checked;
|
||||
void saveSettings(settings);
|
||||
});
|
||||
|
||||
const tabs = document.querySelectorAll<HTMLButtonElement>("#settings-tabs .tab");
|
||||
const tabPanels = document.querySelectorAll<HTMLElement>(".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<void> {
|
|||
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> {
|
|||
}
|
||||
}
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) */
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,24 @@ export interface AvailableUpdate {
|
|||
install(): Promise<void>;
|
||||
}
|
||||
|
||||
/** null if no update, offline, or running outside Tauri (`vite dev`). */
|
||||
export async function checkForUpdate(): Promise<AvailableUpdate | null> {
|
||||
/**
|
||||
* 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<AvailableUpdate | null | "error"> {
|
||||
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<AvailableUpdate | null> {
|
|||
await relaunch();
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.warn("update check failed", e);
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue