import { useEffect, useId, useState, type ReactNode } from "react"; import type { LibraryPlaybackPreference, Profile, UserLibrary } from "@/api/types"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { useCurrentProfile } from "@/hooks/useCurrentProfile"; import { useDeleteLibraryPlaybackPreference, useLibraryPlaybackPreferences, useSetLibraryPlaybackPreference, } from "@/hooks/queries/libraryPlaybackPreferences"; import { DISABLED_LIBRARY_IDS_SETTING_KEY, LIBRARY_ORDER_SETTING_KEY, applyLibraryOrder, parseDisabledLibraryIDs, parseLibraryOrder, serializeDisabledLibraryIDs, serializeLibraryOrder, useAvailableUserLibraries, } from "@/hooks/queries/libraries"; import { useSetting, useSetSetting } from "@/hooks/queries/settings"; import { buildInheritedLanguageLabel, buildInheritedShowForcedSubtitlesLabel, buildInheritedSubtitleLanguageLabel, buildInheritedSubtitleModeLabel, buildLibraryPlaybackRequest, buildLibraryPlaybackSummaryFromState, createLibraryPlaybackEditorState, getProfileDefaultForcedSubtitlesHint, getProfileDefaultLanguageHint, getProfileDefaultSubtitleLanguageHint, getProfileDefaultSubtitleModeHint, hasLibraryPlaybackOverride, INHERIT_VALUE, LANGUAGE_OPTIONS, type LibraryPlaybackEditorState, NONE_VALUE, ORIGINAL_LANGUAGE_LABEL, ORIGINAL_LANGUAGE_VALUE, SUBTITLE_MODE_OPTIONS, } from "./libraryPlaybackPreferences"; import { toast } from "sonner"; import { ChevronDown, ChevronRight, Eye, EyeOff, GripVertical, RotateCcw } from "lucide-react"; import { DndContext, DragOverlay, PointerSensor, KeyboardSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core"; import type { DragStartEvent, DragEndEvent } from "@dnd-kit/core"; import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove, } from "@dnd-kit/sortable"; import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; function sortLibrariesByOrder(libraries: UserLibrary[], ids: number[]) { const selected = new Set(ids); return libraries.filter((library) => selected.has(library.id)).map((library) => library.id); } function PlaybackField({ label, value, disabled, hint, onChange, children, }: { label: string; value: string; disabled?: boolean; hint?: string; onChange: (value: string) => void; children: ReactNode; }) { const controlId = useId(); return (
{hint &&

{hint}

}
); } function SortableLibraryCard({ id, children }: { id: number; children: React.ReactNode }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, }); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1, }; return (
{children}
); } function LibraryCard({ library, enabled, visibilityDisabled = false, preference, profile, onToggleVisibility, }: { library: UserLibrary; enabled: boolean; visibilityDisabled?: boolean; preference: LibraryPlaybackPreference | null; profile: Profile; onToggleVisibility: (checked: boolean) => void; }) { const controlId = useId(); const setPlaybackPreference = useSetLibraryPlaybackPreference(); const deletePlaybackPreference = useDeleteLibraryPlaybackPreference(); const [expanded, setExpanded] = useState(false); const [editorState, setEditorState] = useState(() => createLibraryPlaybackEditorState(preference), ); useEffect(() => { // Keep the inline editor aligned with cached mutations and profile switches. // eslint-disable-next-line react-hooks/set-state-in-effect setEditorState(createLibraryPlaybackEditorState(preference)); }, [preference, profile.id]); const playbackPending = setPlaybackPreference.isPending || deletePlaybackPreference.isPending; const summaryText = buildLibraryPlaybackSummaryFromState(editorState); const hasOverride = hasLibraryPlaybackOverride(editorState); function savePlaybackState( nextState: LibraryPlaybackEditorState, rollbackState: LibraryPlaybackEditorState, ) { setEditorState(nextState); const onError = () => { setEditorState(rollbackState); toast.error("Failed to update playback defaults"); }; if (!hasLibraryPlaybackOverride(nextState)) { deletePlaybackPreference.mutate(library.id, { onError, onSuccess: () => setExpanded(false), }); return; } setPlaybackPreference.mutate( { libraryId: library.id, body: buildLibraryPlaybackRequest(nextState), }, { onError }, ); } function handlePlaybackChange(field: keyof LibraryPlaybackEditorState, value: string) { const rollbackState = editorState; const nextState = { ...editorState, [field]: value }; savePlaybackState(nextState, rollbackState); } function handleReset() { savePlaybackState(createLibraryPlaybackEditorState(null), editorState); } return (
{library.name} {library.type} {hasOverride && ( Custom )}

{summaryText}

{expanded && (

Override your profile's playback defaults for this library. Changes save automatically.

handlePlaybackChange("audioLanguage", value)} > {buildInheritedLanguageLabel(profile.language)} {ORIGINAL_LANGUAGE_LABEL} {LANGUAGE_OPTIONS.map((language) => ( {language.label} ))} handlePlaybackChange("subtitleLanguage", value)} > {buildInheritedSubtitleLanguageLabel(profile.subtitle_language)} None {LANGUAGE_OPTIONS.map((language) => ( {language.label} ))} handlePlaybackChange("subtitleMode", value)} > {buildInheritedSubtitleModeLabel(profile.subtitle_mode)} {SUBTITLE_MODE_OPTIONS.map((mode) => ( {mode.label} ))} handlePlaybackChange("showForcedSubtitles", value)} > {buildInheritedShowForcedSubtitlesLabel(profile.show_forced_subtitles)} On Off
{hasOverride && (
)}
)}
); } export default function LibrarySettings() { const { data: libraries, isLoading: librariesLoading } = useAvailableUserLibraries(); const { data: disabledSetting, isLoading: disabledSettingLoading } = useSetting( DISABLED_LIBRARY_IDS_SETTING_KEY, ); const { data: orderSetting, isLoading: orderSettingLoading } = useSetting(LIBRARY_ORDER_SETTING_KEY); const { profile: currentProfile, isLoading: profileLoading } = useCurrentProfile(); const { data: playbackPreferences, isLoading: playbackPrefsLoading } = useLibraryPlaybackPreferences({ enabled: !!currentProfile, }); const setSetting = useSetSetting(); const [disabledLibraryIDs, setDisabledLibraryIDs] = useState([]); const [orderedLibraries, setOrderedLibraries] = useState([]); const [activeId, setActiveId] = useState(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), ); useEffect(() => { const parsedIDs = parseDisabledLibraryIDs(disabledSetting); // Keep the editable local order in sync with the latest saved setting. // This supports optimistic updates and rollback without changing behavior. // eslint-disable-next-line react-hooks/set-state-in-effect setDisabledLibraryIDs(libraries ? sortLibrariesByOrder(libraries, parsedIDs) : parsedIDs); }, [disabledSetting, libraries]); useEffect(() => { if (!libraries) return; const order = parseLibraryOrder(orderSetting); // eslint-disable-next-line react-hooks/set-state-in-effect setOrderedLibraries(order.length > 0 ? applyLibraryOrder(libraries, order) : libraries); }, [libraries, orderSetting]); function saveDisabledLibraries(nextDisabledLibraryIDs: number[], rollbackIDs: number[]) { setDisabledLibraryIDs(nextDisabledLibraryIDs); setSetting.mutate( { key: DISABLED_LIBRARY_IDS_SETTING_KEY, value: serializeDisabledLibraryIDs(nextDisabledLibraryIDs), }, { onError: () => { setDisabledLibraryIDs(rollbackIDs); toast.error("Failed to update library visibility"); }, }, ); } function handleLibraryToggle(libraryId: number, enabled: boolean) { if (!libraries) return; const current = disabledLibraryIDs; const next = enabled ? current.filter((id) => id !== libraryId) : sortLibrariesByOrder(libraries, [...current, libraryId]); saveDisabledLibraries(next, current); } function handleEnableAll() { saveDisabledLibraries([], disabledLibraryIDs); } function handleDisableAll() { if (!libraries) return; saveDisabledLibraries( libraries.map((library) => library.id), disabledLibraryIDs, ); } function handleDragStart(event: DragStartEvent) { setActiveId(event.active.id as number); } function handleDragEnd(event: DragEndEvent) { setActiveId(null); const { active, over } = event; if (!over || active.id === over.id) return; const oldIndex = orderedLibraries.findIndex((l) => l.id === active.id); const newIndex = orderedLibraries.findIndex((l) => l.id === over.id); if (oldIndex === -1 || newIndex === -1) return; const next = arrayMove(orderedLibraries, oldIndex, newIndex); const prev = orderedLibraries; setOrderedLibraries(next); setSetting.mutate( { key: LIBRARY_ORDER_SETTING_KEY, value: serializeLibraryOrder(next.map((l) => l.id)), }, { onError: () => { setOrderedLibraries(prev); toast.error("Failed to update library order"); }, }, ); } function handleDragCancel() { setActiveId(null); } const activeLibrary = activeId != null ? orderedLibraries.find((l) => l.id === activeId) : null; if ( librariesLoading || disabledSettingLoading || orderSettingLoading || profileLoading || playbackPrefsLoading ) { return
Loading libraries...
; } if (!libraries || libraries.length === 0) { return (
No libraries are available for this account right now.
); } if (!currentProfile) { return
Choose a profile to manage libraries.
; } const displayLibraries = orderedLibraries.length > 0 ? orderedLibraries : libraries; const visibleCount = libraries.filter( (library) => !disabledLibraryIDs.includes(library.id), ).length; const preferencesByLibraryId = new Map( (playbackPreferences ?? []).map((preference) => [preference.library_id, preference]), ); return (

Libraries

Toggle which libraries appear in your navigation and customize playback defaults per library.

{visibleCount} of {libraries.length}{" "} libraries visible for this profile.

l.id)} strategy={verticalListSortingStrategy} >
{displayLibraries.map((library) => { const enabled = !disabledLibraryIDs.includes(library.id); return ( handleLibraryToggle(library.id, checked)} /> ); })}
{activeLibrary ? (
{activeLibrary.name} {activeLibrary.type}
) : null}
); }