import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react"; import type { ClientContext, SnapshotStore } from "@deepseek-ai/dsh-client-runtime/client"; import type {} from "@deepseek-ai/dsh-client-ui-conversation/client"; import type {} from "@deepseek-ai/dsh-client-ui-settings/client"; import type {} from "@deepseek-ai/dsh-client-ui-model-selection/client"; import styles from "./skin.css"; import { PREVIEW_MAX_FRAME, frameForEffort, indicatorLabel, nearestEffortIndex, paletteForFrame, portraitBlendForLevel, selectedEffortIndex, type EffortLike, } from "./logic"; const PACKAGE_ID = "dsh-client-liang-intensity-skin"; const LOCALE_NAMESPACE = "liang.skin"; const ASSET_PREFIX = `/plugins/${PACKAGE_ID}/assets`; const VIDEO_DURATION = 8.033; const BIND_EFFORT_KEY = "dsh-liang-intensity-skin.bind-effort"; const PORTRAIT_ANCHORS = [ { level: 0, file: "stage-00.png" }, { level: 1, file: "level-01.png" }, { level: 3, file: "level-03.png" }, { level: 4, file: "level-04.png" }, { level: 6, file: "stage-06.png" }, { level: 7, file: "level-07.png" }, { level: 9, file: "level-09.png" }, { level: 10, file: "level-10.png" }, { level: 12, file: "stage-12.png" }, { level: 13, file: "level-13.png" }, { level: 14, file: "level-14.png" }, { level: 15, file: "bridge-15.png" }, { level: 16, file: "level-16.png" }, { level: 17, file: "level-17.png" }, { level: 18, file: "stage-18.png" }, { level: 19, file: "level-19.png" }, { level: 21, file: "level-21.png" }, { level: 22, file: "level-22.png" }, { level: 24, file: "stage-24.png" }, { level: 25, file: "level-25.png" }, { level: 27, file: "bridge-27.png" }, { level: 28, file: "level-28.png" }, { level: 29, file: "level-29.png" }, { level: 30, file: "stage-30.png" }, ] as const; const ANCHOR_LEVELS = PORTRAIT_ANCHORS.map((anchor) => anchor.level); interface SkinSettings { enabled: boolean; bindEffort: boolean; } interface PreferenceStore { getSnapshot(): SkinSettings; subscribe(listener: () => void): () => void; set(enabled: boolean): Promise; setBindEffort(enabled: boolean): Promise; dispose(): void; } type NativeThemeId = "light" | "dark"; interface ThemeService { getTheme(): { preference: string }; setTheme(id: NativeThemeId | "system"): void; } interface ModelSelection { provider: string; model: string; reasoningEffort?: string; } interface CatalogModel { id: string; reasoning?: { efforts: EffortLike[]; defaultEffort?: string; }; } interface ModelDirectoryState { current: ModelSelection | null; groups: readonly { id: string; models: CatalogModel[] }[]; status: "idle" | "loading" | "ready" | "selecting" | "error"; error: string | null; } interface ModelDirectory { store: SnapshotStore; load(): Promise; select(selection: ModelSelection): Promise; } interface SliderProps { directory: SnapshotStore; load: () => void; select: (selection: ModelSelection) => Promise; presenter: SkinPresenter; scope: PreferenceStore; } interface SettingsRowProps { scope: PreferenceStore; presenter: SkinPresenter; theme: ThemeService; subscribeTheme: (listener: () => void) => () => void; t: (key: string) => string; } const cssVariables = [ "--liang-strength", "--liang-page", "--liang-bg-base", "--liang-layer-1", "--liang-layer-2", "--liang-layer-3", "--liang-sidebar", "--liang-ink", "--liang-secondary", "--liang-tertiary", "--liang-border", "--liang-accent", "--liang-accent-hover", "--liang-hover", "--liang-portrait-opacity", ] as const; class SkinPresenter { private readonly scope: PreferenceStore; private readonly theme: ThemeService; private readonly root: HTMLDivElement; private readonly video: HTMLVideoElement; private readonly poster: HTMLImageElement; private readonly frames: HTMLImageElement[]; private readonly preloads: HTMLImageElement[]; private enabled = true; // Default to the max frame so the first paint after load is the dark shell; // starting at 0 flashed the light palette before the directory resolved. private frame = PREVIEW_MAX_FRAME; private pendingFrame = PREVIEW_MAX_FRAME; private pendingMode: "drag" | "settle" = "settle"; private activeFrame = 0; private pendingSource: string | null = null; private raf = 0; private disposed = false; private unsubscribe: () => void; constructor(scope: PreferenceStore, theme: ThemeService) { this.scope = scope; this.theme = theme; this.root = document.createElement("div"); this.root.className = "liang-skin-backdrop"; this.root.dataset.plugin = PACKAGE_ID; this.root.dataset.media = "sequence"; this.root.dataset.crossfade = "slow"; this.root.setAttribute("aria-hidden", "true"); this.video = document.createElement("video"); this.video.muted = true; this.video.playsInline = true; this.video.preload = "auto"; // A video poster stays painted while a paused video is scrubbed in some // browsers, which would make every effort show the same portrait. Keep the // separate image elements as the true load-error fallback instead. const webm = document.createElement("source"); webm.src = `${ASSET_PREFIX}/liang-evolution.webm`; webm.type = "video/webm"; const mp4 = document.createElement("source"); mp4.src = `${ASSET_PREFIX}/liang-evolution.mp4`; mp4.type = "video/mp4"; this.video.append(webm, mp4); this.poster = document.createElement("img"); this.poster.className = "liang-skin-poster"; this.poster.src = `${ASSET_PREFIX}/liang-poster.png`; this.poster.alt = ""; // Two stacked crossfade layers: the standby layer loads the next portrait // while the active one stays painted, then an opacity/scale transition // hands over — no hard src swap, no white flash. this.frames = []; for (let index = 0; index < 2; index += 1) { const image = document.createElement("img"); image.className = "liang-skin-sequence-frame"; image.alt = ""; image.draggable = false; image.decoding = "async"; image.addEventListener("error", this.handleSequenceError); image.addEventListener("load", () => this.handleFrameLoaded(image)); this.frames.push(image); } this.preloads = PORTRAIT_ANCHORS.map(({ file }) => { const image = new Image(); image.decoding = "async"; image.src = `${ASSET_PREFIX}/portrait-source-v2/${file}`; return image; }); this.root.append(this.video, this.frames[0], this.frames[1], this.poster); document.body.prepend(this.root); this.video.addEventListener("loadedmetadata", this.handleMetadata); this.video.addEventListener("error", this.handleVideoError); this.poster.addEventListener("error", this.handlePosterError); this.video.load(); this.unsubscribe = scope.subscribe(() => this.syncSettings()); this.syncSettings(); } private readonly handleMetadata = () => { this.seek(this.frame); }; private readonly handleVideoError = () => { this.root.dataset.media = "poster"; }; private readonly handleSequenceError = () => { this.root.dataset.media = "video"; }; private readonly handlePosterError = () => { this.root.dataset.media = "color"; }; private syncSettings() { this.setEnabled(this.scope.getSnapshot().enabled); } isEnabled() { return this.enabled; } getFrame() { return this.frame; } setEnabled(enabled: boolean) { this.enabled = enabled; if (enabled) { if (!this.root.isConnected) document.body.prepend(this.root); document.body.dataset.liangSkin = "on"; this.applyFrame(); } else { this.root.remove(); delete document.body.dataset.liangSkin; delete document.body.dataset.liangStage; for (const name of cssVariables) document.body.style.removeProperty(name); } } setFrame(frame: number, mode: "drag" | "settle" = "drag") { this.pendingFrame = Math.min(PREVIEW_MAX_FRAME, Math.max(0, Math.round(frame))); this.frame = this.pendingFrame; this.pendingMode = mode; if (!this.enabled) return; if (this.raf !== 0) return; this.raf = requestAnimationFrame(() => { this.raf = 0; this.frame = this.pendingFrame; this.applyFrame(); }); } private applyFrame() { const palette = paletteForFrame(this.frame); const body = document.body; this.syncNativeTheme(palette.stage === 5 ? "dark" : "light"); body.dataset.liangStage = String(palette.stage); body.style.setProperty("--liang-strength", String(palette.strength)); body.style.setProperty("--liang-page", palette.page); body.style.setProperty("--liang-bg-base", palette.base); body.style.setProperty("--liang-layer-1", palette.layer1); body.style.setProperty("--liang-layer-2", palette.layer2); body.style.setProperty("--liang-layer-3", palette.layer3); body.style.setProperty("--liang-sidebar", palette.sidebar); body.style.setProperty("--liang-ink", palette.ink); body.style.setProperty("--liang-secondary", palette.secondary); body.style.setProperty("--liang-tertiary", palette.tertiary); body.style.setProperty("--liang-border", palette.border); body.style.setProperty("--liang-accent", palette.accent); body.style.setProperty("--liang-accent-hover", palette.accentHover); body.style.setProperty("--liang-hover", palette.hover); body.style.setProperty("--liang-portrait-opacity", palette.portraitOpacity); this.root.dataset.crossfade = this.pendingMode === "settle" ? "slow" : "fast"; this.updatePortrait(palette.level); // Seeking a hidden