refactor(api): literal-union the closed-value string fields (R7 S3a·1)
Video.status, Playlist.kind/source (+ PlaylistDetail), DownloadJob.job_kind
were bare `string`; type them as named unions verified against the backend
(VALID_STATES={new,watched,hidden}; kind user|watch_later; source local|
youtube; job_kind download|edit). VideoStatus then propagates through the
optimistic-override map, onState, and the VideoCard/VirtualFeed/PlayerModal
props — tsc now rejects a status typo (e.g. a stale "in_progress", which is a
derived filter, never a stored status) at every call site.
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
||||
Youtube,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { api, HttpError, type FeedFilters, type Video } from "../lib/api";
|
||||
import { api, HttpError, type FeedFilters, type Video, type VideoStatus } from "../lib/api";
|
||||
import i18n from "../i18n";
|
||||
import { notify, resolveVideo } from "../lib/notifications";
|
||||
import { useDebounced } from "../lib/useDebounced";
|
||||
@@ -70,7 +70,7 @@ const VIEW_ICON: Record<FeedView, LucideIcon> = {
|
||||
// (which encode both). One entry per concept; a single arrow flips the direction.
|
||||
// "relevance" (full-text search ranking) and "shuffle" are directionless single-string sorts.
|
||||
|
||||
function matchesView(status: string, show: string): boolean {
|
||||
function matchesView(status: VideoStatus, show: string): boolean {
|
||||
switch (show) {
|
||||
case "hidden":
|
||||
return status === "hidden";
|
||||
@@ -126,7 +126,7 @@ export default function Feed({
|
||||
const sortKeys = channelScoped
|
||||
? SORT_KEYS.filter((k) => k !== "subscribers" && k !== "priority")
|
||||
: SORT_KEYS;
|
||||
const [overrides, setOverrides] = useState<Record<string, string>>({});
|
||||
const [overrides, setOverrides] = useState<Record<string, VideoStatus>>({});
|
||||
const [savedOverrides, setSavedOverrides] = useState<Record<string, boolean>>({});
|
||||
// The open player: which video and where to start (null = resume from saved position).
|
||||
const [activeVideo, setActiveVideo] = useState<{ video: Video; startAt: number | null } | null>(
|
||||
@@ -241,7 +241,7 @@ export default function Feed({
|
||||
const loadedRef = useRef<Video[]>([]);
|
||||
|
||||
const onState = useCallback(
|
||||
(id: string, status: string) => {
|
||||
(id: string, status: VideoStatus) => {
|
||||
setOverrides((o) => ({ ...o, [id]: status }));
|
||||
// Refetch once the server has the change so other views (e.g. Hidden) are in sync.
|
||||
// Announce the change only AFTER the server confirms it: a "Marked watched"/"Hidden"
|
||||
|
||||
Reference in New Issue
Block a user