From 79388370e9505f457e4e9968b7d08721c4368aae Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Fri, 28 Aug 2026 09:19:07 +0300 Subject: [PATCH] feat: collapse the Downloading, Completed and Subscriptions sections (closes #1070) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three section headers become toggles. Collapsing removes the section from the DOM rather than hiding it, so a long completed list stops costing render work while it is put away. The chevron sits at the right edge of the header. Putting it before the label indents that title past the ones without a chevron, and the three section titles are a left-aligned column; keeping them aligned matters more than keeping the affordance next to the word. Direction follows the Advanced Options disclosure already in the form — down when open, right when closed. The request covered Completed and Subscriptions only, but leaving Downloading as the one fixed section is arbitrary once its neighbours move. Each section remembers its own state in a metube_* cookie, matching how every other client-side preference here is persisted; the request asked for localStorage, but a second mechanism for the same job is not worth it. All three default to expanded, so an upgrade doesn't hide anything a user was already looking at. The Downloading and Completed blocks hold the viewChild.required targets behind their select-all checkboxes. Nothing reads them while a section is collapsed: the only caller is the checkbox's own (changed) output, and the queueChanged/doneChanged subscriptions reach it through an optional viewChild. Verified live on a download that finished while the section was put away. --- ui/src/app/app.html | 795 +++++++++++++++++++++-------------------- ui/src/app/app.sass | 22 ++ ui/src/app/app.spec.ts | 25 ++ ui/src/app/app.ts | 21 ++ 4 files changed, 476 insertions(+), 387 deletions(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index f613b4b..e6e67d2 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -690,403 +690,424 @@ Connecting to server... } -
Downloading
-
- - -
-
- - - - - - - - - - - - - @for (download of downloads.queue | keyvalue: asIsOrder; track download.key) { - - - - - - - - - } - -
- - VideoFormatSpeedETA
- - -
-
- {{ download.value.title }} - @if (download.value.live_status === 'is_live' && download.value.status !== 'scheduled') { - LIVE - } -
- @if (download.value.status === 'scheduled') { - - - Waiting for stream - @if (liveCountdownSeconds(download.value); as secs) { - - starts in {{ secs | eta }} - } - - } @else { - - @if (download.value.status === 'postprocessing') { - Post-processing - } - - } -
-
{{ formatLabel(download.value) }}{{ download.value.speed | speed }}{{ download.value.eta | eta }} -
- @if (download.value.status === 'pending' || download.value.status === 'scheduled') { - - } - - -
-
-
- -
Completed
-
- - - - - - -
-
- - - - - - - - - - - - - - - @for (entry of cachedSortedDone; track entry[0]) { - - - - - - - - - - - @if (entry[1].chapter_files && entry[1].chapter_files.length > 0) { - @for (chapterFile of entry[1].chapter_files; track chapterFile.filename) { - - - - - - - - - - - } - } - } - -
- - VideoTypeQualityCodec / FormatFile SizeDownloaded
- - -
- @if (entry[1].status === 'finished') { - - } - @if (entry[1].status === 'error') { - - } -
- @if (!!entry[1].filename) { - {{ entry[1].title }} - } @else { - @if (entry[1].status === 'error') { - - } @else { - {{entry[1].title}} - } - } - @if (entry[1].status === 'error' && isErrorExpanded(entry[0])) { -
-
-
- @if (entry[1].msg) { -
Message: {{entry[1].msg}}
- } - @if (entry[1].error) { -
Error: {{entry[1].error}}
- } -
URL: {{entry[1].url}}
-
- -
-
- } -
- {{ downloadTypeLabel(entry[1]) }} - - {{ formatQualityLabel(entry[1]) }} - - {{ formatCodecLabel(entry[1]) }} - - @if (entry[1].size) { - {{ entry[1].size | fileSize }} - } - - @if (entry[1].timestamp) { - {{ entry[1].timestamp / 1000000 | date:'yyyy-MM-dd HH:mm' }} - } - -
- @if (entry[1].status === 'error') { - - } - @if (entry[1].filename) { - - } - @if (entry[1].filename && canShareDownloads()) { - - } - - -
-
- - - @if (chapterFile.size) { - {{ chapterFile.size | fileSize }} - } - -
- -
-
-
- -
Subscriptions
-
- @if (checkingAllSubscriptions) { - - } @else { - - } - @if (checkingSelectedSubscriptions) { - - } @else { - - } -
-
- - - - - - - - - - - - - - - @for (entry of cachedSubs; track entry[0]) { + @if (!downloadingCollapsed) { +
+ + +
+
+
- - NameURLFilterInterval (min)Last checkedStatus
+ - - - - - - - - + + + + + + + + + @for (download of downloads.queue | keyvalue: asIsOrder; track download.key) { + + + + + + + + + } + +
- - - @if (editingNameId === entry[0]) { -
- - - -
- } @else { -
- {{ entry[1].name }} - -
- } -
{{ entry[1].url }} - @if (editingTitleRegexId === entry[0]) { -
- - - -
- } @else { -
- {{ entry[1].title_regex || '—' }} - -
- } -
{{ entry[1].check_interval_minutes }} - @if (entry[1].last_checked !== null) { - {{ entry[1].last_checked! * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} - } @else { - - } - - @if (entry[1].error) { - {{ entry[1].error }} - } @else if (entry[1].enabled) { - Active - } @else { - Paused - } - -
- @if (isSubscriptionChecking(entry[0])) { - - } @else { - - } -
+ + VideoFormatSpeedETA
+ + +
+
+ {{ download.value.title }} + @if (download.value.live_status === 'is_live' && download.value.status !== 'scheduled') { + LIVE + } +
+ @if (download.value.status === 'scheduled') { + + + Waiting for stream + @if (liveCountdownSeconds(download.value); as secs) { + - starts in {{ secs | eta }} + } + } @else { - + + @if (download.value.status === 'postprocessing') { + Post-processing + } + } - - +
+
{{ formatLabel(download.value) }}{{ download.value.speed | speed }}{{ download.value.eta | eta }} +
+ @if (download.value.status === 'pending' || download.value.status === 'scheduled') { + + } + + +
+
+
+ } + +
+ +
+ @if (!completedCollapsed) { +
+ + + + + + +
+
+ + + + + + + + + + + + + + + @for (entry of cachedSortedDone; track entry[0]) { + + + + + + + + + + + @if (entry[1].chapter_files && entry[1].chapter_files.length > 0) { + @for (chapterFile of entry[1].chapter_files; track chapterFile.filename) { + + + + + + + + + + } } - -
+ + VideoTypeQualityCodec / FormatFile SizeDownloaded
+ + +
+ @if (entry[1].status === 'finished') { + + } + @if (entry[1].status === 'error') { + + } +
+ @if (!!entry[1].filename) { + {{ entry[1].title }} + } @else { + @if (entry[1].status === 'error') { + + } @else { + {{entry[1].title}} + } + } + @if (entry[1].status === 'error' && isErrorExpanded(entry[0])) { +
+
+
+ @if (entry[1].msg) { +
Message: {{entry[1].msg}}
+ } + @if (entry[1].error) { +
Error: {{entry[1].error}}
+ } +
URL: {{entry[1].url}}
+
+ +
+
+ } +
+ {{ downloadTypeLabel(entry[1]) }} + + {{ formatQualityLabel(entry[1]) }} + + {{ formatCodecLabel(entry[1]) }} + + @if (entry[1].size) { + {{ entry[1].size | fileSize }} + } + + @if (entry[1].timestamp) { + {{ entry[1].timestamp / 1000000 | date:'yyyy-MM-dd HH:mm' }} + } + +
+ @if (entry[1].status === 'error') { + + } + @if (entry[1].filename) { + + } + @if (entry[1].filename && canShareDownloads()) { + + } + + +
+
+ + + @if (chapterFile.size) { + {{ chapterFile.size | fileSize }} + } + +
+
+ } + + +
+ } + +
+
+ @if (!subscriptionsCollapsed) { +
+ @if (checkingAllSubscriptions) { + + } @else { + + } + @if (checkingSelectedSubscriptions) { + + } @else { + + } + +
+
+ + + + + + + + + + + + + + + @for (entry of cachedSubs; track entry[0]) { + + + + + + + + + + + } + +
+ + NameURLFilterInterval (min)Last checkedStatus
+ + + @if (editingNameId === entry[0]) { +
+ + + +
+ } @else { +
+ {{ entry[1].name }} + +
+ } +
{{ entry[1].url }} + @if (editingTitleRegexId === entry[0]) { +
+ + + +
+ } @else { +
+ {{ entry[1].title_regex || '—' }} + +
+ } +
{{ entry[1].check_interval_minutes }} + @if (entry[1].last_checked !== null) { + {{ entry[1].last_checked! * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} + } @else { + + } + + @if (entry[1].error) { + {{ entry[1].error }} + } @else if (entry[1].enabled) { + Active + } @else { + Paused + } + +
+ @if (isSubscriptionChecking(entry[0])) { + + } @else { + + } + + +
+
+
+ }