From 13d60b3bd96755c0486dc3a5f3b1911092763293 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:00:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E9=9B=86=E5=9B=BE=E7=89=87=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E8=90=BD=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/useManualScrapeDialogState.ts | 3 +- web/src/hooks/useEpisodeArtworkPreference.ts | 53 +++++++++++++++++++ web/src/pages/LibrariesPage.tsx | 3 +- web/src/pages/useLibraryAdminActions.tsx | 3 +- web/src/pages/useMediaDetailPageState.ts | 3 +- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 web/src/hooks/useEpisodeArtworkPreference.ts diff --git a/web/src/components/useManualScrapeDialogState.ts b/web/src/components/useManualScrapeDialogState.ts index 3e07f1f..128d7a0 100644 --- a/web/src/components/useManualScrapeDialogState.ts +++ b/web/src/components/useManualScrapeDialogState.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState, type Dispatch, type SetState import toast from 'react-hot-toast' import { mediaAPI, type ManualScrapeCandidate } from '../api/library' +import { useEpisodeArtworkPreference } from '../hooks/useEpisodeArtworkPreference' import type { Media } from '../types' import { candidateKey, @@ -103,7 +104,7 @@ export function useManualScrapeDialogState({ function useManualScrapeFormState() { const [query, setQuery] = useState('') const [selectedProviders, setSelectedProviders] = useState([]) - const [includeEpisodeArtwork, setIncludeEpisodeArtwork] = useState(false) + const [includeEpisodeArtwork, setIncludeEpisodeArtwork] = useEpisodeArtworkPreference() const [searching, setSearching] = useState(false) const [applyingKey, setApplyingKey] = useState('') const [items, setItems] = useState([]) diff --git a/web/src/hooks/useEpisodeArtworkPreference.ts b/web/src/hooks/useEpisodeArtworkPreference.ts new file mode 100644 index 0000000..e267807 --- /dev/null +++ b/web/src/hooks/useEpisodeArtworkPreference.ts @@ -0,0 +1,53 @@ +import { useCallback, useEffect, useRef, useState, type Dispatch, type SetStateAction } from 'react' +import toast from 'react-hot-toast' + +import { adminAPI } from '../api/admin' + +const SETTING_KEY = 'scrape.episode_images' + +// This is an instance-wide scraper preference. The existing admin settings +// endpoint stores it in the Setting table, so it follows the server instead +// of a particular browser. +export function useEpisodeArtworkPreference(): [boolean, Dispatch>] { + const [enabled, setEnabledState] = useState(false) + const enabledRef = useRef(false) + const changedLocally = useRef(false) + const saveQueue = useRef(Promise.resolve()) + + useEffect(() => { + let active = true + adminAPI.listSettings() + .then((settings) => { + if (!active || changedLocally.current) return + const value = settings.find((setting) => setting.key === SETTING_KEY)?.value + const next = value === 'true' || value === '1' + enabledRef.current = next + setEnabledState(next) + }) + .catch(() => { + if (active) { + toast.error('读取每集图片设置失败') + } + }) + return () => { + active = false + } + }, []) + + const setEnabled = useCallback((nextValue: SetStateAction) => { + const next = typeof nextValue === 'function' ? nextValue(enabledRef.current) : nextValue + if (enabledRef.current === next) return + changedLocally.current = true + enabledRef.current = next + setEnabledState(next) + + saveQueue.current = saveQueue.current + .catch(() => undefined) + .then(() => adminAPI.updateSetting(SETTING_KEY, String(next))) + .catch(() => { + toast.error('保存每集图片设置失败') + }) + }, []) + + return [enabled, setEnabled] +} diff --git a/web/src/pages/LibrariesPage.tsx b/web/src/pages/LibrariesPage.tsx index 228205b..e8f6e51 100644 --- a/web/src/pages/LibrariesPage.tsx +++ b/web/src/pages/LibrariesPage.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { libraryAPI } from '../api/library' import { toolsAPI } from '../api/tools' import { openManageLibrariesDialog } from '../components/manageLibrariesDialog' +import { useEpisodeArtworkPreference } from '../hooks/useEpisodeArtworkPreference' import { usePinnedLibraries } from '../hooks/usePinnedLibraries' import { LibrariesContent, @@ -21,7 +22,7 @@ export function LibrariesPage() { const { pinnedIds, loading: pinnedLoading, togglePin } = usePinnedLibraries() const [loading, setLoading] = useState(true) const [repairing, setRepairing] = useState(false) - const [repairEpisodeArtwork, setRepairEpisodeArtwork] = useState(false) + const [repairEpisodeArtwork, setRepairEpisodeArtwork] = useEpisodeArtworkPreference() const [repairMsg, setRepairMsg] = useState('') const fetchedLibIdsRef = useRef>(new Set()) diff --git a/web/src/pages/useLibraryAdminActions.tsx b/web/src/pages/useLibraryAdminActions.tsx index fb8e66d..475f3a4 100644 --- a/web/src/pages/useLibraryAdminActions.tsx +++ b/web/src/pages/useLibraryAdminActions.tsx @@ -6,6 +6,7 @@ import { libraryAPI } from '../api/library' import { mediaAPI } from '../api/library' import { toolsAPI } from '../api/tools' import { confirmAction, confirmActionResult } from '../components/confirmAction' +import { useEpisodeArtworkPreference } from '../hooks/useEpisodeArtworkPreference' import type { Library, Media } from '../types' import { seriesTitle, type SeriesCard } from '../utils/groupSeries' import { LibraryMovieActions } from './LibraryMovieActions' @@ -33,7 +34,7 @@ export function useLibraryAdminActions({ setManualMovie, }: UseLibraryAdminActionsOptions) { const [scraping, setScraping] = useState(false) - const [scrapeEpisodeArtwork, setScrapeEpisodeArtwork] = useState(false) + const [scrapeEpisodeArtwork, setScrapeEpisodeArtwork] = useEpisodeArtworkPreference() const [repairing, setRepairing] = useState(false) const [seriesToolBusy, setSeriesToolBusy] = useState('') const [movieToolBusy, setMovieToolBusy] = useState('') diff --git a/web/src/pages/useMediaDetailPageState.ts b/web/src/pages/useMediaDetailPageState.ts index 0538b81..8e9d796 100644 --- a/web/src/pages/useMediaDetailPageState.ts +++ b/web/src/pages/useMediaDetailPageState.ts @@ -6,6 +6,7 @@ import { api } from '../api/client' import { mediaAPI } from '../api/library' import { playbackAPI } from '../api/playback' import { confirmActionResult } from '../components/confirmAction' +import { useEpisodeArtworkPreference } from '../hooks/useEpisodeArtworkPreference' import type { Media } from '../types' import { mediaLibraryBackTarget } from './MediaDetailPageModel' @@ -42,7 +43,7 @@ export function useMediaDetailPageState({ id, navigate }: MediaDetailPageStatePa const [manualScrapeOpen, setManualScrapeOpen] = useState(false) const [metadataEditOpen, setMetadataEditOpen] = useState(false) const [organizeOpen, setOrganizeOpen] = useState(false) - const [scrapeEpisodeArtwork, setScrapeEpisodeArtwork] = useState(false) + const [scrapeEpisodeArtwork, setScrapeEpisodeArtwork] = useEpisodeArtworkPreference() const refresh = useMediaDetailRefresh({ id,