metube/ui/src/app/app.component.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>