Add free space and queue order, flat UI and wider settings

This commit is contained in:
SirBlob committed 2026-11-01 13:43:44 -05:00
1 parent 6027cfabb9
commit db4e3c9611
3 files changed
+402 -253

No files matched your search

+1
View File
@@ -69,6 +69,7 @@ func staticHandler() http.Handler {
http.NotFound(writer, request)
return
}
writer.Header().Set("Cache-Control", "no-cache")
fileServer.ServeHTTP(writer, request)
})
}
+77 -44
View File
@@ -127,7 +127,7 @@
</main>
</div>
<dialog id="settings-dialog" class="sheet">
<dialog id="settings-dialog" class="sheet sheet-wide">
<form id="settings-form" class="sheet-form">
<header class="sheet-header">
<h2>Settings</h2>
@@ -135,73 +135,106 @@
</header>
<div class="sheet-body">
<fieldset class="settings-group">
<legend>Downloads</legend>
<section class="settings-section">
<div class="settings-intro">
<h3>Downloads</h3>
<p class="muted">How many files run at once and how fast each one downloads.</p>
</div>
<div class="settings-grid">
<label class="field">
<span>Parallel downloads</span>
<input id="setting-concurrent" type="number" inputmode="numeric" min="1" max="10" required>
<small class="field-hint">Downloads running at the same time (1 to 10).</small>
</label>
<label class="field">
<span>Connections per download</span>
<input id="setting-connections" type="number" inputmode="numeric" min="1" max="16" required>
<small class="field-hint">Parts fetched at once when the server allows it (1 to 16).</small>
</label>
</div>
</section>
<section class="settings-section">
<div class="settings-intro">
<h3>Safety</h3>
<p class="muted">Limits that protect the server.</p>
</div>
<div class="settings-grid">
<label class="field">
<span>Keep free on disk (GB)</span>
<input id="setting-min-free" type="number" inputmode="numeric" min="0" required>
<small class="field-hint">Downloads stop before free space drops below this.</small>
</label>
<label class="field">
<span>Allowed ports</span>
<input id="setting-ports" placeholder="80, 443" autocomplete="off">
<small class="field-hint">Comma separated, or * for any port.</small>
</label>
</div>
</section>
<section class="settings-section">
<div class="settings-intro">
<h3>After download</h3>
<p class="muted">Default choices for new downloads and moves.</p>
</div>
<div class="settings-stack">
<label class="checkbox">
<input id="setting-auto-extract" type="checkbox">
<span>Extract archives automatically</span>
</label>
<label class="checkbox">
<input id="setting-cleanup" type="checkbox">
<span>Delete temporary files after a move</span>
</label>
</div>
</section>
<section class="settings-section">
<div class="settings-intro">
<h3>Download paths</h3>
<p class="muted">Folders listed under "Saved paths" when you pick where a download goes.</p>
</div>
<label class="field">
<span>Allowed ports</span>
<input id="setting-ports" placeholder="80, 443 or * for any" autocomplete="off">
<span>Folders, one per line</span>
<textarea id="setting-download-paths" rows="5" placeholder="/media/blob/TeaNet/Plex/Movies/Kids" autocomplete="off" autocapitalize="off" spellcheck="false"></textarea>
<small class="field-hint">Must be inside a destination root. Missing folders are created on first use.</small>
</label>
</fieldset>
</section>
<fieldset class="settings-group">
<legend>Defaults</legend>
<label class="checkbox">
<input id="setting-auto-extract" type="checkbox">
<span>Extract archives automatically</span>
</label>
<label class="checkbox">
<input id="setting-cleanup" type="checkbox">
<span>Delete temporary files after a move</span>
</label>
</fieldset>
<section class="settings-section">
<div class="settings-intro">
<h3>This browser</h3>
<p class="muted">Saved only on this device.</p>
</div>
<div class="settings-stack">
<label class="field settings-narrow">
<span>Theme</span>
<select id="setting-theme">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<label class="checkbox">
<input id="setting-notifications" type="checkbox">
<span>Notify when downloads finish</span>
</label>
</div>
</section>
<fieldset class="settings-group">
<legend>Download paths</legend>
<label class="field">
<span>One folder per line, inside a destination root. Missing folders are created on first use.</span>
<textarea id="setting-download-paths" rows="4" placeholder="/media/blob/TeaNet/Plex/Movies/Kids" autocomplete="off" autocapitalize="off" spellcheck="false"></textarea>
</label>
</fieldset>
<fieldset class="settings-group">
<legend>Appearance</legend>
<label class="field">
<span>Theme (this browser)</span>
<select id="setting-theme">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
<label class="checkbox">
<input id="setting-notifications" type="checkbox">
<span>Notify when downloads finish (this browser)</span>
</label>
</fieldset>
<fieldset class="settings-group">
<legend>Server</legend>
<section class="settings-section">
<div class="settings-intro">
<h3>Server</h3>
<p class="muted">Set in the server environment, read only.</p>
</div>
<dl class="info-list">
<dt>Download folder</dt>
<dd id="info-download-dir" class="mono"></dd>
<dt>Destination roots</dt>
<dd id="info-roots"></dd>
</dl>
</fieldset>
</section>
<p id="settings-error" class="error" role="alert"></p>
</div>
+324 -209
View File
@@ -1,69 +1,76 @@
:root {
color-scheme: light;
--background: #f2f3f6;
--background: #eef0f3;
--surface: #ffffff;
--surface-alt: #f7f8fa;
--surface-hover: #eef0f4;
--border: #dde0e6;
--text: #191c22;
--muted: #687080;
--accent: #3b6ef5;
--accent-hover: #2f5ddb;
--accent-soft: rgba(59, 110, 245, 0.12);
--surface-alt: #f5f6f8;
--surface-hover: #e8ebf0;
--border: #d8dce3;
--text: #15181e;
--muted: #626a77;
--accent: #2f6bed;
--accent-hover: #2459cc;
--accent-soft: #dde7fc;
--accent-text: #ffffff;
--danger: #d03c32;
--danger-soft: rgba(208, 60, 50, 0.1);
--success: #1f9d62;
--success-soft: rgba(31, 157, 98, 0.12);
--info: #3b6ef5;
--info-soft: rgba(59, 110, 245, 0.12);
--shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 2px 8px rgba(16, 24, 40, 0.04);
--radius: 12px;
--radius-small: 8px;
--danger: #c7372c;
--danger-soft: #fae2df;
--success: #17855a;
--success-soft: #d8f1e5;
--info: #2f6bed;
--info-soft: #dde7fc;
--warning: #a86400;
--warning-soft: #fcebc9;
--track: #e1e4ea;
--overlay: rgba(10, 12, 16, 0.6);
--radius: 10px;
--radius-small: 6px;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--background: #0f1115;
--surface: #171a20;
--surface-alt: #1d2128;
--surface-hover: #242933;
--border: #2a2f39;
--text: #e7e9ee;
--muted: #949cab;
--accent: #5b87ff;
--accent-hover: #7298ff;
--accent-soft: rgba(91, 135, 255, 0.16);
--danger: #f06a5f;
--danger-soft: rgba(240, 106, 95, 0.14);
--success: #3fc486;
--success-soft: rgba(63, 196, 134, 0.14);
--info: #5b87ff;
--info-soft: rgba(91, 135, 255, 0.16);
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
--background: #101216;
--surface: #171a1f;
--surface-alt: #1e2228;
--surface-hover: #262b33;
--border: #2c313a;
--text: #e6e8ec;
--muted: #949ba7;
--accent: #4f7cff;
--accent-hover: #6a90ff;
--accent-soft: #1f2a45;
--danger: #ef6a5e;
--danger-soft: #3a201e;
--success: #3bbf80;
--success-soft: #17332a;
--info: #4f7cff;
--info-soft: #1f2a45;
--warning: #e2a63c;
--warning-soft: #3a2e17;
--track: #262b33;
}
}
:root[data-theme="dark"] {
color-scheme: dark;
--background: #0f1115;
--surface: #171a20;
--surface-alt: #1d2128;
--surface-hover: #242933;
--border: #2a2f39;
--text: #e7e9ee;
--muted: #949cab;
--accent: #5b87ff;
--accent-hover: #7298ff;
--accent-soft: rgba(91, 135, 255, 0.16);
--danger: #f06a5f;
--danger-soft: rgba(240, 106, 95, 0.14);
--success: #3fc486;
--success-soft: rgba(63, 196, 134, 0.14);
--info: #5b87ff;
--info-soft: rgba(91, 135, 255, 0.16);
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
--background: #101216;
--surface: #171a1f;
--surface-alt: #1e2228;
--surface-hover: #262b33;
--border: #2c313a;
--text: #e6e8ec;
--muted: #949ba7;
--accent: #4f7cff;
--accent-hover: #6a90ff;
--accent-soft: #1f2a45;
--danger: #ef6a5e;
--danger-soft: #3a201e;
--success: #3bbf80;
--success-soft: #17332a;
--info: #4f7cff;
--info-soft: #1f2a45;
--warning: #e2a63c;
--warning-soft: #3a2e17;
--track: #262b33;
}
* {
@@ -89,7 +96,7 @@ body {
}
h2 {
font-size: 1.05rem;
font-size: 1rem;
font-weight: 650;
margin: 0 0 14px;
overflow-wrap: anywhere;
@@ -107,16 +114,29 @@ input:not([type="checkbox"]),
select,
textarea {
width: 100%;
min-height: 42px;
padding: 9px 12px;
min-height: 40px;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-small);
background: var(--surface-alt);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
color: var(--text);
transition: border-color 0.15s ease;
}
textarea {
resize: vertical;
line-height: 1.5;
}
input::placeholder,
textarea::placeholder {
color: var(--muted);
opacity: 1;
}
input:not([type="checkbox"]):disabled {
opacity: 0.5;
cursor: not-allowed;
}
input:not([type="checkbox"]):focus,
@@ -124,7 +144,6 @@ select:focus,
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
input[type="checkbox"] {
@@ -144,12 +163,12 @@ button {
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
transition: background 0.15s ease, border-color 0.15s ease;
}
button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--accent-soft);
outline: 2px solid var(--accent);
outline-offset: 2px;
}
button:disabled {
@@ -168,22 +187,22 @@ button.primary:hover:not(:disabled) {
button.ghost,
button.secondary {
background: transparent;
background: var(--surface);
border-color: var(--border);
}
button.ghost:hover,
button.secondary:hover {
button.ghost:hover:not(:disabled),
button.secondary:hover:not(:disabled) {
background: var(--surface-hover);
}
button.danger {
background: transparent;
background: var(--surface);
color: var(--danger);
border-color: var(--danger);
}
button.danger:hover {
button.danger:hover:not(:disabled) {
background: var(--danger-soft);
}
@@ -225,7 +244,6 @@ button.block {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
margin-bottom: 16px;
}
@@ -234,29 +252,31 @@ button.block {
display: flex;
flex-direction: column;
gap: 6px;
font-size: 0.85rem;
font-weight: 550;
min-width: 0;
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
}
.field input,
.field select {
.field select,
.field textarea {
font-size: 0.95rem;
font-weight: 400;
color: var(--text);
}
.checkbox {
display: flex;
align-items: center;
gap: 10px;
margin: 14px 0;
margin: 14px 0 0;
min-height: 32px;
cursor: pointer;
}
.brand-mark {
font-weight: 800;
font-size: 1.15rem;
font-size: 1.1rem;
letter-spacing: 0.06em;
color: var(--accent);
}
@@ -309,9 +329,7 @@ button.block {
position: sticky;
top: 0;
z-index: 10;
background: color-mix(in srgb, var(--surface) 88%, transparent);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
background: var(--surface);
border-bottom: 1px solid var(--border);
padding-top: env(safe-area-inset-top);
}
@@ -339,13 +357,56 @@ button.block {
.download-form {
display: flex;
align-items: stretch;
gap: 10px;
}
.download-form textarea {
flex: 1 1 auto;
min-height: 76px;
}
.download-form button {
min-width: 120px;
}
.download-options {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 16px;
}
.list-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
}
.list-toolbar h2 {
margin: 0;
}
.list-toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.list-toolbar-actions select {
width: auto;
min-height: 32px;
padding: 4px 10px;
font-size: 0.85rem;
}
.empty-state {
text-align: center;
color: var(--muted);
padding: 24px 8px;
padding: 28px 8px;
margin: 0;
border: 1px dashed var(--border);
border-radius: var(--radius-small);
@@ -370,7 +431,6 @@ button.block {
.job.selected {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.job-header {
@@ -413,7 +473,7 @@ button.block {
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 3px 9px;
border-radius: 999px;
border-radius: 4px;
white-space: nowrap;
flex-shrink: 0;
}
@@ -438,10 +498,15 @@ button.block {
background: var(--info-soft);
}
.badge-paused {
color: var(--warning);
background: var(--warning-soft);
}
.progress {
height: 8px;
background: var(--border);
border-radius: 999px;
height: 6px;
background: var(--track);
border-radius: 3px;
overflow: hidden;
}
@@ -449,7 +514,6 @@ button.block {
height: 100%;
width: 0;
background: var(--accent);
border-radius: 999px;
transition: width 0.4s ease;
}
@@ -513,7 +577,7 @@ button.block {
gap: 10px;
padding: 4px 8px;
min-height: 40px;
border-radius: 6px;
border-radius: 4px;
}
.tree-row:hover {
@@ -546,11 +610,97 @@ button.block {
.move-controls {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
#job-detail .checkbox {
margin-bottom: 14px;
}
.history-card {
padding: 0;
}
.history-card summary {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
cursor: pointer;
font-weight: 650;
list-style: none;
}
.history-card summary::-webkit-details-marker {
display: none;
}
.history-card summary::before {
content: "+";
width: 18px;
text-align: center;
color: var(--muted);
font-weight: 700;
}
.history-card[open] summary::before {
content: "-";
}
.history-card[open] summary {
border-bottom: 1px solid var(--border);
}
.history-card > :not(summary) {
margin-left: 20px;
margin-right: 20px;
}
.history-card > :last-child {
margin-bottom: 20px;
}
.history-list {
list-style: none;
padding: 0;
margin-top: 16px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 420px;
overflow-y: auto;
}
.history-list:empty {
display: none;
}
.history-item {
border: 1px solid var(--border);
border-radius: var(--radius-small);
padding: 10px 12px;
background: var(--surface-alt);
}
.history-name {
font-weight: 600;
overflow-wrap: anywhere;
}
.history-meta {
font-size: 0.8rem;
margin: 4px 0 0;
overflow-wrap: anywhere;
}
.history-card .empty-state {
margin-top: 16px;
margin-bottom: 20px;
}
.sheet {
width: min(560px, calc(100% - 32px));
max-height: calc(100dvh - 48px);
@@ -559,11 +709,10 @@ button.block {
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.25);
}
.sheet::backdrop {
background: rgba(10, 12, 16, 0.55);
background: var(--overlay);
}
.sheet-form {
@@ -595,38 +744,62 @@ button.block {
}
.sheet-body {
padding: 4px 20px 16px;
padding: 0 24px 8px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.settings-group {
border: none;
padding: 0;
margin: 16px 0 0;
.sheet-wide {
width: min(860px, calc(100% - 32px));
}
.settings-section {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 24px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
}
.settings-section:last-of-type {
border-bottom: none;
}
.settings-intro h3 {
margin: 0;
font-size: 0.95rem;
font-weight: 650;
}
.settings-intro p {
margin: 4px 0 0;
font-size: 0.85rem;
}
.settings-stack {
display: flex;
flex-direction: column;
gap: 12px;
gap: 10px;
}
.settings-group legend {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
padding: 0;
margin-bottom: 10px;
}
.settings-group .checkbox {
.settings-stack .checkbox {
margin: 0;
}
.settings-narrow {
max-width: 240px;
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.field-hint {
font-size: 0.78rem;
font-weight: 400;
color: var(--muted);
}
.info-list {
@@ -636,15 +809,29 @@ button.block {
}
.info-list dt {
font-size: 0.85rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
margin-top: 6px;
}
.info-list dd + dt {
margin-top: 8px;
}
.info-list dd {
margin: 0;
}
@media (max-width: 760px) {
.download-options {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.download-options .field:first-child {
grid-column: 1 / -1;
}
}
@media (max-width: 640px) {
body {
font-size: 16px;
@@ -652,7 +839,19 @@ button.block {
.card {
padding: 16px;
border-radius: 10px;
}
.history-card {
padding: 0;
}
.history-card summary {
padding: 14px 16px;
}
.history-card > :not(summary) {
margin-left: 16px;
margin-right: 16px;
}
.content {
@@ -674,17 +873,32 @@ button.block {
flex-direction: column;
}
.download-form textarea {
width: 100%;
}
.download-form button,
button.block-mobile {
width: 100%;
min-height: 46px;
}
.download-options,
.move-controls,
.settings-grid {
.settings-grid,
.settings-section {
grid-template-columns: 1fr;
}
.settings-section {
gap: 12px;
padding: 16px 0;
}
.settings-narrow {
max-width: none;
}
.job-actions button {
flex: 1;
min-height: 40px;
@@ -708,7 +922,7 @@ button.block {
max-width: none;
max-height: 92dvh;
margin: auto 0 0;
border-radius: 16px 16px 0 0;
border-radius: 12px 12px 0 0;
border-bottom: none;
}
@@ -731,102 +945,3 @@ button.block {
display: none;
}
}
.badge-paused {
color: var(--muted);
background: var(--border);
}
.download-form {
align-items: flex-start;
}
.download-form textarea {
flex: 1 1 auto;
min-height: 64px;
}
.download-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 14px;
}
.list-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 14px;
}
.list-toolbar h2 {
margin: 0;
}
.list-toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.list-toolbar-actions select {
width: auto;
min-height: 36px;
padding: 6px 10px;
}
.history-card summary {
cursor: pointer;
font-size: 1.05rem;
font-weight: 650;
}
.history-list {
list-style: none;
padding: 0;
margin: 14px 0 12px;
display: flex;
flex-direction: column;
gap: 8px;
max-height: 420px;
overflow-y: auto;
}
.history-list:empty {
display: none;
}
.history-item {
border: 1px solid var(--border);
border-radius: var(--radius-small);
padding: 10px 12px;
background: var(--surface-alt);
}
.history-name {
font-weight: 600;
overflow-wrap: anywhere;
}
.history-meta {
font-size: 0.8rem;
margin: 4px 0 0;
overflow-wrap: anywhere;
}
.history-card .empty-state {
margin: 14px 0 12px;
}
@media (max-width: 640px) {
.download-form textarea {
width: 100%;
}
.download-options {
grid-template-columns: 1fr;
}
}