feat: add tabbed settings dialog

This commit is contained in:
2026-05-03 23:12:40 +02:00
parent e8d4d2e400
commit 3df489ba6a
5 changed files with 291 additions and 65 deletions

View File

@@ -58,6 +58,8 @@ The GUI opens in your browser and lets you:
- change the reminder interval - change the reminder interval
- configure automatic refresh - configure automatic refresh
- choose the terminal used for installing updates - choose the terminal used for installing updates
- manage settings in a dedicated dialog with tabs for general behavior, notifications, installation, and ignored packages
- disable reminders, keep install terminals open or let them close, and control confirmation for selective installs
Selective package installs use `pacman -S --needed` or the configured AUR helper with `-S --needed`. On Arch, full system updates are still the safer default because partial upgrades can cause dependency mismatches. Selective package installs use `pacman -S --needed` or the configured AUR helper with `-S --needed`. On Arch, full system updates are still the safer default because partial upgrades can cause dependency mismatches.

View File

@@ -134,6 +134,9 @@ func notifyUpdates(args []string) int {
if result.Total() == 0 { if result.Total() == 0 {
return 0 return 0
} }
if !cfg.NotificationsEnabled {
return 0
}
store, err := state.Load(defaultStatePath()) store, err := state.Load(defaultStatePath())
if err != nil { if err != nil {

View File

@@ -13,6 +13,9 @@ type Config struct {
Terminal string `json:"terminal"` Terminal string `json:"terminal"`
AutoRefreshMinutes int `json:"auto_refresh_minutes"` AutoRefreshMinutes int `json:"auto_refresh_minutes"`
ShowIgnored bool `json:"show_ignored"` ShowIgnored bool `json:"show_ignored"`
NotificationsEnabled bool `json:"notifications_enabled"`
KeepTerminalOpen bool `json:"keep_terminal_open"`
ConfirmSelectedInstalls bool `json:"confirm_selected_installs"`
IgnoredPackages []string `json:"ignored_packages"` IgnoredPackages []string `json:"ignored_packages"`
} }
@@ -23,6 +26,9 @@ func Default() Config {
Terminal: "auto", Terminal: "auto",
AutoRefreshMinutes: 30, AutoRefreshMinutes: 30,
ShowIgnored: false, ShowIgnored: false,
NotificationsEnabled: true,
KeepTerminalOpen: true,
ConfirmSelectedInstalls: true,
IgnoredPackages: []string{}, IgnoredPackages: []string{},
} }
} }
@@ -37,10 +43,25 @@ func Load(path string) (Config, error) {
if err != nil { if err != nil {
return cfg, err return cfg, err
} }
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return cfg, err
}
if err := json.Unmarshal(data, &cfg); err != nil { if err := json.Unmarshal(data, &cfg); err != nil {
return cfg, err return cfg, err
} }
defaults := Default()
if _, ok := raw["notifications_enabled"]; !ok {
cfg.NotificationsEnabled = defaults.NotificationsEnabled
}
if _, ok := raw["keep_terminal_open"]; !ok {
cfg.KeepTerminalOpen = defaults.KeepTerminalOpen
}
if _, ok := raw["confirm_selected_installs"]; !ok {
cfg.ConfirmSelectedInstalls = defaults.ConfirmSelectedInstalls
}
cfg = normalize(cfg) cfg = normalize(cfg)
return cfg, nil return cfg, nil
} }

View File

