feat: 下载列表增加标签页切换并优化 Docker 构建缓存分层

This commit is contained in:
tigerenwork 2026-08-08 00:49:01 +08:00
parent 9cf1c79f2a
commit d95a97d656
5 changed files with 59 additions and 34 deletions

View File

@ -27,8 +27,8 @@ default, e.g. via QEMU emulation).
```sh
docker buildx build --platform linux/amd64 \
--build-arg VERSION=1.10 \
-t 192.168.2.212:3000/tigeren/metube:1.10 \
--build-arg VERSION=1.11 \
-t 192.168.2.212:3000/tigeren/metube:1.11 \
--push .
```

View File

@ -12,23 +12,30 @@ FROM python:3.13-slim
WORKDIR /app
COPY pyproject.toml uv.lock docker-entrypoint.sh ./
# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows)
# Install dependencies
RUN sed -i 's/\r$//g' docker-entrypoint.sh && \
chmod +x docker-entrypoint.sh && \
# Heavy system dependencies in their own layer, independent of app/dep files,
# so it stays cached unless this block itself changes.
# apt caches are BuildKit cache mounts: even when this layer is invalidated,
# package downloads are reused instead of hitting deb.debian.org again.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends ffmpeg aria2 curl tini gosu ca-certificates unzip \
chromium libnss3 libfreetype6 libharfbuzz0b fonts-freefont-ttf && \
ln -s /usr/sbin/gosu /usr/local/bin/su-exec && \
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && \
pip install --no-cache-dir uv && \
UV_PROJECT_ENVIRONMENT=/usr/local uv sync --frozen --no-dev --compile-bytecode --extra headless && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* && \
mkdir /.cache && chmod 777 /.cache
# Python dependencies: only invalidated when pyproject.toml/uv.lock change.
# The uv cache mount makes re-resolving after a lockfile change much faster.
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
UV_PROJECT_ENVIRONMENT=/usr/local uv sync --frozen --no-dev --compile-bytecode --extra headless
# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows)
COPY docker-entrypoint.sh ./
RUN sed -i 's/\r$//g' docker-entrypoint.sh && chmod +x docker-entrypoint.sh
COPY app ./app
COPY --from=builder /metube/dist/metube ./ui/dist/metube

View File

@ -313,11 +313,27 @@
<div *ngIf="downloads.loading" class="alert alert-info" role="alert">
Connecting to server...
</div>
<div class="metube-section-header">Downloading</div>
<div class="px-2 py-3 border-bottom">
<ul class="nav nav-tabs metube-tabs">
<li class="nav-item">
<a class="nav-link" role="button" [class.active]="activeTab === 'queue'" (click)="setActiveTab('queue')">
Downloading <span class="badge text-bg-secondary">{{ downloads.queue.size }}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" role="button" [class.active]="activeTab === 'done'" (click)="setActiveTab('done')">
Completed <span class="badge text-bg-secondary">{{ downloads.done.size }}</span>
</a>
</li>
</ul>
<div [class.d-none]="activeTab !== 'queue'">
<div class="px-2 py-3 border-bottom d-flex align-items-center flex-wrap">
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #queueDelSelected (click)="delSelectedDownloads('queue')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Cancel selected</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #queueDownloadSelected (click)="startSelectedDownloads('queue')"><fa-icon [icon]="faDownload"></fa-icon>&nbsp; Download selected</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" [ngClass]="{'fw-bold': groupByFolder}" (click)="toggleGroupByFolder()" ngbTooltip="Group downloads by folder"><fa-icon [icon]="faFolderOpen"></fa-icon>&nbsp; Group by folder</button>
<div class="form-check form-switch d-flex align-items-center mb-0 ps-0">
<input class="form-check-input ms-0 me-2" type="checkbox" role="switch" id="queueGroupByFolder" [(ngModel)]="groupByFolder" (change)="groupByFolderChanged()" ngbTooltip="Group downloads by folder">
<label class="form-check-label mb-0" for="queueGroupByFolder"><fa-icon [icon]="faFolderOpen"></fa-icon>&nbsp; Group by folder</label>
</div>
</div>
<div class="overflow-auto">
<table class="table">
@ -374,15 +390,19 @@
</tbody>
</table>
</div>
</div>
<div class="metube-section-header">Completed</div>
<div class="px-2 py-3 border-bottom">
<div [class.d-none]="activeTab !== 'done'">
<div class="px-2 py-3 border-bottom d-flex align-items-center flex-wrap">
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneDelSelected (click)="delSelectedDownloads('done')"><fa-icon [icon]="faTrashAlt"></fa-icon>&nbsp; Clear selected</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneClearCompleted (click)="clearCompletedDownloads()"><fa-icon [icon]="faCheckCircle"></fa-icon>&nbsp; Clear completed</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneClearFailed (click)="clearFailedDownloads()"><fa-icon [icon]="faTimesCircle"></fa-icon>&nbsp; Clear failed</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneRetryFailed (click)="retryFailedDownloads()"><fa-icon [icon]="faRedoAlt"></fa-icon>&nbsp; Retry failed</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" disabled #doneDownloadSelected (click)="downloadSelectedFiles()"><fa-icon [icon]="faDownload"></fa-icon>&nbsp; Download Selected</button>
<button type="button" class="btn btn-link text-decoration-none px-0 me-4" [ngClass]="{'fw-bold': groupByFolder}" (click)="toggleGroupByFolder()" ngbTooltip="Group downloads by folder"><fa-icon [icon]="faFolderOpen"></fa-icon>&nbsp; Group by folder</button>
<div class="form-check form-switch d-flex align-items-center mb-0 ps-0">
<input class="form-check-input ms-0 me-2" type="checkbox" role="switch" id="doneGroupByFolder" [(ngModel)]="groupByFolder" (change)="groupByFolderChanged()" ngbTooltip="Group downloads by folder">
<label class="form-check-label mb-0" for="doneGroupByFolder"><fa-icon [icon]="faFolderOpen"></fa-icon>&nbsp; Group by folder</label>
</div>
</div>
<div class="overflow-auto">
<table class="table">
@ -450,6 +470,7 @@
</tbody>
</table>
</div>
</div>
</main><!-- /.container -->
<footer class="footer navbar-dark bg-dark py-3 mt-5">

