feat: 支持根据筛选条件批量编辑视频的下载状态 (#558)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-12-06 19:47:16 +08:00
committed by GitHub
parent 930660045f
commit f1703096fd
10 changed files with 761 additions and 114 deletions

View File

@@ -5,7 +5,7 @@ import type {
VideosResponse,
VideoResponse,
ResetVideoResponse,
ResetAllVideosResponse,
ResetFilteredVideosResponse,
UpdateVideoStatusRequest,
UpdateVideoStatusResponse,
ApiError,
@@ -21,8 +21,12 @@ import type {
DashBoardResponse,
SysInfo,
TaskStatus,
ResetRequest,
UpdateVideoSourceResponse
ResetVideoStatusRequest,
UpdateVideoSourceResponse,
Notifier,
UpdateFilteredVideoStatusRequest,
UpdateFilteredVideoStatusResponse,
ResetFilteredVideoStatusRequest
} from './types';
import { wsManager } from './ws';
@@ -152,12 +156,17 @@ class ApiClient {
return this.get<VideoResponse>(`/videos/${id}`);
}
async resetVideo(id: number, request: ResetRequest): Promise<ApiResponse<ResetVideoResponse>> {
return this.post<ResetVideoResponse>(`/videos/${id}/reset`, request);
async resetVideoStatus(
id: number,
request: ResetVideoStatusRequest
): Promise<ApiResponse<ResetVideoResponse>> {
return this.post<ResetVideoResponse>(`/videos/${id}/reset-status`, request);
}
async resetAllVideos(request: ResetRequest): Promise<ApiResponse<ResetAllVideosResponse>> {
return this.post<ResetAllVideosResponse>('/videos/reset-all', request);
async resetFilteredVideoStatus(
request: ResetFilteredVideoStatusRequest
): Promise<ApiResponse<ResetFilteredVideosResponse>> {
return this.post<ResetFilteredVideosResponse>('/videos/reset-status', request);
}
async updateVideoStatus(
@@ -167,6 +176,12 @@ class ApiClient {
return this.post<UpdateVideoStatusResponse>(`/videos/${id}/update-status`, request);
}
async updateFilteredVideoStatus(
request: UpdateFilteredVideoStatusRequest
): Promise<ApiResponse<UpdateFilteredVideoStatusResponse>> {
return this.post<UpdateFilteredVideoStatusResponse>('/videos/update-status', request);
}
async getCreatedFavorites(): Promise<ApiResponse<FavoritesResponse>> {
return this.get<FavoritesResponse>('/me/favorites');
}
@@ -268,10 +283,14 @@ const api = {
getVideoSources: () => apiClient.getVideoSources(),
getVideos: (params?: VideosRequest) => apiClient.getVideos(params),
getVideo: (id: number) => apiClient.getVideo(id),
resetVideo: (id: number, request: ResetRequest) => apiClient.resetVideo(id, request),
resetAllVideos: (request: ResetRequest) => apiClient.resetAllVideos(request),
resetVideoStatus: (id: number, request: ResetVideoStatusRequest) =>
apiClient.resetVideoStatus(id, request),
resetFilteredVideoStatus: (request: ResetFilteredVideoStatusRequest) =>
apiClient.resetFilteredVideoStatus(request),
updateVideoStatus: (id: number, request: UpdateVideoStatusRequest) =>
apiClient.updateVideoStatus(id, request),
updateFilteredVideoStatus: (request: UpdateFilteredVideoStatusRequest) =>
apiClient.updateFilteredVideoStatus(request),
getCreatedFavorites: () => apiClient.getCreatedFavorites(),
getFollowedCollections: (pageNum?: number, pageSize?: number) =>
apiClient.getFollowedCollections(pageNum, pageSize),

View File

@@ -0,0 +1,322 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button/index.js';
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle
} from '$lib/components/ui/sheet/index.js';
import type { StatusUpdate, UpdateFilteredVideoStatusRequest } from '$lib/types';
import { toast } from 'svelte-sonner';
export let open = false;
export let hasFilters = false;
export let loading = false;
export let filterDescriptionParts: string[] = [];
export let onsubmit: (request: UpdateFilteredVideoStatusRequest) => void;
// 视频任务名称(与后端 VideoStatus 对应)
const videoTaskNames = ['视频封面', '视频信息', 'UP 主头像', 'UP 主信息', '分页下载'];
// 分页任务名称(与后端 PageStatus 对应)
const pageTaskNames = ['视频封面', '视频内容', '视频信息', '视频弹幕', '视频字幕'];
// 状态选项null 表示未选择0 表示未开始7 表示已完成
type StatusValue = null | 0 | 7;
// 视频任务状态,默认都是 null未选择
let videoStatuses: StatusValue[] = Array(5).fill(null);
// 分页任务状态,默认都是 null未选择
let pageStatuses: StatusValue[] = Array(5).fill(null);
function setVideoStatus(taskIndex: number, value: StatusValue) {
videoStatuses[taskIndex] = value;
videoStatuses = [...videoStatuses];
}
function setPageStatus(taskIndex: number, value: StatusValue) {
pageStatuses[taskIndex] = value;
pageStatuses = [...pageStatuses];
}
function resetVideoStatus(taskIndex: number) {
videoStatuses[taskIndex] = null;
videoStatuses = [...videoStatuses];
}
function resetPageStatus(taskIndex: number) {
pageStatuses[taskIndex] = null;
pageStatuses = [...pageStatuses];
}
function resetAllStatuses() {
videoStatuses = Array(5).fill(null);
pageStatuses = Array(5).fill(null);
}
function hasAnyChanges(): boolean {
return (
videoStatuses.some((status) => status !== null) ||
pageStatuses.some((status) => status !== null)
);
}
function buildRequest(): UpdateFilteredVideoStatusRequest {
const request: UpdateFilteredVideoStatusRequest = {};
// 添加视频更新
const videoUpdates: StatusUpdate[] = [];
videoStatuses.forEach((status, index) => {
if (status !== null) {
videoUpdates.push({
status_index: index,
status_value: status
});
}
});
if (videoUpdates.length > 0) {
request.video_updates = videoUpdates;
}
// 添加分页更新
const pageUpdates: StatusUpdate[] = [];
pageStatuses.forEach((status, index) => {
if (status !== null) {
pageUpdates.push({
status_index: index,
status_value: status
});
}
});
if (pageUpdates.length > 0) {
request.page_updates = pageUpdates;
}
return request;
}
function handleSubmit() {
if (!hasAnyChanges()) {
toast.info('请至少选择一个状态进行修改');
return;
}
const request = buildRequest();
onsubmit(request);
}
// 当 Sheet 关闭时重置状态
$: if (!open) {
resetAllStatuses();
}
function getStatusInfo(status: StatusValue) {
if (status === 0) {
return { label: '未开始', class: 'text-yellow-600', dotClass: 'bg-yellow-600' };
}
if (status === 7) {
return { label: '已完成', class: 'text-emerald-600', dotClass: 'bg-emerald-600' };
}
return { label: '无修改', class: 'text-muted-foreground', dotClass: 'bg-muted-foreground' };
}
</script>
<Sheet bind:open>
<SheetContent side="right" class="flex w-full flex-col sm:max-w-3xl">
<SheetHeader class="px-6 pb-2">
<SheetTitle class="text-lg">{hasFilters ? '编辑筛选视频' : '编辑全部视频'}</SheetTitle>
<SheetDescription class="text-muted-foreground space-y-2 text-sm"
>批量编辑视频和分页的下载状态。可将任意子任务状态修改为“未开始”或“已完成”。<br />
{#if hasFilters}
正在编辑<strong>符合以下筛选条件</strong>的视频的下载状态:
<div class="bg-muted my-2 rounded-md p-2 text-left">
{#each filterDescriptionParts as part, index (index)}
<div><strong>{part}</strong></div>
{/each}
</div>
{:else}
正在编辑<strong>全部视频</strong>的下载状态。 <br />
{/if}
<div class="leading-relaxed text-orange-600">
⚠️ 仅当分页下载状态不是"已完成"时,程序才会尝试执行分页下载。
</div>
</SheetDescription>
</SheetHeader>
<div class="flex-1 overflow-y-auto px-6">
<div class="space-y-6 py-2">
<!-- 视频状态编辑 -->
<div>
<h3 class="mb-4 text-base font-medium">视频状态</h3>
<div class="bg-card rounded-lg border p-4">
<div class="space-y-3">
{#each videoTaskNames as taskName, index (index)}
{@const statusInfo = getStatusInfo(videoStatuses[index])}
{@const isModified = videoStatuses[index] !== null}
<div
class="bg-background hover:bg-muted/30 flex items-center justify-between rounded-md border p-3 transition-colors {isModified
? 'border-blue-200 ring-2 ring-blue-500/20'
: ''}"
>
<div class="flex items-center gap-3">
<div>
<div class="flex items-center gap-2">
<span class="text-sm font-medium">{taskName}</span>
{#if isModified}
<span class="hidden text-xs font-medium text-blue-600 sm:inline"
>已修改</span
>
<div
class="h-2 w-2 rounded-full bg-blue-500 sm:hidden"
title="已修改"
></div>
{/if}
</div>
<div class="mt-0.5 flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full {statusInfo.dotClass}"></div>
<span class="text-xs {statusInfo.class}">{statusInfo.label}</span>
</div>
</div>
</div>
<div class="flex gap-1.5">
{#if isModified}
<Button
variant="ghost"
size="sm"
onclick={() => resetVideoStatus(index)}
disabled={loading}
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs text-gray-600 hover:bg-gray-100"
title="恢复到原始状态"
>
重置
</Button>
{/if}
<Button
variant={videoStatuses[index] === 0 ? 'default' : 'outline'}
size="sm"
onclick={() => setVideoStatus(index, 0)}
disabled={loading}
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {videoStatuses[index] ===
0
? 'border-yellow-600 bg-yellow-600 font-medium text-white hover:bg-yellow-700'
: 'hover:border-yellow-400 hover:bg-yellow-50 hover:text-yellow-700'}"
>
未开始
</Button>
<Button
variant={videoStatuses[index] === 7 ? 'default' : 'outline'}
size="sm"
onclick={() => setVideoStatus(index, 7)}
disabled={loading}
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {videoStatuses[index] ===
7
? 'border-emerald-600 bg-emerald-600 font-medium text-white hover:bg-emerald-700'
: 'hover:border-emerald-400 hover:bg-emerald-50 hover:text-emerald-700'}"
>
已完成
</Button>
</div>
</div>
{/each}
</div>
</div>
</div>
<!-- 分页状态编辑 -->
<div>
<h3 class="mb-4 text-base font-medium">分页状态</h3>
<div class="bg-card rounded-lg border p-4">
<div class="space-y-3">
{#each pageTaskNames as taskName, index (index)}
{@const statusInfo = getStatusInfo(pageStatuses[index])}
{@const isModified = pageStatuses[index] !== null}
<div
class="bg-background hover:bg-muted/30 flex items-center justify-between rounded-md border p-3 transition-colors {isModified
? 'border-blue-200 ring-2 ring-blue-500/20'
: ''}"
>
<div class="flex items-center gap-3">
<div>
<div class="flex items-center gap-2">
<span class="text-sm font-medium">{taskName}</span>
{#if isModified}
<span class="hidden text-xs font-medium text-blue-600 sm:inline"
>已修改</span
>
<div
class="h-2 w-2 rounded-full bg-blue-500 sm:hidden"
title="已修改"
></div>
{/if}
</div>
<div class="mt-0.5 flex items-center gap-1.5">
<div class="h-1.5 w-1.5 rounded-full {statusInfo.dotClass}"></div>
<span class="text-xs {statusInfo.class}">{statusInfo.label}</span>
</div>
</div>
</div>
<div class="flex gap-1.5">
{#if isModified}
<Button
variant="ghost"
size="sm"
onclick={() => resetPageStatus(index)}
disabled={loading}
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs text-gray-600 hover:bg-gray-100"
title="恢复到原始状态"
>
重置
</Button>
{/if}
<Button
variant={pageStatuses[index] === 0 ? 'default' : 'outline'}
size="sm"
onclick={() => setPageStatus(index, 0)}
disabled={loading}
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {pageStatuses[index] === 0
? 'border-yellow-600 bg-yellow-600 font-medium text-white hover:bg-yellow-700'
: 'hover:border-yellow-400 hover:bg-yellow-50 hover:text-yellow-700'}"
>
未开始
</Button>
<Button
variant={pageStatuses[index] === 7 ? 'default' : 'outline'}
size="sm"
onclick={() => setPageStatus(index, 7)}
disabled={loading}
class="h-7 min-w-[60px] cursor-pointer px-3 text-xs {pageStatuses[index] === 7
? 'border-emerald-600 bg-emerald-600 font-medium text-white hover:bg-emerald-700'
: 'hover:border-emerald-400 hover:bg-emerald-50 hover:text-emerald-700'}"
>
已完成
</Button>
</div>
</div>
{/each}
</div>
</div>
</div>
</div>
</div>
<SheetFooter class="bg-background flex gap-2 border-t px-6 pt-4">
<Button
variant="outline"
onclick={resetAllStatuses}
disabled={!hasAnyChanges() || loading}
class="flex-1 cursor-pointer"
>
重置所有状态
</Button>
<Button
onclick={handleSubmit}
disabled={loading || !hasAnyChanges()}
class="flex-1 cursor-pointer"
>
{loading ? '提交中...' : '提交更改'}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>

View File

@@ -31,6 +31,41 @@ export const ToQuery = (state: AppState): string => {
return queryString ? `videos?${queryString}` : 'videos';
};
// 将 AppState 转换为请求体中的筛选参数
export const ToFilterParams = (
state: AppState
): {
query?: string;
collection?: number;
favorite?: number;
submission?: number;
watch_later?: number;
} => {
const params: {
query?: string;
collection?: number;
favorite?: number;
submission?: number;
watch_later?: number;
} = {};
if (state.query.trim()) {
params.query = state.query;
}
if (state.videoSource && state.videoSource.type && state.videoSource.id) {
const { type, id } = state.videoSource;
params[type as 'collection' | 'favorite' | 'submission' | 'watch_later'] = parseInt(id);
}
return params;
};
// 检查是否有活动的筛选条件
export const hasActiveFilters = (state: AppState): boolean => {
return !!(state.query.trim() || state.videoSource);
};
export const setQuery = (query: string) => {
appStateStore.update((state) => ({
...state,

View File

@@ -1,11 +1,8 @@
// API 响应包装器
export interface ApiResponse<T> {
status_code: number;
data: T;
}
// 请求参数类型
export interface VideosRequest {
collection?: number;
favorite?: number;
@@ -16,13 +13,11 @@ export interface VideosRequest {
page_size?: number;
}
// 视频来源类型
export interface VideoSource {
id: number;
name: string;
}
// 视频来源响应类型
export interface VideoSourcesResponse {
collection: VideoSource[];
favorite: VideoSource[];
@@ -30,7 +25,6 @@ export interface VideoSourcesResponse {
watch_later: VideoSource[];
}
// 视频信息类型
export interface VideoInfo {
id: number;
bvid: string;
@@ -40,13 +34,11 @@ export interface VideoInfo {
download_status: [number, number, number, number, number];
}
// 视频列表响应类型
export interface VideosResponse {
videos: VideoInfo[];
total_count: number;
}
// 分页信息类型
export interface PageInfo {
id: number;
pid: number;
@@ -54,59 +46,75 @@ export interface PageInfo {
download_status: [number, number, number, number, number];
}
// 单个视频响应类型
export interface VideoResponse {
video: VideoInfo;
pages: PageInfo[];
}
// 重置视频响应类型
export interface ResetVideoResponse {
resetted: boolean;
video: VideoInfo;
pages: PageInfo[];
}
// 重置所有视频响应类型
export interface ResetAllVideosResponse {
export interface ResetFilteredVideosResponse {
resetted: boolean;
resetted_videos_count: number;
resetted_pages_count: number;
}
// API 错误类型
export interface ApiError {
message: string;
status?: number;
}
// 状态更新类型
export interface StatusUpdate {
status_index: number;
status_value: number;
}
// 页面状态更新类型
export interface PageStatusUpdate {
page_id: number;
updates: StatusUpdate[];
}
// 重置视频状态请求类型
export interface UpdateVideoStatusRequest {
video_updates?: StatusUpdate[];
page_updates?: PageStatusUpdate[];
}
// 重置视频状态响应类型
export interface UpdateVideoStatusResponse {
success: boolean;
video: VideoInfo;
pages: PageInfo[];
}
// 重置请求类型
export interface ResetRequest {
export interface UpdateFilteredVideoStatusResponse {
success: boolean;
updated_videos_count: number;
updated_pages_count: number;
}
export interface ApiError {
message: string;
status?: number;
}
export interface StatusUpdate {
status_index: number;
status_value: number;
}
export interface PageStatusUpdate {
page_id: number;
updates: StatusUpdate[];
}
export interface UpdateVideoStatusRequest {
video_updates?: StatusUpdate[];
page_updates?: PageStatusUpdate[];
}
export interface UpdateFilteredVideoStatusRequest {
collection?: number;
favorite?: number;
submission?: number;
watch_later?: number;
query?: string;
video_updates?: StatusUpdate[];
page_updates?: StatusUpdate[];
}
export interface ResetVideoStatusRequest {
force: boolean;
}
export interface ResetFilteredVideoStatusRequest {
collection?: number;
favorite?: number;
submission?: number;
watch_later?: number;
query?: string;
force: boolean;
}
@@ -170,7 +178,6 @@ export interface InsertSubmissionRequest {
path: string;
}
// Rule 相关类型
export interface Condition<T> {
operator: string;
value: T | T[];
@@ -184,7 +191,6 @@ export interface RuleTarget<T> {
export type AndGroup = RuleTarget<string | number | Date>[];
export type Rule = AndGroup[];
// 视频源详细信息类型
export interface VideoSourceDetail {
id: number;
name: string;
@@ -195,7 +201,6 @@ export interface VideoSourceDetail {
enabled: boolean;
}
// 视频源详细信息响应类型
export interface VideoSourcesDetailsResponse {
collections: VideoSourceDetail[];
favorites: VideoSourceDetail[];
@@ -203,7 +208,6 @@ export interface VideoSourcesDetailsResponse {
watch_later: VideoSourceDetail[];
}
// 更新视频源请求类型
export interface UpdateVideoSourceRequest {
path: string;
enabled: boolean;
@@ -211,7 +215,6 @@ export interface UpdateVideoSourceRequest {
useDynamicApi?: boolean | null;
}
// 配置相关类型
export interface Credential {
sessdata: string;
bili_jct: string;
@@ -273,7 +276,6 @@ export interface ConcurrentLimit {
download: ConcurrentDownloadLimit;
}
// Notifier 相关类型
export interface TelegramNotifier {
type: 'telegram';
bot_token: string;
@@ -312,13 +314,11 @@ export interface Config {
version: number;
}
// 日期计数对类型
export interface DayCountPair {
day: string;
cnt: number;
}
// 仪表盘响应类型
export interface DashBoardResponse {
enabled_favorites: number;
enabled_collections: number;
@@ -327,7 +327,6 @@ export interface DashBoardResponse {
videos_by_day: DayCountPair[];
}
// 系统信息响应类型
export interface SysInfo {
total_memory: number;
used_memory: number;

View File

@@ -166,7 +166,7 @@
bind:resetting
onReset={async (forceReset: boolean) => {
try {
const result = await api.resetVideo(videoData!.video.id, { force: forceReset });
const result = await api.resetVideoStatus(videoData!.video.id, { force: forceReset });
const data = result.data;
if (data.resetted) {
videoData = {

View File

@@ -3,11 +3,18 @@
import Pagination from '$lib/components/pagination.svelte';
import { Button } from '$lib/components/ui/button/index.js';
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
import EditIcon from '@lucide/svelte/icons/edit';
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
import api from '$lib/api';
import { Checkbox } from '$lib/components/ui/checkbox/index.js';
import { Label } from '$lib/components/ui/label/index.js';
import type { VideosResponse, VideoSourcesResponse, ApiError, VideoSource } from '$lib/types';
import type {
VideosResponse,
VideoSourcesResponse,
ApiError,
VideoSource,
UpdateFilteredVideoStatusRequest
} from '$lib/types';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
@@ -19,11 +26,14 @@
setAll,
setCurrentPage,
setQuery,
ToQuery
ToQuery,
ToFilterParams,
hasActiveFilters
} from '$lib/stores/filter';
import { toast } from 'svelte-sonner';
import DropdownFilter, { type Filter } from '$lib/components/dropdown-filter.svelte';
import SearchBar from '$lib/components/search-bar.svelte';
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
const pageSize = 20;
@@ -37,6 +47,9 @@
let forceReset = false;
let updateAllDialogOpen = false;
let updatingAll = false;
let videoSources: VideoSourcesResponse | null = null;
let filters: Record<string, Filter> | null = null;
@@ -97,7 +110,7 @@
async function handleResetVideo(id: number, forceReset: boolean) {
try {
const result = await api.resetVideo(id, { force: forceReset });
const result = await api.resetVideoStatus(id, { force: forceReset });
const data = result.data;
if (data.resetted) {
toast.success('重置成功', {
@@ -121,7 +134,12 @@
async function handleResetAllVideos() {
resettingAll = true;
try {
const result = await api.resetAllVideos({ force: forceReset });
// 获取筛选参数
const filterParams = ToFilterParams($appStateStore);
const result = await api.resetFilteredVideoStatus({
...filterParams,
force: forceReset
});
const data = result.data;
if (data.resetted) {
toast.success('重置成功', {
@@ -143,6 +161,59 @@
}
}
async function handleUpdateAllVideos(request: UpdateFilteredVideoStatusRequest) {
updatingAll = true;
try {
// 获取筛选参数并合并
const filterParams = ToFilterParams($appStateStore);
const fullRequest = {
...filterParams,
...request
};
const result = await api.updateFilteredVideoStatus(fullRequest);
const data = result.data;
if (data.success) {
toast.success('更新成功', {
description: `已更新 ${data.updated_videos_count} 个视频和 ${data.updated_pages_count} 个分页`
});
const { query, currentPage, videoSource } = $appStateStore;
await loadVideos(query, currentPage, videoSource);
} else {
toast.info('没有视频被更新');
}
} catch (error) {
console.error('更新失败:', error);
toast.error('更新失败', {
description: (error as ApiError).message
});
} finally {
updatingAll = false;
updateAllDialogOpen = false;
}
}
// 获取筛选条件的显示数组
function getFilterDescriptionParts(): string[] {
const state = $appStateStore;
const parts: string[] = [];
if (state.query.trim()) {
parts.push(`搜索词:"${state.query}"`);
}
if (state.videoSource && videoSources) {
const sourceType = state.videoSource.type;
const sourceId = parseInt(state.videoSource.id);
const sourceConfig = Object.values(VIDEO_SOURCES).find((s) => s.type === sourceType);
if (sourceConfig) {
const sourceList = videoSources[sourceType as keyof VideoSourcesResponse] as VideoSource[];
const source = sourceList.find((s) => s.id === sourceId);
if (source) {
parts.push(`${sourceConfig.title}${source.name}`);
}
}
}
return parts;
}
$: if ($page.url.search !== lastSearch) {
lastSearch = $page.url.search;
handleSearchParamsChange($page.url.searchParams);
@@ -177,6 +248,8 @@
});
$: totalPages = videosData ? Math.ceil(videosData.total_count / pageSize) : 0;
$: hasFilters = hasActiveFilters($appStateStore);
$: filterDescriptionParts = videoSources && $appStateStore && getFilterDescriptionParts();
</script>
<svelte:head>
@@ -221,6 +294,16 @@
</div>
</div>
<div class="flex items-center gap-2">
<Button
size="sm"
variant="outline"
class="hover:bg-accent hover:text-accent-foreground h-8 cursor-pointer text-xs font-medium"
onclick={() => (updateAllDialogOpen = true)}
disabled={updatingAll || loading}
>
<EditIcon class="mr-1.5 h-3 w-3" />
{hasFilters ? '编辑筛选' : '编辑全部'}
</Button>
<Button
size="sm"
variant="outline"
@@ -229,7 +312,7 @@
disabled={resettingAll || loading}
>
<RotateCcwIcon class="mr-1.5 h-3 w-3 {resettingAll ? 'animate-spin' : ''}" />
重置全部
{hasFilters ? '重置筛选' : '重置全部'}
</Button>
</div>
</div>
@@ -271,16 +354,25 @@
<AlertDialog.Root bind:open={resetAllDialogOpen}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>重置全部视频</AlertDialog.Title>
<AlertDialog.Title>{hasFilters ? '重置筛选视频' : '重置全部视频'}</AlertDialog.Title>
<AlertDialog.Description>
确定要重置<strong>全部视频</strong>的下载状态吗?<br />
{#if hasFilters}
确定要重置<strong>符合以下筛选条件</strong>的视频的下载状态吗?<br />
<div class="bg-muted my-2 rounded-md p-2 text-left">
{#each filterDescriptionParts as part, index (index)}
<div><strong>{part}</strong></div>
{/each}
</div>
{:else}
确定要重置<strong>全部视频</strong>的下载状态吗?<br />
{/if}
此操作会将所有的失败状态重置为未开始,<span class="text-destructive font-medium"
>无法撤销</span
>
</AlertDialog.Description>
</AlertDialog.Header>
<div class="space-y-4 py-4">
<div class="py-2">
<div class="rounded-lg border border-orange-200 bg-orange-50 p-3">
<div class="mb-2 flex items-center space-x-2">
<Checkbox id="force-reset-all" bind:checked={forceReset} />
@@ -317,3 +409,11 @@
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
<FilteredStatusEditor
bind:open={updateAllDialogOpen}
{hasFilters}
loading={updatingAll}
filterDescriptionParts={filterDescriptionParts || []}
onsubmit={handleUpdateAllVideos}
/>