feat: 下载列表增加标签页切换并优化 Docker 构建缓存分层
This commit is contained in:
parent
9cf1c79f2a
commit
d95a97d656
|
|
@ -27,8 +27,8 @@ default, e.g. via QEMU emulation).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker buildx build --platform linux/amd64 \
|
docker buildx build --platform linux/amd64 \
|
||||||
--build-arg VERSION=1.10 \
|
--build-arg VERSION=1.11 \
|
||||||
-t 192.168.2.212:3000/tigeren/metube:1.10 \
|
-t 192.168.2.212:3000/tigeren/metube:1.11 \
|
||||||
--push .
|
--push .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
25
Dockerfile
25
Dockerfile
|
|
@ -12,23 +12,30 @@ FROM python:3.13-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY pyproject.toml uv.lock docker-entrypoint.sh ./
|
# Heavy system dependencies in their own layer, independent of app/dep files,
|
||||||
|
# so it stays cached unless this block itself changes.
|
||||||
# Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows)
|
# apt caches are BuildKit cache mounts: even when this layer is invalidated,
|
||||||
# Install dependencies
|
# package downloads are reused instead of hitting deb.debian.org again.
|
||||||
RUN sed -i 's/\r$//g' docker-entrypoint.sh && \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
chmod +x docker-entrypoint.sh && \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ffmpeg aria2 curl tini gosu ca-certificates unzip \
|
apt-get install -y --no-install-recommends ffmpeg aria2 curl tini gosu ca-certificates unzip \
|
||||||
chromium libnss3 libfreetype6 libharfbuzz0b fonts-freefont-ttf && \
|
chromium libnss3 libfreetype6 libharfbuzz0b fonts-freefont-ttf && \
|
||||||
ln -s /usr/sbin/gosu /usr/local/bin/su-exec && \
|
ln -s /usr/sbin/gosu /usr/local/bin/su-exec && \
|
||||||
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && \
|
curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && \
|
||||||
pip install --no-cache-dir uv && \
|
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
|
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 app ./app
|
||||||
COPY --from=builder /metube/dist/metube ./ui/dist/metube
|
COPY --from=builder /metube/dist/metube ./ui/dist/metube
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,11 +313,27 @@
|
||||||
<div *ngIf="downloads.loading" class="alert alert-info" role="alert">
|
<div *ngIf="downloads.loading" class="alert alert-info" role="alert">
|
||||||
Connecting to server...
|
Connecting to server...
|
||||||
</div>
|
</div>
|
||||||
<div class="metube-section-header">Downloading</div>
|
<ul class="nav nav-tabs metube-tabs">
|
||||||
<div class="px-2 py-3 border-bottom">
|
<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> Cancel selected</button>
|
<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> 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> Download 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> 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> 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> Group by folder</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-auto">
|
<div class="overflow-auto">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
|
@ -374,15 +390,19 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="metube-section-header">Completed</div>
|
<div [class.d-none]="activeTab !== 'done'">
|
||||||
<div class="px-2 py-3 border-bottom">
|
<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> Clear selected</button>
|
<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> 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> Clear completed</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> 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> Clear failed</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> 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> Retry 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> 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> Download Selected</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> 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> 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> Group by folder</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-auto">
|
<div class="overflow-auto">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
|
@ -450,6 +470,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</main><!-- /.container -->
|
</main><!-- /.container -->
|
||||||
|
|
||||||
<footer class="footer navbar-dark bg-dark py-3 mt-5">
|
<footer class="footer navbar-dark bg-dark py-3 mt-5">
|
||||||
|
|
|
||||||
|
|
@ -28,23 +28,12 @@ button.add-url
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
user-select: none
|
user-select: none
|
||||||
|
|
||||||
.metube-section-header
|
.metube-tabs
|
||||||
font-size: 1.8rem
|
|
||||||
font-weight: 300
|
|
||||||
position: relative
|
|
||||||
background: var(--bs-secondary-bg)
|
|
||||||
padding: 0.5rem 0
|
|
||||||
margin-top: 3.5rem
|
margin-top: 3.5rem
|
||||||
|
font-size: 1.1rem
|
||||||
|
|
||||||
.metube-section-header:before
|
.nav-link
|
||||||
content: ""
|
cursor: pointer
|
||||||
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)
|
|
||||||
|
|
||||||
button:hover
|
button:hover
|
||||||
text-decoration: none
|
text-decoration: none
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
// Group downloads by folder instead of showing a flat list
|
// Group downloads by folder instead of showing a flat list
|
||||||
groupByFolder = false;
|
groupByFolder = false;
|
||||||
|
|
||||||
|
// Which downloads tab is shown: the queue (downloading) or the completed list
|
||||||
|
activeTab: 'queue' | 'done' = 'queue';
|
||||||
|
|
||||||
// Download metrics
|
// Download metrics
|
||||||
activeDownloads = 0;
|
activeDownloads = 0;
|
||||||
queuedDownloads = 0;
|
queuedDownloads = 0;
|
||||||
|
|
@ -104,6 +107,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
||||||
this.autoStart = cookieService.get('metube_auto_start') !== 'false';
|
this.autoStart = cookieService.get('metube_auto_start') !== 'false';
|
||||||
this.sortAscending = cookieService.get('metube_sort_order') === 'asc';
|
this.sortAscending = cookieService.get('metube_sort_order') === 'asc';
|
||||||
this.groupByFolder = cookieService.get('metube_group_by_folder') === 'true';
|
this.groupByFolder = cookieService.get('metube_group_by_folder') === 'true';
|
||||||
|
this.activeTab = cookieService.get('metube_active_tab') === 'done' ? 'done' : 'queue';
|
||||||
|
|
||||||
this.activeTheme = this.getPreferredTheme(cookieService);
|
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 });
|
this.cookieService.set('metube_sort_order', this.sortAscending ? 'asc' : 'desc', { expires: 3650 });
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleGroupByFolder() {
|
groupByFolderChanged() {
|
||||||
this.groupByFolder = !this.groupByFolder;
|
|
||||||
this.cookieService.set('metube_group_by_folder', this.groupByFolder ? 'true' : 'false', { expires: 3650 });
|
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
|
// 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.
|
// interleaved with the items of each folder when group-by-folder is enabled.
|
||||||
displayRows(where: 'queue' | 'done'): DisplayRow[] {
|
displayRows(where: 'queue' | 'done'): DisplayRow[] {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue