feat: add tabbed settings dialog
This commit is contained in:
@@ -58,6 +58,8 @@ The GUI opens in your browser and lets you:
|
||||
- change the reminder interval
|
||||
- configure automatic refresh
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -134,6 +134,9 @@ func notifyUpdates(args []string) int {
|
||||
if result.Total() == 0 {
|
||||
return 0
|
||||
}
|
||||
if !cfg.NotificationsEnabled {
|
||||
return 0
|
||||
}
|
||||
|
||||
store, err := state.Load(defaultStatePath())
|
||||
if err != nil {
|
||||
|
||||
@@ -8,22 +8,28 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
CheckAUR bool `json:"check_aur"`
|
||||
ReminderIntervalHours int `json:"reminder_interval_hours"`
|
||||
Terminal string `json:"terminal"`
|
||||
AutoRefreshMinutes int `json:"auto_refresh_minutes"`
|
||||
ShowIgnored bool `json:"show_ignored"`
|
||||
IgnoredPackages []string `json:"ignored_packages"`
|
||||
CheckAUR bool `json:"check_aur"`
|
||||
ReminderIntervalHours int `json:"reminder_interval_hours"`
|
||||
Terminal string `json:"terminal"`
|
||||
AutoRefreshMinutes int `json:"auto_refresh_minutes"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func Default() Config {
|
||||
return Config{
|
||||
CheckAUR: true,
|
||||
ReminderIntervalHours: 168,
|
||||
Terminal: "auto",
|
||||
AutoRefreshMinutes: 30,
|
||||
ShowIgnored: false,
|
||||
IgnoredPackages: []string{},
|
||||
CheckAUR: true,
|
||||
ReminderIntervalHours: 168,
|
||||
Terminal: "auto",
|
||||
AutoRefreshMinutes: 30,
|
||||
ShowIgnored: false,
|
||||
NotificationsEnabled: true,
|
||||
KeepTerminalOpen: true,
|
||||
ConfirmSelectedInstalls: true,
|
||||
IgnoredPackages: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +43,25 @@ func Load(path string) (Config, error) {
|
||||
if err != nil {
|
||||
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 {
|
||||
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)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const indexHTML = `<!doctype html>
|
||||
<p id="summary">Pruefe Updates...</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="settingsBtn" type="button">Einstellungen</button>
|
||||
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
|
||||
<button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button>
|
||||
<button id="installBtn" type="button">Alle installieren</button>
|
||||
@@ -114,55 +115,89 @@ const indexHTML = `<!doctype html>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="panel settings-panel">
|
||||
<div class="panel-head">
|
||||
<h2>Einstellungen</h2>
|
||||
<span id="saveState"></span>
|
||||
</div>
|
||||
<form id="settingsForm">
|
||||
<label class="toggle">
|
||||
<input id="checkAur" type="checkbox">
|
||||
<span>AUR Updates pruefen</span>
|
||||
</label>
|
||||
|
||||
<label class="toggle">
|
||||
<input id="showIgnored" type="checkbox">
|
||||
<span>Ausgeblendete anzeigen</span>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Erinnerung alle</span>
|
||||
<input id="reminderHours" type="number" min="1" step="1">
|
||||
<small>Stunden</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Auto-Refresh</span>
|
||||
<input id="autoRefreshMinutes" type="number" min="0" step="1">
|
||||
<small>Minuten, 0 deaktiviert</small>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Terminal</span>
|
||||
<select id="terminal">
|
||||
<option value="auto">Automatisch</option>
|
||||
<option value="foot">foot</option>
|
||||
<option value="kitty">kitty</option>
|
||||
<option value="alacritty">alacritty</option>
|
||||
<option value="wezterm">wezterm</option>
|
||||
<option value="ghostty">ghostty</option>
|
||||
<option value="konsole">konsole</option>
|
||||
<option value="xterm">xterm</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
</section>
|
||||
</aside>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog id="settingsDialog">
|
||||
<form id="settingsForm" method="dialog" class="settings-modal">
|
||||
<header class="modal-head">
|
||||
<div>
|
||||
<h2>Einstellungen</h2>
|
||||
<p>Update-Verhalten, Installation und ausgeblendete Pakete</p>
|
||||
</div>
|
||||
<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">
|
||||
<input id="checkAur" type="checkbox">
|
||||
<span>AUR Updates pruefen</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input id="showIgnored" type="checkbox">
|
||||
<span>Ausgeblendete Updates in der App anzeigen</span>
|
||||
</label>
|
||||
<label>
|
||||
<span>Auto-Refresh</span>
|
||||
<input id="autoRefreshMinutes" type="number" min="0" step="1">
|
||||
<small>Minuten, 0 deaktiviert</small>
|
||||
</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>
|
||||
<span>Terminal</span>
|
||||
<select id="terminal">
|
||||
<option value="auto">Automatisch</option>
|
||||
<option value="foot">foot</option>
|
||||
<option value="kitty">kitty</option>
|
||||
<option value="alacritty">alacritty</option>
|
||||
<option value="wezterm">wezterm</option>
|
||||
<option value="ghostty">ghostty</option>
|
||||
<option value="konsole">konsole</option>
|
||||
<option value="xterm">xterm</option>
|
||||
</select>
|
||||
</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>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -498,6 +533,105 @@ form {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -539,6 +673,10 @@ input[type="checkbox"] {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#searchInput {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -557,6 +695,9 @@ const ignoredCount = document.querySelector("#ignoredCount");
|
||||
const diskFree = document.querySelector("#diskFree");
|
||||
const kernel = document.querySelector("#kernel");
|
||||
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 installSelectedBtn = document.querySelector("#installSelectedBtn");
|
||||
const selectAll = document.querySelector("#selectAll");
|
||||
@@ -578,10 +719,16 @@ const lockState = document.querySelector("#lockState");
|
||||
const form = document.querySelector("#settingsForm");
|
||||
const checkAur = document.querySelector("#checkAur");
|
||||
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 autoRefreshMinutes = document.querySelector("#autoRefreshMinutes");
|
||||
const terminal = document.querySelector("#terminal");
|
||||
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 refreshTimer = null;
|
||||
@@ -718,6 +865,9 @@ function render(data) {
|
||||
|
||||
checkAur.checked = data.settings.check_aur;
|
||||
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;
|
||||
autoRefreshMinutes.value = data.settings.auto_refresh_minutes;
|
||||
terminal.value = data.settings.terminal || "auto";
|
||||
@@ -727,9 +877,36 @@ function render(data) {
|
||||
|
||||
renderTable();
|
||||
renderIgnored();
|
||||
renderIgnoredSettings();
|
||||
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()) {
|
||||
const selectedCount = selectedPackages.size;
|
||||
selectionText.textContent = selectedCount === 1 ? "1 Update ausgewaehlt" : selectedCount + " Updates ausgewaehlt";
|
||||
@@ -768,6 +945,8 @@ async function setIgnored(name, ignored) {
|
||||
}
|
||||
|
||||
refreshBtn.addEventListener("click", loadStatus);
|
||||
settingsBtn.addEventListener("click", () => settingsDialog.showModal());
|
||||
closeSettingsBtn.addEventListener("click", () => settingsDialog.close());
|
||||
searchInput.addEventListener("input", renderTable);
|
||||
sourceFilter.addEventListener("change", renderTable);
|
||||
selectAll.addEventListener("change", () => {
|
||||
@@ -799,9 +978,11 @@ installSelectedBtn.addEventListener("click", async () => {
|
||||
if (packages.length === 0) {
|
||||
return;
|
||||
}
|
||||
const ok = window.confirm("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.");
|
||||
if (!ok) {
|
||||
return;
|
||||
if (currentData?.settings.confirm_selected_installs) {
|
||||
const ok = window.confirm("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
installSelectedBtn.disabled = true;
|
||||
@@ -827,6 +1008,9 @@ form.addEventListener("submit", async (event) => {
|
||||
body: JSON.stringify({
|
||||
check_aur: checkAur.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),
|
||||
auto_refresh_minutes: Number(autoRefreshMinutes.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();
|
||||
`
|
||||
|
||||
@@ -312,20 +312,24 @@ func kernelVersion() string {
|
||||
}
|
||||
|
||||
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 {
|
||||
selected := shellPackageList(packages)
|
||||
if 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 {
|
||||
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 len(packages) == 0 {
|
||||
updateCommand = helper + " -Syu; printf '\\nDone. Press enter to close... '; read _"
|
||||
updateCommand = helper + " -Syu" + hold
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user