mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 06:00:21 +02:00
Persist resume state every piece; replace conn field with a stepper
Resume: write the sidecar after every completed piece instead of batching every 8. Each piece's bytes are already fdatasync'd and the sidecar write is a tiny atomic tmp+rename, so an unclean kill now re-downloads only the pieces still in flight, not a batch of up-to-8 already-finished ones. Drops the now -unused 'unpersisted' counter. UI: the "parallel download connections" control is now a stepper — two large 54px -/+ buttons around a tabular value, in a row beside its label, instead of a full-width number field for a 1-16 value. Big targets and a clear green focus ring suit controller navigation (the UI is driven by the PS5 pad via the home tile). Tapping -/+ updates and auto-saves that field alone (debounced), applying live on the server.
This commit is contained in:
1 parent
6024dbfc5d
commit
986ff00c36
4 files changed
+88
-13
No files matched your search
@@ -92,7 +92,6 @@ typedef struct dl_job {
|
|||||||
int pieces_done;
|
int pieces_done;
|
||||||
int pieces_failed;
|
int pieces_failed;
|
||||||
int inflight;
|
int inflight;
|
||||||
int unpersisted; /* DONE pieces since last sidecar flush */
|
|
||||||
int fd; /* O_RDWR dest fd, -1 until admit */
|
int fd; /* O_RDWR dest fd, -1 until admit */
|
||||||
int rc; /* 0 ok, -1 net/io, -2 verify */
|
int rc; /* 0 ok, -1 net/io, -2 verify */
|
||||||
struct dl_job *next;
|
struct dl_job *next;
|
||||||
@@ -1225,7 +1224,7 @@ admit_next(void) {
|
|||||||
patchdl_manifest_free(&q->mf);
|
patchdl_manifest_free(&q->mf);
|
||||||
free(q->ps); q->ps = NULL;
|
free(q->ps); q->ps = NULL;
|
||||||
free(q->bitmap); q->bitmap = NULL;
|
free(q->bitmap); q->bitmap = NULL;
|
||||||
q->done_bytes = 0; q->pieces_failed = 0; q->unpersisted = 0; q->rc = 0;
|
q->done_bytes = 0; q->pieces_failed = 0; q->rc = 0;
|
||||||
|
|
||||||
q->mf = mf; q->ps = ps; q->bitmap = bitmap; q->fd = fd; q->total = total;
|
q->mf = mf; q->ps = ps; q->bitmap = bitmap; q->fd = fd; q->total = total;
|
||||||
for (int i = 0; i < mf.count; i++) q->ps[i].slot = -1;
|
for (int i = 0; i < mf.count; i++) q->ps[i].slot = -1;
|
||||||
@@ -1351,7 +1350,11 @@ dl_worker(void *arg) {
|
|||||||
job->pieces_done++;
|
job->pieces_done++;
|
||||||
job->done_bytes += sz;
|
job->done_bytes += sz;
|
||||||
job->bitmap[pidx / 8] |= (unsigned char)(1 << (pidx % 8));
|
job->bitmap[pidx / 8] |= (unsigned char)(1 << (pidx % 8));
|
||||||
if (++job->unpersisted >= 8) { write_job_state(job); job->unpersisted = 0; }
|
/* Persist after every completed piece: the bytes were already
|
||||||
|
fdatasync'd, and a tiny atomic sidecar write means an unclean
|
||||||
|
kill re-downloads only the pieces still in flight, not a batch
|
||||||
|
of up-to-8 already-finished ones. */
|
||||||
|
write_job_state(job);
|
||||||
} else if (job->abort &&
|
} else if (job->abort &&
|
||||||
(job->state == JOB_PAUSING || job->state == JOB_CANCELLING)) {
|
(job->state == JOB_PAUSING || job->state == JOB_CANCELLING)) {
|
||||||
job->ps[pidx].state = PC_PENDING; /* aborted by pause -> redo on resume */
|
job->ps[pidx].state = PC_PENDING; /* aborted by pause -> redo on resume */
|
||||||
|
|||||||
+43
-5
@@ -117,7 +117,9 @@ function bindElements() {
|
|||||||
deleteAfterInstall: document.getElementById("deleteAfterInstall"),
|
deleteAfterInstall: document.getElementById("deleteAfterInstall"),
|
||||||
verifyDownloads: document.getElementById("verifyDownloads"),
|
verifyDownloads: document.getElementById("verifyDownloads"),
|
||||||
homeShortcut: document.getElementById("homeShortcut"),
|
homeShortcut: document.getElementById("homeShortcut"),
|
||||||
maxConnections: document.getElementById("maxConnections"),
|
connValue: document.getElementById("connValue"),
|
||||||
|
connMinus: document.getElementById("connMinus"),
|
||||||
|
connPlus: document.getElementById("connPlus"),
|
||||||
refreshBtn: document.getElementById("refreshBtn"),
|
refreshBtn: document.getElementById("refreshBtn"),
|
||||||
saveBtn: document.getElementById("saveBtn"),
|
saveBtn: document.getElementById("saveBtn"),
|
||||||
clearLogBtn: document.getElementById("clearLogBtn"),
|
clearLogBtn: document.getElementById("clearLogBtn"),
|
||||||
@@ -150,6 +152,10 @@ function bindEvents() {
|
|||||||
els.refreshBtn.addEventListener("click", loadInitialData);
|
els.refreshBtn.addEventListener("click", loadInitialData);
|
||||||
els.saveBtn.addEventListener("click", saveConfig);
|
els.saveBtn.addEventListener("click", saveConfig);
|
||||||
els.clearLogBtn.addEventListener("click", () => { state.logs = []; renderLogs(); });
|
els.clearLogBtn.addEventListener("click", () => { state.logs = []; renderLogs(); });
|
||||||
|
if (els.connMinus)
|
||||||
|
els.connMinus.addEventListener("click", () => setConnections(clampConn(state.config.max_connections) - 1));
|
||||||
|
if (els.connPlus)
|
||||||
|
els.connPlus.addEventListener("click", () => setConnections(clampConn(state.config.max_connections) + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setView(view) {
|
function setView(view) {
|
||||||
@@ -228,6 +234,40 @@ function renderStatus() {
|
|||||||
if (els.railSpace) els.railSpace.textContent = `${space} free`;
|
if (els.railSpace) els.railSpace.textContent = `${space} free`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONN_MIN = 1, CONN_MAX = 16;
|
||||||
|
function clampConn(n) {
|
||||||
|
n = parseInt(n, 10);
|
||||||
|
if (!Number.isFinite(n)) n = 4;
|
||||||
|
return Math.max(CONN_MIN, Math.min(CONN_MAX, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConnStepper() {
|
||||||
|
const n = clampConn(state.config.max_connections);
|
||||||
|
if (els.connValue) els.connValue.textContent = String(n);
|
||||||
|
if (els.connMinus) els.connMinus.disabled = n <= CONN_MIN;
|
||||||
|
if (els.connPlus) els.connPlus.disabled = n >= CONN_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
let connSaveTimer = null;
|
||||||
|
// Stepper +/-: update + re-render immediately, then persist just this field
|
||||||
|
// (debounced) so rapid taps collapse into one POST and other unsaved form
|
||||||
|
// fields stay untouched. The server applies the new count live (no restart).
|
||||||
|
function setConnections(n) {
|
||||||
|
const v = clampConn(n);
|
||||||
|
if (v === clampConn(state.config.max_connections)) { renderConnStepper(); return; }
|
||||||
|
state.config.max_connections = v;
|
||||||
|
renderConnStepper();
|
||||||
|
clearTimeout(connSaveTimer);
|
||||||
|
connSaveTimer = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await postJson(API.config, { max_connections: v });
|
||||||
|
showToast(`Parallel connections: ${v}`);
|
||||||
|
} catch (e) {
|
||||||
|
showToast("Could not save connections — API not reachable.");
|
||||||
|
}
|
||||||
|
}, 450);
|
||||||
|
}
|
||||||
|
|
||||||
function renderSettings() {
|
function renderSettings() {
|
||||||
els.defaultPolicy.value = state.config.default_policy || "deny";
|
els.defaultPolicy.value = state.config.default_policy || "deny";
|
||||||
els.downloadDir.value = state.config.download_dir || "";
|
els.downloadDir.value = state.config.download_dir || "";
|
||||||
@@ -235,7 +275,7 @@ function renderSettings() {
|
|||||||
els.deleteAfterInstall.checked = Boolean(state.config.delete_pkg_after_install);
|
els.deleteAfterInstall.checked = Boolean(state.config.delete_pkg_after_install);
|
||||||
if (els.verifyDownloads) els.verifyDownloads.checked = Boolean(state.config.verify_downloads);
|
if (els.verifyDownloads) els.verifyDownloads.checked = Boolean(state.config.verify_downloads);
|
||||||
if (els.homeShortcut) els.homeShortcut.checked = state.config.home_shortcut !== false;
|
if (els.homeShortcut) els.homeShortcut.checked = state.config.home_shortcut !== false;
|
||||||
if (els.maxConnections) els.maxConnections.value = state.config.max_connections || 4;
|
renderConnStepper();
|
||||||
els.allowlistHosts.replaceChildren(...(state.config.cdn_allowlist || []).map((host) => {
|
els.allowlistHosts.replaceChildren(...(state.config.cdn_allowlist || []).map((host) => {
|
||||||
const chip = document.createElement("span");
|
const chip = document.createElement("span");
|
||||||
chip.className = "host-chip";
|
chip.className = "host-chip";
|
||||||
@@ -615,9 +655,7 @@ async function saveConfig() {
|
|||||||
delete_pkg_after_install: els.deleteAfterInstall.checked,
|
delete_pkg_after_install: els.deleteAfterInstall.checked,
|
||||||
verify_downloads: els.verifyDownloads ? els.verifyDownloads.checked : Boolean(state.config.verify_downloads),
|
verify_downloads: els.verifyDownloads ? els.verifyDownloads.checked : Boolean(state.config.verify_downloads),
|
||||||
home_shortcut: els.homeShortcut ? els.homeShortcut.checked : state.config.home_shortcut !== false,
|
home_shortcut: els.homeShortcut ? els.homeShortcut.checked : state.config.home_shortcut !== false,
|
||||||
max_connections: els.maxConnections
|
max_connections: clampConn(state.config.max_connections),
|
||||||
? Math.max(1, Math.min(16, parseInt(els.maxConnections.value, 10) || 4))
|
|
||||||
: (state.config.max_connections || 4),
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await postJson(API.config, config);
|
await postJson(API.config, config);
|
||||||
|
|||||||
+11
-5
@@ -186,11 +186,17 @@
|
|||||||
<span class="track"></span>
|
<span class="track"></span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<div class="switch-row">
|
||||||
<span>Parallel download connections</span>
|
<span>
|
||||||
<input id="maxConnections" type="number" min="1" max="16" step="1"
|
<strong>Parallel download connections</strong>
|
||||||
title="Connections used to download a patch in parallel — applies live on Save (raising is instant; lowering settles as in-flight pieces finish)" />
|
<em>1–16 · applies live, no payload restart</em>
|
||||||
</label>
|
</span>
|
||||||
|
<div class="stepper" role="group" aria-label="Parallel download connections">
|
||||||
|
<button type="button" class="stepper-btn" id="connMinus" aria-label="Fewer connections">−</button>
|
||||||
|
<output class="stepper-value" id="connValue" aria-live="polite">4</output>
|
||||||
|
<button type="button" class="stepper-btn" id="connPlus" aria-label="More connections">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="allowlist">
|
<div class="allowlist">
|
||||||
|
|||||||
@@ -395,6 +395,34 @@ h2 { font-size: 18px; line-height: 1.2; }
|
|||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* number stepper — big, controller-friendly targets with a clear focus ring
|
||||||
|
(the UI is operated by the PS5 controller via the home tile). */
|
||||||
|
.stepper { display: inline-flex; align-items: center; gap: 12px; flex: none; }
|
||||||
|
.stepper-btn {
|
||||||
|
width: 54px; height: 54px;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 30px; line-height: 1; font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: background .12s ease, border-color .12s ease, transform .07s ease;
|
||||||
|
}
|
||||||
|
.stepper-btn:hover { background: var(--surface); border-color: var(--muted); }
|
||||||
|
.stepper-btn:active { transform: scale(0.93); background: var(--green-soft); border-color: var(--green); }
|
||||||
|
/* :focus (not only :focus-visible) so the controller's focus is always obvious */
|
||||||
|
.stepper-btn:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px var(--green-soft); }
|
||||||
|
.stepper-btn:disabled { opacity: 0.32; cursor: default; transform: none; }
|
||||||
|
.stepper-value {
|
||||||
|
min-width: 52px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26px; font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
.switch-row > span:first-child strong { display: block; font-size: 14px; }
|
.switch-row > span:first-child strong { display: block; font-size: 14px; }
|
||||||
.switch-row > span:first-child em { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; font-style: normal; }
|
.switch-row > span:first-child em { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; font-style: normal; }
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user