feat: 下载列表增加标签页切换并优化 Docker 构建缓存分层
This commit is contained in:
parent
9cf1c79f2a
commit
d95a97d656
|
|
@ -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 .
|
||||
```
|
||||
|
||||
|
|
|
|||
25
Dockerfile
25
Dockerfile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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> 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" [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 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> 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 #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 #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 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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue