feat: add selected update installs and polished UI
This commit is contained in:
@@ -17,7 +17,8 @@ const indexHTML = `<!doctype html>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="refreshBtn" type="button" title="Updates pruefen">Refresh</button>
|
||||
<button id="installBtn" type="button">Installieren</button>
|
||||
<button id="installSelectedBtn" type="button" disabled>Auswahl installieren</button>
|
||||
<button id="installBtn" type="button">Alle installieren</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -57,11 +58,19 @@ const indexHTML = `<!doctype html>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="selection-bar">
|
||||
<label class="select-all">
|
||||
<input id="selectAll" type="checkbox">
|
||||
<span id="selectionText">0 Updates ausgewaehlt</span>
|
||||
</label>
|
||||
<span>Selektive Updates koennen unter Arch riskant sein. Vollupdate bleibt empfohlen.</span>
|
||||
</div>
|
||||
<div id="warnings" class="warnings" hidden></div>
|
||||
<div id="emptyState" class="empty">Keine passenden Updates gefunden.</div>
|
||||
<table id="updatesTable" hidden>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Quelle</th>
|
||||
<th>Paket</th>
|
||||
<th>Aktuell</th>
|
||||
@@ -161,14 +170,16 @@ const indexHTML = `<!doctype html>
|
||||
const appCSS = `
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #111317;
|
||||
--panel: #191d23;
|
||||
--panel-soft: #222832;
|
||||
--bg: #0f1215;
|
||||
--panel: #191e25;
|
||||
--panel-soft: #232a34;
|
||||
--panel-lift: #202732;
|
||||
--text: #f4f7fb;
|
||||
--muted: #9ba7b6;
|
||||
--line: #303844;
|
||||
--accent: #44d19d;
|
||||
--accent-strong: #2eb984;
|
||||
--blue: #6ea8fe;
|
||||
--warn: #f4c95d;
|
||||
--danger: #ff6f61;
|
||||
}
|
||||
@@ -182,7 +193,9 @@ body {
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(68, 209, 157, 0.14), transparent 34rem),
|
||||
linear-gradient(180deg, #13171d 0%, var(--bg) 22rem);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
@@ -249,10 +262,12 @@ button {
|
||||
color: var(--text);
|
||||
background: var(--panel-soft);
|
||||
cursor: pointer;
|
||||
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@@ -273,6 +288,17 @@ form button:hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
#installSelectedBtn {
|
||||
border-color: rgba(110, 168, 254, 0.42);
|
||||
color: #dcebff;
|
||||
background: rgba(110, 168, 254, 0.12);
|
||||
font-weight: 720;
|
||||
}
|
||||
|
||||
#installSelectedBtn:hover {
|
||||
border-color: var(--blue);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
@@ -285,6 +311,7 @@ form button:hover {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.stat {
|
||||
@@ -292,6 +319,7 @@ form button:hover {
|
||||
gap: 6px;
|
||||
min-height: 82px;
|
||||
padding: 14px;
|
||||
background: linear-gradient(180deg, var(--panel-lift), var(--panel));
|
||||
}
|
||||
|
||||
.stat span,
|
||||
@@ -345,6 +373,27 @@ select {
|
||||
width: min(32vw, 320px);
|
||||
}
|
||||
|
||||
.selection-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 46px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
color: var(--warn);
|
||||
background: rgba(244, 201, 93, 0.07);
|
||||
}
|
||||
|
||||
.select-all {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.warnings {
|
||||
margin: 14px 16px 0;
|
||||
padding: 12px;
|
||||
@@ -384,7 +433,12 @@ td {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
.updates-panel td:first-child {
|
||||
width: 46px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.updates-panel td:nth-child(2) {
|
||||
width: 92px;
|
||||
color: var(--accent);
|
||||
font-weight: 720;
|
||||
@@ -399,6 +453,10 @@ tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
tr.selected td {
|
||||
background: rgba(110, 168, 254, 0.08);
|
||||
}
|
||||
|
||||
.ghost {
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
@@ -477,12 +535,16 @@ input[type="checkbox"] {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.selection-bar {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
#searchInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th:nth-child(3),
|
||||
td:nth-child(3) {
|
||||
th:nth-child(4),
|
||||
td:nth-child(4) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -496,6 +558,9 @@ const diskFree = document.querySelector("#diskFree");
|
||||
const kernel = document.querySelector("#kernel");
|
||||
const refreshBtn = document.querySelector("#refreshBtn");
|
||||
const installBtn = document.querySelector("#installBtn");
|
||||
const installSelectedBtn = document.querySelector("#installSelectedBtn");
|
||||
const selectAll = document.querySelector("#selectAll");
|
||||
const selectionText = document.querySelector("#selectionText");
|
||||
const updatesTable = document.querySelector("#updatesTable");
|
||||
const updatesBody = document.querySelector("#updatesBody");
|
||||
const ignoredPanel = document.querySelector("#ignoredPanel");
|
||||
@@ -520,6 +585,7 @@ const saveState = document.querySelector("#saveState");
|
||||
|
||||
let currentData = null;
|
||||
let refreshTimer = null;
|
||||
let selectedPackages = new Set();
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
@@ -568,6 +634,22 @@ function renderTable() {
|
||||
updatesBody.replaceChildren();
|
||||
for (const pkg of packages) {
|
||||
const row = document.createElement("tr");
|
||||
row.classList.toggle("selected", selectedPackages.has(pkg.Name));
|
||||
|
||||
const selectCell = document.createElement("td");
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = selectedPackages.has(pkg.Name);
|
||||
checkbox.addEventListener("change", () => {
|
||||
if (checkbox.checked) {
|
||||
selectedPackages.add(pkg.Name);
|
||||
} else {
|
||||
selectedPackages.delete(pkg.Name);
|
||||
}
|
||||
renderTable();
|
||||
});
|
||||
selectCell.append(checkbox);
|
||||
row.append(selectCell);
|
||||
row.append(cell(pkg.Source));
|
||||
row.append(cell(pkg.Name));
|
||||
row.append(cell(pkg.Current || "-"));
|
||||
@@ -586,6 +668,7 @@ function renderTable() {
|
||||
|
||||
updatesTable.hidden = packages.length === 0;
|
||||
emptyState.hidden = packages.length !== 0;
|
||||
updateSelectionUi(packages);
|
||||
}
|
||||
|
||||
function renderIgnored() {
|
||||
@@ -618,6 +701,8 @@ function cell(value) {
|
||||
|
||||
function render(data) {
|
||||
currentData = data;
|
||||
const availableNames = new Set(data.packages.filter((pkg) => !pkg.Ignored).map((pkg) => pkg.Name));
|
||||
selectedPackages = new Set([...selectedPackages].filter((name) => availableNames.has(name)));
|
||||
summary.textContent = data.summary;
|
||||
activeCount.textContent = data.total;
|
||||
ignoredCount.textContent = data.ignored_total;
|
||||
@@ -645,6 +730,14 @@ function render(data) {
|
||||
scheduleAutoRefresh();
|
||||
}
|
||||
|
||||
function updateSelectionUi(packages = visiblePackages()) {
|
||||
const selectedCount = selectedPackages.size;
|
||||
selectionText.textContent = selectedCount === 1 ? "1 Update ausgewaehlt" : selectedCount + " Updates ausgewaehlt";
|
||||
installSelectedBtn.disabled = selectedCount === 0 || currentData?.system.pacman_locked;
|
||||
selectAll.checked = packages.length > 0 && packages.every((pkg) => selectedPackages.has(pkg.Name));
|
||||
selectAll.indeterminate = packages.some((pkg) => selectedPackages.has(pkg.Name)) && !selectAll.checked;
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
refreshBtn.disabled = true;
|
||||
summary.textContent = "Pruefe Updates...";
|
||||
@@ -677,11 +770,22 @@ async function setIgnored(name, ignored) {
|
||||
refreshBtn.addEventListener("click", loadStatus);
|
||||
searchInput.addEventListener("input", renderTable);
|
||||
sourceFilter.addEventListener("change", renderTable);
|
||||
selectAll.addEventListener("change", () => {
|
||||
const packages = visiblePackages();
|
||||
for (const pkg of packages) {
|
||||
if (selectAll.checked) {
|
||||
selectedPackages.add(pkg.Name);
|
||||
} else {
|
||||
selectedPackages.delete(pkg.Name);
|
||||
}
|
||||
}
|
||||
renderTable();
|
||||
});
|
||||
|
||||
installBtn.addEventListener("click", async () => {
|
||||
installBtn.disabled = true;
|
||||
try {
|
||||
await request("/api/install", { method: "POST" });
|
||||
await request("/api/install", { method: "POST", body: "{}" });
|
||||
summary.textContent = "Installation im Terminal gestartet.";
|
||||
} catch (error) {
|
||||
summary.textContent = error.message;
|
||||
@@ -690,6 +794,30 @@ installBtn.addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
installSelectedBtn.addEventListener("click", async () => {
|
||||
const packages = [...selectedPackages];
|
||||
if (packages.length === 0) {
|
||||
return;
|
||||
}
|
||||
const ok = window.confirm("Ausgewaehlte Pakete gezielt installieren? Unter Arch ist ein vollstaendiges Systemupdate meistens sicherer.");
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
installSelectedBtn.disabled = true;
|
||||
try {
|
||||
await request("/api/install", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ packages }),
|
||||
});
|
||||
summary.textContent = "Ausgewaehlte Installation im Terminal gestartet.";
|
||||
} catch (error) {
|
||||
summary.textContent = error.message;
|
||||
} finally {
|
||||
installSelectedBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
saveState.textContent = "Speichere...";
|
||||
|
||||
Reference in New Issue
Block a user