import { DetailedMeta } from "@/backend/metadata/getmeta"; import { MWMediaMeta } from "@/backend/metadata/types"; import { createContext, ReactNode, useCallback, useContext, useMemo, useRef, useState, } from "react"; import { VideoProgressStore } from "./store"; const FIVETEEN_MINUTES = 15 * 60; const FIVE_MINUTES = 5 * 60; function shouldSave(time: number, duration: number): boolean { const timeFromEnd = Math.max(0, duration - time); // short movie if (duration < FIVETEEN_MINUTES) { if (time < 5) return false; if (timeFromEnd < 60) return false; return true; } // long movie if (time < 30) return false; if (timeFromEnd < FIVE_MINUTES) return false; return true; } interface MediaItem { meta: MWMediaMeta; series?: { episode: number; season: number; }; } interface WatchedStoreItem { item: MediaItem; progress: number; percentage: number; } export interface WatchedStoreData { items: WatchedStoreItem[]; } interface WatchedStoreDataWrapper { updateProgress(media: MediaItem, progress: number, total: number): void; getFilteredWatched(): WatchedStoreItem[]; removeProgress(id: string): void; watched: WatchedStoreData; } const WatchedContext = createContext({ updateProgress: () => {}, getFilteredWatched: () => [], removeProgress: () => {}, watched: { items: [], }, }); WatchedContext.displayName = "WatchedContext"; export function WatchedContextProvider(props: { children: ReactNode }) { const watchedLocalstorage = VideoProgressStore.get(); const [watched, setWatchedReal] = useState( watchedLocalstorage as WatchedStoreData ); const setWatched = useCallback( (data: any) => { setWatchedReal((old) => { let newData = data; if (data.constructor === Function) { newData = data(old); } watchedLocalstorage.save(newData); return newData; }); }, [setWatchedReal, watchedLocalstorage] ); const contextValue = useMemo( () => ({ removeProgress(id: string) { setWatched((data: WatchedStoreData) => { const newData = { ...data }; newData.items = newData.items.filter((v) => v.item.meta.id !== id); return newData; }); }, updateProgress(media: MediaItem, progress: number, total: number): void { // TODO series support setWatched((data: WatchedStoreData) => { const newData = { ...data }; let item = newData.items.find( (v) => v.item.meta.id === media.meta.id ); if (!item) { item = { item: { ...media, meta: { ...media.meta }, series: media.series ? { ...media.series } : undefined, }, progress: 0, percentage: 0, }; newData.items.push(item); } // update actual item item.progress = progress; item.percentage = Math.round((progress / total) * 100); // remove item if shouldnt save if (!shouldSave(progress, total)) { newData.items = data.items.filter( (v) => v.item.meta.id !== media.meta.id ); } return newData; }); }, getFilteredWatched() { let filtered = watched.items; // get highest episode number for every anime/season const highestEpisode: Record = {}; const highestWatchedItem: Record = {}; filtered = filtered.filter((item) => { if (item.item.series) { const key = item.item.meta.id; const current: [number, number] = [ item.item.series.episode, item.item.series.season, ]; let existing = highestEpisode[key]; if (!existing) { existing = current; highestEpisode[key] = current; highestWatchedItem[key] = item; } if ( current[0] > existing[0] || (current[0] === existing[0] && current[1] > existing[1]) ) { highestEpisode[key] = current; highestWatchedItem[key] = item; } return false; } return true; }); return [...filtered, ...Object.values(highestWatchedItem)]; }, watched, }), [watched, setWatched] ); return ( {props.children} ); } export function useWatchedContext() { return useContext(WatchedContext); } export function useWatchedItem(meta: DetailedMeta | null) { const { watched, updateProgress } = useContext(WatchedContext); const item = useMemo( () => watched.items.find((v) => meta && v.item.meta.id === meta?.meta.id), [watched, meta] ); const lastCommitedTime = useRef([0, 0]); const callback = useCallback( (progress: number, total: number) => { // TODO add series support const hasChanged = lastCommitedTime.current[0] !== progress || lastCommitedTime.current[1] !== total; if (meta && hasChanged) { lastCommitedTime.current = [progress, total]; updateProgress({ meta: meta.meta }, progress, total); } }, [meta, updateProgress] ); return { updateProgress: callback, watchedItem: item }; }