View File

@ -28,23 +28,12 @@ button.add-url
cursor: pointer
user-select: none
.metube-section-header
font-size: 1.8rem
font-weight: 300
position: relative
background: var(--bs-secondary-bg)
padding: 0.5rem 0
.metube-tabs
margin-top: 3.5rem
font-size: 1.1rem
.metube-section-header:before
content: ""
position: absolute
top: 0
bottom: 0
left: -9999px
right: 0
border-left: 9999px solid var(--bs-secondary-bg)
box-shadow: 9999px 0 0 var(--bs-secondary-bg)
.nav-link
cursor: pointer
button:hover
text-decoration: none

View File

@ -55,6 +55,9 @@ export class AppComponent implements OnInit, AfterViewInit {
// Group downloads by folder instead of showing a flat list
groupByFolder = false;
// Which downloads tab is shown: the queue (downloading) or the completed list
activeTab: 'queue' | 'done' = 'queue';
// Download metrics
activeDownloads = 0;
queuedDownloads = 0;
@ -104,6 +107,7 @@ export class AppComponent implements OnInit, AfterViewInit {
this.autoStart = cookieService.get('metube_auto_start') !== 'false';
this.sortAscending = cookieService.get('metube_sort_order') === 'asc';
this.groupByFolder = cookieService.get('metube_group_by_folder') === 'true';
this.activeTab = cookieService.get('metube_active_tab') === 'done' ? 'done' : 'queue';
this.activeTheme = this.getPreferredTheme(cookieService);
@ -182,11 +186,15 @@ export class AppComponent implements OnInit, AfterViewInit {
this.cookieService.set('metube_sort_order', this.sortAscending ? 'asc' : 'desc', { expires: 3650 });
}
toggleGroupByFolder() {
this.groupByFolder = !this.groupByFolder;
groupByFolderChanged() {
this.cookieService.set('metube_group_by_folder', this.groupByFolder ? 'true' : 'false', { expires: 3650 });
}
setActiveTab(tab: 'queue' | 'done') {
this.activeTab = tab;
this.cookieService.set('metube_active_tab', tab, { expires: 3650 });
}
// Build the rows for a download table: a flat list, or folder header rows
// interleaved with the items of each folder when group-by-folder is enabled.
displayRows(where: 'queue' | 'done'): DisplayRow[] {