feat: 在视频页显示视频属于哪个视频源 (#676)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2026-03-15 21:53:15 +08:00
committed by GitHub
parent d39cce043c
commit dd96a32b35
5 changed files with 72 additions and 5 deletions

View File

@@ -77,6 +77,10 @@ pub struct VideoInfo {
pub should_download: bool,
#[serde(serialize_with = "serde_video_download_status")]
pub download_status: u32,
pub collection_id: Option<i32>,
pub favorite_id: Option<i32>,
pub submission_id: Option<i32>,
pub watch_later_id: Option<i32>,
}
#[derive(Serialize, DerivePartialModel, FromQueryResult)]

View File

@@ -200,6 +200,10 @@ pub async fn clear_and_reset_video_status(
valid: video_info.valid,
should_download: video_info.should_download,
download_status: video_info.download_status,
collection_id: video_info.collection_id,
favorite_id: video_info.favorite_id,
submission_id: video_info.submission_id,
watch_later_id: video_info.watch_later_id,
},
}))
}

View File

@@ -13,13 +13,17 @@
BrushCleaningIcon,
UserIcon,
SquareArrowOutUpRightIcon,
EllipsisIcon
EllipsisIcon,
HeartIcon,
FolderIcon,
ClockIcon
} from '@lucide/svelte/icons';
import { goto } from '$app/navigation';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
// 将 bvid 设置为可选属性,但保留 VideoInfo 的其它所有属性
export let video: Omit<VideoInfo, 'bvid'> & { bvid?: string };
export let source: { type: string; name: string } | null = null; // 视频源信息
export let showActions: boolean = true; // 控制是否显示操作按钮
export let mode: 'default' | 'detail' | 'page' = 'default'; // 卡片模式
export let customTitle: string = ''; // 自定义标题
@@ -132,8 +136,8 @@
</script>
<Card class={cardClasses}>
<CardHeader class="shrink-0 pb-3">
<div class="flex min-w-0 items-start justify-between gap-3">
<CardHeader class="shrink-0 pb-1">
<div class="flex min-w-0 items-start justify-between gap-3 {source ? 'min-h-12' : ''}">
<CardTitle
class="line-clamp-2 min-w-0 flex-1 cursor-default {mode === 'default'
? 'text-sm'
@@ -157,6 +161,24 @@
</span>
</div>
{/if}
{#if source}
<div class="text-muted-foreground mt-2 flex min-w-0 items-center justify-end gap-1 text-sm">
<Badge variant="outline" class="shrink-0 px-1.5 py-0.5">
{#if source.type === 'favorite'}
<HeartIcon class="h-3.5 w-3.5 shrink-0" />
{:else if source.type === 'collection'}
<FolderIcon class="h-3.5 w-3.5 shrink-0" />
{:else if source.type === 'submission'}
<UserIcon class="h-3.5 w-3.5 shrink-0" />
{:else if source.type === 'watch_later'}
<ClockIcon class="h-3.5 w-3.5 shrink-0" />
{/if}
<span class="min-w-0 truncate" title={source.name}>
{source.name}
</span>
</Badge>
</div>
{/if}
</CardHeader>
<CardContent
class={mode === 'default' ? 'flex min-w-0 flex-1 flex-col justify-end pt-0 pb-3' : 'pt-0 pb-4'}

View File

@@ -35,6 +35,10 @@ export interface VideoInfo {
valid: boolean;
should_download: boolean;
download_status: [number, number, number, number, number];
collection_id?: number;
favorite_id?: number;
submission_id?: number;
watch_later_id?: number;
}
export interface VideosResponse {

View File

@@ -12,7 +12,8 @@
VideoSourcesResponse,
ApiError,
VideoSource,
UpdateFilteredVideoStatusRequest
UpdateFilteredVideoStatusRequest,
VideoInfo
} from '$lib/types';
import { onMount } from 'svelte';
import { page } from '$app/stores';
@@ -39,6 +40,7 @@
import FilteredStatusEditor from '$lib/components/filtered-status-editor.svelte';
import StatusFilter from '$lib/components/status-filter.svelte';
import ValidationFilter from '$lib/components/validation-filter.svelte';
import { SvelteMap } from 'svelte/reactivity';
const pageSize = 20;
@@ -56,7 +58,9 @@
let updatingAll = false;
let videoSources: VideoSourcesResponse | null = null;
let videoSourcesLoaded = false;
let filters: Record<string, Filter> | null = null;
let sourceMap: SvelteMap<string, { type: string; name: string }> = new SvelteMap();
function getApiParams(searchParams: URLSearchParams) {
let videoSource = null;
@@ -246,6 +250,22 @@
}
}
function getVideoSource(video: VideoInfo): { type: string; name: string } | null {
if (video.collection_id != null) {
return sourceMap.get(`collection:${video.collection_id}`) || null;
}
if (video.favorite_id != null) {
return sourceMap.get(`favorite:${video.favorite_id}`) || null;
}
if (video.submission_id != null) {
return sourceMap.get(`submission:${video.submission_id}`) || null;
}
if (video.watch_later_id != null) {
return sourceMap.get(`watch_later:${video.watch_later_id}`) || null;
}
return null;
}
// 获取筛选条件的显示数组
function getFilterDescriptionParts(): string[] {
const state = $appStateStore;
@@ -284,7 +304,7 @@
return parts;
}
$: if ($page.url.search !== lastSearch) {
$: if (videoSourcesLoaded && $page.url.search !== lastSearch) {
lastSearch = $page.url.search;
handleSearchParamsChange($page.url.searchParams);
}
@@ -304,8 +324,19 @@
}
])
);
sourceMap.clear();
for (const source of Object.values(VIDEO_SOURCES)) {
const sourceList = videoSources[source.type as keyof VideoSourcesResponse] as VideoSource[];
for (const item of sourceList) {
sourceMap.set(`${source.type}:${item.id}`, {
type: source.type,
name: item.name
});
}
}
} else {
filters = null;
sourceMap.clear();
}
onMount(async () => {
@@ -315,6 +346,7 @@
}
]);
videoSources = (await api.getVideoSources()).data;
videoSourcesLoaded = true;
});
$: totalPages = videosData ? Math.ceil(videosData.total_count / pageSize) : 0;
@@ -435,6 +467,7 @@
{#each videosData.videos as video (video.id)}
<VideoCard
{video}
source={getVideoSource(video)}
onReset={async (forceReset: boolean) => {
await handleResetVideo(video.id, forceReset);
}}