diff --git a/src/admin/DrivesPage.tsx b/src/admin/DrivesPage.tsx index 1d033ab..9ca5279 100644 --- a/src/admin/DrivesPage.tsx +++ b/src/admin/DrivesPage.tsx @@ -632,7 +632,7 @@ export function DrivesPage() { handleStopDriveTasks(d)} disabled={!!stoppingDriveId} title="停止此网盘当前的扫描、封面、预览视频和视频指纹生成任务。" @@ -642,7 +642,7 @@ export function DrivesPage() { {d.kind !== "spider91" && ( - openEdit(d)}> + openEdit(d)}> 编辑配置凭证 )} diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx index b7408a4..6cf3313 100644 --- a/src/components/VideoPlayer.tsx +++ b/src/components/VideoPlayer.tsx @@ -92,8 +92,11 @@ const LONG_PRESS_MS = 400; const FAST_RATE = 2; /** 默认倍速。 */ const NORMAL_RATE = 1; +/** ArtPlayer 内部播放失败自动重连次数。 */ +const ARTPLAYER_RECONNECT_TIME_MAX = 3; Artplayer.FAST_FORWARD_VALUE = FAST_RATE; +Artplayer.RECONNECT_TIME_MAX = ARTPLAYER_RECONNECT_TIME_MAX; const DEFAULT_SETTINGS: PlayerSettings = { volume: 0.7, diff --git a/src/styles/video-card.css b/src/styles/video-card.css index 24840f0..063ecd9 100644 --- a/src/styles/video-card.css +++ b/src/styles/video-card.css @@ -492,10 +492,15 @@ } .skeleton-card { + --skeleton-card-bg: var(--bg-surface); + --skeleton-card-border: var(--border-subtle); + --skeleton-shimmer-base: rgba(255, 255, 255, 0.03); + --skeleton-shimmer-highlight: rgba(255, 255, 255, 0.08); + position: relative; aspect-ratio: 16 / 12.5; - background: var(--bg-surface); - border: 1px solid var(--border-subtle); + background: var(--skeleton-card-bg); + border: 1px solid var(--skeleton-card-border); border-radius: var(--radius-md); padding: 8px; box-sizing: border-box; @@ -504,6 +509,18 @@ flex-direction: column; } +:root[data-theme="pink"] .skeleton-card { + --skeleton-card-border: rgba(255, 91, 138, 0.18); + --skeleton-shimmer-base: rgba(255, 91, 138, 0.12); + --skeleton-shimmer-highlight: rgba(255, 91, 138, 0.26); +} + +:root[data-theme="sky"] .skeleton-card { + --skeleton-card-border: rgba(60, 100, 170, 0.18); + --skeleton-shimmer-base: rgba(60, 100, 170, 0.13); + --skeleton-shimmer-highlight: rgba(60, 100, 170, 0.26); +} + /* Skeleton image thumbnail area */ .skeleton-card::before { content: ""; @@ -513,9 +530,9 @@ border-radius: var(--radius-sm); background: linear-gradient( 90deg, - rgba(255, 255, 255, 0.03) 25%, - rgba(255, 255, 255, 0.08) 50%, - rgba(255, 255, 255, 0.03) 75% + var(--skeleton-shimmer-base) 25%, + var(--skeleton-shimmer-highlight) 50%, + var(--skeleton-shimmer-base) 75% ); background-size: 200% 100%; animation: skeleton-shimmer 1.6s ease-in-out infinite; @@ -529,8 +546,8 @@ width: 100%; height: 28px; background: - linear-gradient(90deg, rgba(255, 255, 255, 0.03) 25%, rgba(255, 255, 255, 0.08) 50%, rgba(255, 255, 255, 0.03) 75%) 0 0 / 70% 12px no-repeat, - linear-gradient(90deg, rgba(255, 255, 255, 0.03) 25%, rgba(255, 255, 255, 0.08) 50%, rgba(255, 255, 255, 0.03) 75%) 0 20px / 42% 8px no-repeat; + linear-gradient(90deg, var(--skeleton-shimmer-base) 25%, var(--skeleton-shimmer-highlight) 50%, var(--skeleton-shimmer-base) 75%) 0 0 / 70% 12px no-repeat, + linear-gradient(90deg, var(--skeleton-shimmer-base) 25%, var(--skeleton-shimmer-highlight) 50%, var(--skeleton-shimmer-base) 75%) 0 20px / 42% 8px no-repeat; background-size: 200% 100%; animation: skeleton-shimmer 1.6s ease-in-out infinite; } diff --git a/tests/adminDriveForm.test.ts b/tests/adminDriveForm.test.ts index 91667e6..fabf70f 100644 --- a/tests/adminDriveForm.test.ts +++ b/tests/adminDriveForm.test.ts @@ -304,6 +304,21 @@ test("drive management exposes stop task controls", () => { assert.match(drivesPageSource, /停止所有网盘任务/); }); +test("drive detail primary actions use the rescan button color", () => { + assert.match( + drivesPageSource, + /className="admin-btn is-primary"\s+onClick=\{\(\) => handleRescan\(d\)\}/ + ); + assert.match( + drivesPageSource, + /className="admin-btn is-primary"\s+onClick=\{\(\) => handleStopDriveTasks\(d\)\}/ + ); + assert.match( + drivesPageSource, + /className="admin-btn is-primary"\s+onClick=\{\(\) => openEdit\(d\)\}/ + ); +}); + test("drive rescan reports busy storage tasks instead of queueing duplicates", () => { assert.match(apiSource, /accepted:\s*boolean;\s*message\?:\s*string/); assert.match(apiSource, /scanGenerationStatus\?: DriveGenerationStatus/); diff --git a/tests/videoGridSkeleton.test.ts b/tests/videoGridSkeleton.test.ts new file mode 100644 index 0000000..e55bfc2 --- /dev/null +++ b/tests/videoGridSkeleton.test.ts @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; + +const videoCardCss = readFileSync( + new URL("../src/styles/video-card.css", import.meta.url), + "utf8" +); + +function ruleBody(css: string, selector: string): string { + const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const match = css.match(new RegExp(`${escapedSelector}\\s*\\{([^}]*)\\}`)); + assert.ok(match, `Expected CSS rule for ${selector}`); + return match[1]; +} + +test("home video skeleton uses theme-aware non-white shimmer colors", () => { + const skeleton = ruleBody(videoCardCss, ".skeleton-card"); + const pink = ruleBody(videoCardCss, ':root[data-theme="pink"] .skeleton-card'); + const sky = ruleBody(videoCardCss, ':root[data-theme="sky"] .skeleton-card'); + const thumb = ruleBody(videoCardCss, ".skeleton-card::before"); + const text = ruleBody(videoCardCss, ".skeleton-card::after"); + + assert.match(skeleton, /--skeleton-shimmer-base\s*:/); + assert.match(skeleton, /--skeleton-shimmer-highlight\s*:/); + assert.match(thumb, /var\(--skeleton-shimmer-base\)/); + assert.match(thumb, /var\(--skeleton-shimmer-highlight\)/); + assert.match(text, /var\(--skeleton-shimmer-base\)/); + assert.match(text, /var\(--skeleton-shimmer-highlight\)/); + + assert.match(pink, /--skeleton-shimmer-base\s*:\s*rgba\(255,\s*91,\s*138,\s*0\.12\)/); + assert.match(pink, /--skeleton-shimmer-highlight\s*:\s*rgba\(255,\s*91,\s*138,\s*0\.26\)/); + assert.match(sky, /--skeleton-shimmer-base\s*:\s*rgba\(60,\s*100,\s*170,\s*0\.13\)/); + assert.match(sky, /--skeleton-shimmer-highlight\s*:\s*rgba\(60,\s*100,\s*170,\s*0\.26\)/); +}); diff --git a/tests/videoPlayerPoster.test.ts b/tests/videoPlayerPoster.test.ts index 60160e3..35a1bd1 100644 --- a/tests/videoPlayerPoster.test.ts +++ b/tests/videoPlayerPoster.test.ts @@ -74,6 +74,14 @@ test("detail player exposes a non-persistent loop switch in ArtPlayer settings", assert.match(playerSource, /item\.tooltip = next \? "开" : "关"/); }); +test("detail player limits ArtPlayer automatic reconnect attempts", () => { + assert.match(playerSource, /const ARTPLAYER_RECONNECT_TIME_MAX = 3;/); + assert.match( + playerSource, + /Artplayer\.RECONNECT_TIME_MAX = ARTPLAYER_RECONNECT_TIME_MAX;/ + ); +}); + test("detail loading skeleton matches current desktop video page layout", () => { assert.match(detailPageSource, /className="vd-layout vd-skeleton"/); assert.match(detailPageSource, /className="vd-skeleton__summary"/);