diff --git a/Cargo.lock b/Cargo.lock index a8d5416..6f8af31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,7 +1089,7 @@ dependencies = [ [[package]] name = "hound" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "clap", @@ -1104,7 +1104,7 @@ dependencies = [ [[package]] name = "hound-api" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "serde", @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "hound-defs" -version = "0.1.3" +version = "0.1.4" dependencies = [ "ed25519-dalek", "serde", @@ -1124,7 +1124,7 @@ dependencies = [ [[package]] name = "hound-mcp" -version = "0.1.3" +version = "0.1.4" dependencies = [ "hound-api", "hound-supply", @@ -1134,7 +1134,7 @@ dependencies = [ [[package]] name = "hound-supply" -version = "0.1.3" +version = "0.1.4" dependencies = [ "hound-defs", "serde", @@ -1143,7 +1143,7 @@ dependencies = [ [[package]] name = "houndd" -version = "0.1.3" +version = "0.1.4" dependencies = [ "anyhow", "ed25519-dalek", diff --git a/Cargo.toml b/Cargo.toml index f794998..ee66e19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "0.1.3" +version = "0.1.4" edition = "2021" license = "MIT" repository = "https://git.joelovestech.com/Hound/Antivirus.git" diff --git a/dist/hound_0.1.4_amd64.deb b/dist/hound_0.1.4_amd64.deb new file mode 100644 index 0000000..5a78369 Binary files /dev/null and b/dist/hound_0.1.4_amd64.deb differ diff --git a/gui/dist/app.js b/gui/dist/app.js index e523cef..1cc77e1 100644 --- a/gui/dist/app.js +++ b/gui/dist/app.js @@ -13,6 +13,27 @@ const $ = (id) => document.getElementById(id); /// Paint the chosen theme. "auto" defers to the desktop, which the webview /// reports through prefers-color-scheme — and which can change while the app /// is open, so the listener stays attached rather than sampling once. +// Appearance is a per-user preference and is stored here, not in the daemon. +// Daemon settings need root to write, so putting a theme toggle there meant a +// password prompt to change your icon colour, and one user's choice would +// have been every user's. +const PREFS_KEY = "hound.appearance"; +function loadPrefs() { + try { + return JSON.parse(localStorage.getItem(PREFS_KEY)) || {}; + } catch { + return {}; + } +} +function savePrefs(patch) { + try { + localStorage.setItem(PREFS_KEY, JSON.stringify({ ...loadPrefs(), ...patch })); + } catch { + // A browser refusing storage is not a reason to refuse the change; it + // just will not survive a restart. + } +} + const SYSTEM_DARK = window.matchMedia("(prefers-color-scheme: dark)"); let themeChoice = "auto"; function applyTheme(choice) { @@ -436,9 +457,9 @@ async function loadSettings() { $("set-ransom").checked = s.ransomware_guard; $("set-ransom-thresh").value = s.ransomware_threshold_per_min; $("set-rootkit").checked = s.rootkit_enabled; - $("set-theme").value = s.theme || "auto"; - $("set-monochrome").checked = (s.tray_icon_style || "colour") === "monochrome"; - applyTheme(s.theme || "auto"); + const prefs = loadPrefs(); + $("set-theme").value = prefs.theme || "auto"; + $("set-monochrome").checked = prefs.monochrome === true; } catch (e) { $("settings-msg").textContent = "Failed to load: " + explain(e); } @@ -453,8 +474,6 @@ async function saveSettings() { s.realtime_enabled = $("set-realtime").checked; s.realtime_watch = $("set-watch").value.split("\n").map((x) => x.trim()).filter(Boolean); s.on_detect = $("set-ondetect").value; - s.theme = $("set-theme").value; - s.tray_icon_style = $("set-monochrome").checked ? "monochrome" : "colour"; s.max_file_size_mb = Math.max(1, parseInt($("set-maxsize").value, 10) || 0); s.exclude_paths = $("set-excludes").value.split("\n").map((x) => x.trim()).filter(Boolean); s.recursive_default = $("set-recursive").checked; @@ -462,17 +481,32 @@ async function saveSettings() { s.ransomware_threshold_per_min = Math.max(10, parseInt($("set-ransom-thresh").value, 10) || 100); s.rootkit_enabled = $("set-rootkit").checked; try { + $("settings-msg").textContent = "Saving…"; const saved = await invoke("set_settings", { s }); paused = saved.paused; $("btn-pause").textContent = paused ? "Resume Protection" : "Pause Protection"; $("settings-msg").textContent = "Saved ✓"; - setTimeout(() => ($("settings-msg").textContent = ""), 2500); + setTimeout(() => { + if ($("settings-msg").textContent === "Saved ✓") $("settings-msg").textContent = ""; + }, 2000); loadRealtime().catch(() => {}); } catch (e) { $("settings-msg").textContent = explain(e); + // The control now shows something the daemon rejected. Put it back + // rather than leaving the screen disagreeing with reality. + loadSettings().catch(() => {}); } } +/// Persist on change, with a short delay so typing in a text box does not +/// send a request per keystroke — and so toggling three switches in a row is +/// one write, and one authentication prompt, rather than three. +let saveTimer = null; +function saveSoon(delay = 400) { + clearTimeout(saveTimer); + saveTimer = setTimeout(() => saveSettings(), delay); +} + // ── Wiring ───────────────────────────────────────────────────────── function setButtons(disabled) { for (const id of ["btn-scan-home", "btn-scan-custom", "btn-update", "btn-pause"]) @@ -508,7 +542,20 @@ $("btn-pause").addEventListener("click", togglePause); $("btn-qt-refresh").addEventListener("click", () => loadQuarantine()); $("btn-alerts-refresh").addEventListener("click", () => loadAlerts()); $("btn-rootkit").addEventListener("click", runRootkit); -$("btn-settings-save").addEventListener("click", saveSettings); +// There is no Save button. A settings screen that needs one lets you leave +// with your changes discarded, and it hid a real failure: writing these needs +// root, so a save could be refused while the switch stayed where you put it. +for (const id of [ + "set-paused", "set-autoupdate", "set-notify", "set-realtime", + "set-ondetect", "set-recursive", "set-ransom", "set-rootkit", +]) { + $(id).addEventListener("change", () => saveSoon(0)); +} +// Typed fields wait for a pause in typing. +for (const id of ["set-watch", "set-maxsize", "set-excludes", "set-ransom-thresh"]) { + $(id).addEventListener("input", () => saveSoon(800)); + $(id).addEventListener("blur", () => saveSoon(0)); +} $("btn-qt-add").addEventListener("click", async () => { const path = $("qt-add-path").value.trim(); @@ -571,6 +618,24 @@ async function boot() { // Tells the watchdog in index.html that the script actually ran. window.__houndBooted = true; -$("set-theme").addEventListener("change", (e) => applyTheme(e.target.value)); +$("set-theme").addEventListener("change", (e) => { + savePrefs({ theme: e.target.value }); + applyTheme(e.target.value); +}); +$("set-monochrome").addEventListener("change", (e) => { + const monochrome = e.target.checked; + savePrefs({ monochrome }); + // Repaint the tray now. Waiting for a Save button, or for the next poll, + // makes a toggle feel broken. + invoke("set_tray_style", { monochrome }).catch(() => {}); +}); + +// Apply the stored appearance before the first paint, so the window does not +// flash the wrong theme on the way in. +(() => { + const prefs = loadPrefs(); + applyTheme(prefs.theme || "auto"); + invoke("set_tray_style", { monochrome: prefs.monochrome === true }).catch(() => {}); +})(); boot(); diff --git a/gui/dist/index.html b/gui/dist/index.html index 1d3e97b..030cefa 100644 --- a/gui/dist/index.html +++ b/gui/dist/index.html @@ -285,8 +285,8 @@