225 lines
9.2 KiB
HTML
225 lines
9.2 KiB
HTML
<div class="app">
|
|
<app-sidebar
|
|
[status]="status"
|
|
[selectedFolder]="selectedFolder"
|
|
[statusCounts]="statusCounts"
|
|
[folders]="folderNavs"
|
|
[allCount]="allCount"
|
|
[metubeVersion]="metubeVersion"
|
|
[themes]="themes"
|
|
[activeTheme]="activeTheme"
|
|
[open]="sidebarOpen"
|
|
[mode]="mode"
|
|
[libraryEnabled]="libraryEnabled"
|
|
[libraryFolders]="libraryFolders"
|
|
[libraryTotal]="libraryTotal"
|
|
[selectedLibraryFolder]="libraryFolder"
|
|
(statusChange)="onStatusChange($event)"
|
|
(folderChange)="onFolderChange($event)"
|
|
(modeChange)="setMode($event)"
|
|
(libraryFolderChange)="selectLibraryFolder($event)"
|
|
(openAdvanced)="toggleAdvanced(true)"
|
|
(openActivity)="activityOpen = !activityOpen"
|
|
(themeChanged)="themeChanged($event)"
|
|
(close)="sidebarOpen = false">
|
|
</app-sidebar>
|
|
|
|
<main class="main">
|
|
<app-command-bar
|
|
[url]="addUrl"
|
|
(urlChange)="addUrl = $event"
|
|
[quality]="quality"
|
|
[format]="format"
|
|
[folder]="folder"
|
|
[qualities]="qualities"
|
|
[formats]="formats"
|
|
[customDirs$]="customDirs$"
|
|
[allowCustomDir]="allowCustomDir.bind(this)"
|
|
[disabled]="addInProgress || downloads.loading"
|
|
(add)="addDownload($event)"
|
|
(qualityChange)="setQuality($event)"
|
|
(formatChange)="setFormat($event)"
|
|
(folderChange)="setFolder($event)"
|
|
(openAdvanced)="toggleAdvanced(true)"
|
|
(menu)="sidebarOpen = true">
|
|
</app-command-bar>
|
|
|
|
<app-stats-strip
|
|
[active]="activeDownloads"
|
|
[speed]="totalSpeed"
|
|
[queued]="queuedDownloads"
|
|
[storage]="librarySize">
|
|
</app-stats-strip>
|
|
|
|
<div class="content" [class.d-none]="mode !== 'downloads'">
|
|
<app-active-downloads
|
|
[items]="downloadingRows"
|
|
[totalSpeed]="totalSpeed"
|
|
[loading]="downloads.loading"
|
|
(cancel)="cancelDownload($event)">
|
|
</app-active-downloads>
|
|
|
|
<app-download-list
|
|
[status]="status"
|
|
[rows]="filteredRows"
|
|
[folderName]="folderName"
|
|
[filterText]="filter"
|
|
(filterChange)="filter = $event"
|
|
[sort]="sort"
|
|
(sortChange)="sort = $event"
|
|
[sortAscending]="sortAscending"
|
|
(sortAscendingChange)="setSortDirection($event)"
|
|
[view]="view"
|
|
(viewChange)="setView($event)"
|
|
[loading]="downloads.loading"
|
|
[customDirsFor]="customDirsForDownload.bind(this)"
|
|
[allowCustomDir]="allowCustomDir.bind(this)"
|
|
[downloadUrlFor]="buildDownloadLink.bind(this)"
|
|
[libraryEnabled]="libraryEnabled"
|
|
(start)="startItems($event)"
|
|
(del)="delItems($event, status === 'completed' ? 'done' : 'queue')"
|
|
(move)="moveItems($event.ids, $event.folder)"
|
|
(retry)="retryDownload($event.key, $event.dl)"
|
|
(folderEdit)="folderChanged($event.key, $event.folder)"
|
|
(clearCompleted)="clearCompletedDownloads()"
|
|
(clearFailed)="clearFailedDownloads()"
|
|
(retryFailed)="retryFailedDownloads()"
|
|
(downloadSelected)="downloadSelectedFiles($event)"
|
|
(play)="openPlayer($event)"
|
|
(organize)="openOrganize($event)">
|
|
</app-download-list>
|
|
</div>
|
|
|
|
<div class="content" *ngIf="mode === 'library'">
|
|
<app-library-browser
|
|
[files]="libraryFiles"
|
|
[folderPath]="libraryFolder"
|
|
[folders]="libraryFolderNames"
|
|
[loading]="libraryLoading"
|
|
(play)="playLibraryFile($event)"
|
|
(del)="deleteLibraryFile($event)"
|
|
(batchDelete)="deleteLibraryFiles($event)"
|
|
(batchMove)="moveLibraryFiles($event)"
|
|
(refresh)="loadLibraryFolders(); loadLibraryFiles()">
|
|
</app-library-browser>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Advanced options slide-over -->
|
|
<app-advanced-options
|
|
[open]="isAdvancedOpen"
|
|
[autoStart]="autoStart"
|
|
(autoStartChange)="setAutoStart($event)"
|
|
[playlistStrictMode]="playlistStrictMode"
|
|
(strictChange)="playlistStrictMode = $event"
|
|
[playlistItemLimit]="playlistItemLimit"
|
|
(limitChange)="playlistItemLimit = $event"
|
|
[customNamePrefix]="customNamePrefix"
|
|
(prefixChange)="customNamePrefix = $event"
|
|
[metubeVersion]="metubeVersion"
|
|
[ytDlpVersion]="ytDlpVersion"
|
|
[ytDlpOptionsUpdateTime]="ytDlpOptionsUpdateTime"
|
|
(close)="toggleAdvanced(false)"
|
|
(import)="openBatchImportModal()"
|
|
(export)="exportBatchUrls('all')"
|
|
(copy)="copyBatchUrls('all')"
|
|
(cookie)="saveCookie($event.domain, $event.cookie)">
|
|
</app-advanced-options>
|
|
|
|
<!-- Batch import modal -->
|
|
<div class="modal fade" tabindex="-1" role="dialog" [ngClass]="{'show': batchImportModalOpen}" [ngStyle]="{'display': batchImportModalOpen ? 'block' : 'none'}">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">批量导入 URL</h5>
|
|
<button type="button" class="btn-close" aria-label="关闭" (click)="closeBatchImportModal()"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<textarea [(ngModel)]="batchImportText" class="form-control" rows="6" placeholder="每行粘贴一个视频 URL"></textarea>
|
|
<div class="mt-2">
|
|
<small *ngIf="batchImportStatus" class="batch-status">{{ batchImportStatus }}</small>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-danger me-auto" *ngIf="importInProgress" (click)="cancelBatchImport()">取消导入</button>
|
|
<button type="button" class="btn btn-secondary" (click)="closeBatchImportModal()">关闭</button>
|
|
<button type="button" class="btn btn-primary" (click)="startBatchImport()" [disabled]="importInProgress">导入 URL</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Organize to library modal -->
|
|
<div class="modal fade" tabindex="-1" role="dialog" [ngClass]="{'show': organizeOpen}" [ngStyle]="{'display': organizeOpen ? 'block' : 'none'}">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">整理到媒体库</h5>
|
|
<button type="button" class="btn-close" aria-label="关闭" (click)="closeOrganize()"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>将 {{ organizeItems.length }} 个文件移动到媒体库文件夹:</p>
|
|
<div *ngIf="organizeSuggestions.length > 0" class="mb-3">
|
|
<div class="text-muted small mb-2">建议(点击直接整理):</div>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm me-2" *ngFor="let s of organizeSuggestions" (click)="confirmOrganize(s)" [disabled]="organizeInProgress">{{ s }}</button>
|
|
</div>
|
|
<ng-select [items]="libraryFolderNames"
|
|
[(ngModel)]="organizeTarget"
|
|
[addTag]="true"
|
|
addTagText="新建文件夹"
|
|
placeholder="搜索或新建文件夹…"
|
|
[searchable]="true"
|
|
[clearable]="true"
|
|
appendTo="body">
|
|
<ng-template ng-option-tmp let-item="item">
|
|
<span [title]="item">{{ item }}</span>
|
|
</ng-template>
|
|
</ng-select>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" (click)="closeOrganize()">取消</button>
|
|
<button type="button" class="btn btn-primary" (click)="confirmOrganize(organizeTarget)" [disabled]="!organizeTarget || organizeInProgress">
|
|
{{ organizeInProgress ? '整理中…' : '整理' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity drawer -->
|
|
<div class="scrim" *ngIf="activityOpen" (click)="activityOpen = false"></div>
|
|
<aside class="activity" [class.show]="activityOpen" aria-label="活动日志">
|
|
<div class="activity-head">
|
|
<span class="activity-title">活动日志</span>
|
|
<button type="button" class="icon-btn neutral" (click)="activityOpen = false" aria-label="关闭">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
</div>
|
|
<div class="activity-body">
|
|
<div *ngIf="events.length === 0" class="activity-empty">暂无事件</div>
|
|
<div *ngFor="let event of events" class="activity-item">
|
|
<div class="activity-msg">{{ event.message }}</div>
|
|
<div class="activity-url" *ngIf="event.url" [title]="event.url">{{ event.url }}</div>
|
|
<div class="activity-time num">{{ getRelativeTime(event.timestamp) }}</div>
|
|
</div>
|
|
<button type="button" class="ghost-btn activity-clear" (click)="clearEvents()">清空日志</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- In-page video player -->
|
|
<div class="scrim" *ngIf="playerUrl" (click)="closePlayer()"></div>
|
|
<div class="player" *ngIf="playerUrl" role="dialog" aria-label="视频播放">
|
|
<div class="player-head">
|
|
<span class="player-title" [title]="playerTitle">{{ playerTitle }}</span>
|
|
<button type="button" class="icon-btn neutral" (click)="closePlayer()" aria-label="关闭">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
</button>
|
|
</div>
|
|
<video class="player-video" [src]="playerUrl" controls autoplay
|
|
(keydown.escape)="closePlayer()"
|
|
(focusin)="onVideoFocusIn($event)"></video>
|
|
</div>
|
|
|
|
<app-toasts></app-toasts>
|