@@ -16,6 +16,7 @@ const indexHTML = `<!doctype html>
<p id="summary">Pruefe Updates...</p> <p id="summary">Pruefe Updates...</p>
</div> </div>
<div class="actions"> <div class="actions">
<button id="settingsBtn" type="button">Einstellungen</button>
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button> <button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
<button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button> <button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button>
<button id="installBtn" type="button">Alle installieren</button> <button id="installBtn" type="button">Alle installieren</button>
@@ -114,35 +115,56 @@ const indexHTML = `<!doctype html>
</div> </div>
</dl> </dl>
</section> </section>
</aside>
</section>
</main>
<section class="panel settings-panel"> <dialog id="settingsDialog">
<div class="panel-head"> <form id="settingsForm" method="dialog" class="settings-modal">
<header class="modal-head">
<div>
<h2>Einstellungen</h2> <h2>Einstellungen</h2>
<span id="saveState"></span> <p>Update-Verhalten, Installation und ausgeblendete Pakete</p>
</div> </div>
<form id="settingsForm"> <button id="closeSettingsBtn" type="button" class="icon-btn">Schliessen</button>
</header>
<nav class="tabs" aria-label="Einstellungen">
<button class="tab active" type="button" data-tab="general">Allgemein</button>
<button class="tab" type="button" data-tab="notifications">Benachrichtigungen</button>
<button class="tab" type="button" data-tab="install">Installation</button>
<button class="tab" type="button" data-tab="ignored">Ausgeblendet</button>
</nav>
<section class="tab-panel active" data-panel="general">
<label class="toggle"> <label class="toggle">
<input id="checkAur" type="checkbox"> <input id="checkAur" type="checkbox">
<span>AUR Updates pruefen</span> <span>AUR Updates pruefen</span>
</label> </label>
<label class="toggle"> <label class="toggle">
<input id="showIgnored" type="checkbox"> <input id="showIgnored" type="checkbox">
<span>Ausgeblendete anzeigen</span> <span>Ausgeblendete Updates in der App anzeigen</span>
</label> </label>
<label>
<span>Erinnerung alle</span>
<input id="reminderHours" type="number" min="1" step="1">
<small>Stunden</small>
</label>
<label> <label>
<span>Auto-Refresh</span> <span>Auto-Refresh</span>
<input id="autoRefreshMinutes" type="number" min="0" step="1"> <input id="autoRefreshMinutes" type="number" min="0" step="1">
<small>Minuten, 0 deaktiviert</small> <small>Minuten, 0 deaktiviert</small>
</label> </label>
</section>
<section class="tab-panel" data-panel="notifications">
<label class="toggle">
<input id="notificationsEnabled" type="checkbox">
<span>Woechentliche Erinnerungen senden</span>
</label>
<label>
<span>Erinnerung alle</span>
<input id="reminderHours" type="number" min="1" step="1">
<small>Stunden</small>
</label>
</section>
<section class="tab-panel" data-panel="install">
<label> <label>
<span>Terminal</span> <span>Terminal</span>
<select id="terminal"> <select id="terminal">
@@ -156,13 +178,26 @@ const indexHTML = `<!doctype html>
<option value="xterm">xterm</option> <option value="xterm">xterm</option>
</select> </select>
</label> </label>
<label class="toggle">
<input id="keepTerminalOpen" type="checkbox">
<span>Terminal nach Installationen offen lassen</span>
</label>
<label class="toggle">
<input id="confirmSelectedInstalls" type="checkbox">
<span>Selektive Installationen bestaetigen</span>
</label>
</section>
<section class="tab-panel" data-panel="ignored">
<div id="ignoredSettingsList" class="ignored-settings-list"></div>
</section>
<footer class="modal-actions">
<span id="saveState"></span>
<button type="submit">Speichern</button> <button type="submit">Speichern</button>
</footer>
</form> </form>
</section> </dialog>
</aside>
</section>
</main>
<script src="/app.js"></script> <script src="/app.js"></script>
</body> </body>
</html>` </html>`
@@ -498,6 +533,105 @@ form {
padding: 16px; padding: 16px;
} }
dialog {
width: min(760px, calc(100vw - 28px));
max-height: min(760px, calc(100vh - 28px));
border: 1px solid var(--line);
border-radius: 8px;
padding: 0;
color: var(--text);
background: var(--panel);
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.45);
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.62);
}
.settings-modal {
padding: 0;
}
.modal-head,
.modal-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
}
.modal-head {
border-bottom: 1px solid var(--line);
}
.modal-head p {
margin-top: 4px;
color: var(--muted);
}
.icon-btn {
min-height: 36px;
}
.tabs {
display: flex;
gap: 8px;
padding: 12px 16px 0;
border-bottom: 1px solid var(--line);
}
.tab {
min-height: 38px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
color: var(--muted);
background: transparent;
}
.tab.active {
border-color: var(--line);
border-bottom-color: var(--panel);
color: var(--text);
background: var(--panel-soft);
}
.tab-panel {
display: none;
gap: 16px;
padding: 16px;
}
.tab-panel.active {
display: grid;
}
.modal-actions {
border-top: 1px solid var(--line);
}
.ignored-settings-list {
display: grid;
gap: 10px;
}
.ignored-setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 42px;
padding: 0 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-soft);
}
.empty-inline {
margin: 0;
color: var(--muted);
}
label { label {
display: grid; display: grid;
gap: 8px; gap: 8px;
@@ -539,6 +673,10 @@ input[type="checkbox"] {
display: grid; display: grid;
} }
.tabs {
flex-wrap: wrap;
}
#searchInput { #searchInput {
width: 100%; width: 100%;
} }
@@ -557,6 +695,9 @@ const ignoredCount = document.querySelector("#ignoredCount");
const diskFree = document.querySelector("#diskFree"); const diskFree = document.querySelector("#diskFree");
const kernel = document.querySelector("#kernel"); const kernel = document.querySelector("#kernel");
const refreshBtn = document.querySelector("#refreshBtn"); const refreshBtn = document.querySelector("#refreshBtn");
const settingsBtn = document.querySelector("#settingsBtn");
const settingsDialog = document.querySelector("#settingsDialog");
const closeSettingsBtn = document.querySelector("#closeSettingsBtn");
const installBtn = document.querySelector("#installBtn"); const installBtn = document.querySelector("#installBtn");
const installSelectedBtn = document.querySelector("#installSelectedBtn"); const installSelectedBtn = document.querySelector("#installSelectedBtn");
const selectAll = document.querySelector("#selectAll"); const selectAll = document.querySelector("#selectAll");
@@ -578,10 +719,16 @@ const lockState = document.querySelector("#lockState");
const form = document.querySelector("#settingsForm"); const form = document.querySelector("#settingsForm");
const checkAur = document.querySelector("#checkAur"); const checkAur = document.querySelector("#checkAur");
const showIgnored = document.querySelector("#showIgnored"); const showIgnored = document.querySelector("#showIgnored");
const notificationsEnabled = document.querySelector("#notificationsEnabled");
const keepTerminalOpen = document.querySelector("#keepTerminalOpen");
const confirmSelectedInstalls = document.querySelector("#confirmSelectedInstalls");
const reminderHours = document.querySelector("#reminderHours"); const reminderHours = document.querySelector("#reminderHours");
const autoRefreshMinutes = document.querySelector("#autoRefreshMinutes"); const autoRefreshMinutes = document.querySelector("#autoRefreshMinutes");
const terminal = document.querySelector("#terminal"); const terminal = document.querySelector("#terminal");
const saveState = document.querySelector("#saveState"); const saveState = document.querySelector("#saveState");
const ignoredSettingsList = document.querySelector("#ignoredSettingsList");
const tabs = document.querySelectorAll(".tab");
const tabPanels = document.querySelectorAll(".tab-panel");
let currentData = null; let currentData = null;
let refreshTimer = null; let refreshTimer = null;
@@ -718,6 +865,9 @@ function render(data) {
checkAur.checked = data.settings.check_aur; checkAur.checked = data.settings.check_aur;
showIgnored.checked = data.settings.show_ignored; showIgnored.checked = data.settings.show_ignored;
notificationsEnabled.checked = data.settings.notifications_enabled;
keepTerminalOpen.checked = data.settings.keep_terminal_open;
confirmSelectedInstalls.checked = data.settings.confirm_selected_installs;
reminderHours.value = data.settings.reminder_interval_hours; reminderHours.value = data.settings.reminder_interval_hours;
autoRefreshMinutes.value = data.settings.auto_refresh_minutes; autoRefreshMinutes.value = data.settings.auto_refresh_minutes;
terminal.value = data.settings.terminal || "auto"; terminal.value = data.settings.terminal || "auto";
@@ -727,9 +877,36 @@ function render(data) {
renderTable(); renderTable();
renderIgnored(); renderIgnored();
renderIgnoredSettings();
scheduleAutoRefresh(); scheduleAutoRefresh();
} }
function renderIgnoredSettings() {
ignoredSettingsList.replaceChildren();
const names = currentData?.settings.ignored_packages || [];
if (names.length === 0) {
const empty = document.createElement("p");
empty.className = "empty-inline";
empty.textContent = "Keine ausgeblendeten Pakete.";
ignoredSettingsList.append(empty);
return;
}
for (const name of names) {
const row = document.createElement("div");
row.className = "ignored-setting-row";
const label = document.createElement("span");
label.textContent = name;
const button = document.createElement("button");
button.type = "button";
button.className = "ghost";
button.textContent = "Einblenden";
button.addEventListener("click", () => setIgnored(name, false));
row.append(label, button);
ignoredSettingsList.append(row);
}
}
function updateSelectionUi(packages = visiblePackages()) { function updateSelectionUi(packages = visiblePackages()) {
const selectedCount = selectedPackages.size; const selectedCount = selectedPackages.size;
selectionText.textContent = selectedCount === 1 ? "1 Update ausgewaehlt" : selectedCount + " Updates ausgewaehlt"; selectionText.textContent = selectedCount === 1 ? "1 Update ausgewaehlt" : selectedCount + " Updates ausgewaehlt";
@@ -768,6 +945,8 @@ async function setIgnored(name, ignored) {
} }
refreshBtn.addEventListener("click", loadStatus); refreshBtn.addEventListener("click", loadStatus);
settingsBtn.addEventListener("click", () => settingsDialog.showModal());
closeSettingsBtn.addEventListener("click", () => settingsDialog.close());
searchInput.addEventListener("input", renderTable); searchInput.addEventListener("input", renderTable);
sourceFilter.addEventListener("change", renderTable); sourceFilter.addEventListener("change", renderTable);
selectAll.addEventListener("change", () => { selectAll.addEventListener("change", () => {
@@ -799,10 +978,12 @@ installSelectedBtn.addEventListener("click", async () => {
if (packages.length === 0) { if (packages.length === 0) {
return; return;
} }
if (currentData?.settings.confirm_selected_installs) {
const ok = window.confirm("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer."); const ok = window.confirm("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.");
if (!ok) { if (!ok) {
return; return;
} }
}
installSelectedBtn.disabled = true; installSelectedBtn.disabled = true;
try { try {
@@ -827,6 +1008,9 @@ form.addEventListener("submit", async (event) => {
body: JSON.stringify({ body: JSON.stringify({
check_aur: checkAur.checked, check_aur: checkAur.checked,
show_ignored: showIgnored.checked, show_ignored: showIgnored.checked,
notifications_enabled: notificationsEnabled.checked,
keep_terminal_open: keepTerminalOpen.checked,
confirm_selected_installs: confirmSelectedInstalls.checked,
reminder_interval_hours: Number(reminderHours.value), reminder_interval_hours: Number(reminderHours.value),
auto_refresh_minutes: Number(autoRefreshMinutes.value), auto_refresh_minutes: Number(autoRefreshMinutes.value),
terminal: terminal.value, terminal: terminal.value,
@@ -840,5 +1024,17 @@ form.addEventListener("submit", async (event) => {
} }
}); });
for (const tab of tabs) {
tab.addEventListener("click", () => {
const target = tab.dataset.tab;
for (const item of tabs) {
item.classList.toggle("active", item === tab);
}
for (const panel of tabPanels) {
panel.classList.toggle("active", panel.dataset.panel === target);
}
});
}
loadStatus(); loadStatus();
` `

View File

@@ -312,20 +312,24 @@ func kernelVersion() string {
} }
func terminalCommand(cfg config.Config, packages []string) (string, []string, error) { func terminalCommand(cfg config.Config, packages []string) (string, []string, error) {
updateCommand := "lazy-update-manager update; printf '\\nDone. Press enter to close... '; read _" hold := ""
if cfg.KeepTerminalOpen {
hold = "; printf '\\nDone. Press enter to close... '; read _"
}
updateCommand := "lazy-update-manager update" + hold
if len(packages) > 0 { if len(packages) > 0 {
selected := shellPackageList(packages) selected := shellPackageList(packages)
if selected == "" { if selected == "" {
return "", nil, errors.New("no valid packages selected") return "", nil, errors.New("no valid packages selected")
} }
updateCommand = "sudo pacman -S --needed " + selected + "; printf '\\nDone. Press enter to close... '; read _" updateCommand = "sudo pacman -S --needed " + selected + hold
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR { if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
updateCommand = helper + " -S --needed " + selected + "; printf '\\nDone. Press enter to close... '; read _" updateCommand = helper + " -S --needed " + selected + hold
} }
} }
if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR { if helper := updater.AURHelper(); helper != "" && cfg.CheckAUR {
if len(packages) == 0 { if len(packages) == 0 {
updateCommand = helper + " -Syu; printf '\\nDone. Press enter to close... '; read _" updateCommand = helper + " -Syu" + hold
} }
} }