diff --git a/src/index.tsx b/src/index.tsx index b30b7595..a49d556a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -10,12 +10,12 @@ import { ErrorBoundary } from "@/pages/errors/ErrorBoundary"; import App from "@/setup/App"; import { conf } from "@/setup/config"; import i18n from "@/setup/i18n"; - import "@/setup/ga"; import "@/setup/index.css"; +import { useLanguageStore } from "@/stores/language"; + import { initializeChromecast } from "./setup/chromecast"; -import { SettingsStore } from "./state/settings/store"; -import { initializeStores } from "./utils/storage"; +import { initializeOldStores } from "./stores/__old/migrations"; // initialize const key = @@ -29,8 +29,8 @@ registerSW({ }); const LazyLoadedApp = React.lazy(async () => { - await initializeStores(); - i18n.changeLanguage(SettingsStore.get().language ?? "en"); + await initializeOldStores(); + i18n.changeLanguage(useLanguageStore.getState().language); return { default: App, }; diff --git a/src/setup/App.tsx b/src/setup/App.tsx index c09c816e..2bca1e51 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -19,10 +19,8 @@ import { HomePage } from "@/pages/HomePage"; import { PlayerView } from "@/pages/PlayerView"; import { SettingsPage } from "@/pages/Settings"; import { Layout } from "@/setup/Layout"; -import { BookmarkContextProvider } from "@/state/bookmark"; -import { SettingsProvider } from "@/state/settings"; -import { WatchedContextProvider } from "@/state/watched"; import { useHistoryListener } from "@/stores/history"; +import { useLanguageListener } from "@/stores/language"; function LegacyUrlView({ children }: { children: ReactElement }) { const location = useLocation(); @@ -60,83 +58,66 @@ function QuickSearch() { function App() { useHistoryListener(); useOnlineListener(); + useLanguageListener(); return ( - - - - - - {/* functional routes */} - - - - - - - - {({ match }) => { - if (match?.params.query) - return ( - - ); - return ; - }} - + + + {/* functional routes */} + + + + + + + + {({ match }) => { + if (match?.params.query) + return ( + + ); + return ; + }} + - {/* pages */} - - - - - - - - + {/* pages */} + + + + + + + + - {/* Settings page */} - + {/* Settings page */} + - {/* admin routes */} - + {/* admin routes */} + - {/* other */} - import("@/pages/DeveloperPage"))} - /> - import("@/pages/developer/VideoTesterView") - )} - /> - {/* developer routes that can abuse workers are disabled in production */} - {process.env.NODE_ENV === "development" ? ( - import("@/pages/developer/TestView"))} - /> - ) : null} - - - - - - + {/* other */} + import("@/pages/DeveloperPage"))} + /> + import("@/pages/developer/VideoTesterView"))} + /> + {/* developer routes that can abuse workers are disabled in production */} + {process.env.NODE_ENV === "development" ? ( + import("@/pages/developer/TestView"))} + /> + ) : null} + + + ); } diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx deleted file mode 100644 index 692d4e76..00000000 --- a/src/state/bookmark/context.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { ReactNode, createContext, useContext, useMemo } from "react"; - -import { MWMediaMeta } from "@/backend/metadata/types/mw"; -import { useStore } from "@/utils/storage"; - -import { BookmarkStore } from "./store"; -import { BookmarkStoreData } from "./types"; - -interface BookmarkStoreDataWrapper { - setItemBookmark(media: MWMediaMeta, bookedmarked: boolean): void; - getFilteredBookmarks(): MWMediaMeta[]; - bookmarkStore: BookmarkStoreData; -} - -const BookmarkedContext = createContext({ - setItemBookmark: () => {}, - getFilteredBookmarks: () => [], - bookmarkStore: { - bookmarks: [], - }, -}); - -function getBookmarkIndexFromMedia( - bookmarks: MWMediaMeta[], - media: MWMediaMeta -): number { - const a = bookmarks.findIndex((v) => v.id === media.id); - return a; -} - -export function BookmarkContextProvider(props: { children: ReactNode }) { - const [bookmarkStorage, setBookmarked] = useStore(BookmarkStore); - - const contextValue = useMemo( - () => ({ - setItemBookmark(media: MWMediaMeta, bookmarked: boolean) { - setBookmarked((data: BookmarkStoreData): BookmarkStoreData => { - let bookmarks = [...data.bookmarks]; - bookmarks = bookmarks.filter((v) => v.id !== media.id); - if (bookmarked) bookmarks.push({ ...media }); - return { - bookmarks, - }; - }); - }, - getFilteredBookmarks() { - return [...bookmarkStorage.bookmarks]; - }, - bookmarkStore: bookmarkStorage, - }), - [bookmarkStorage, setBookmarked] - ); - - return ( - - {props.children} - - ); -} - -export function useBookmarkContext() { - return useContext(BookmarkedContext); -} - -export function getIfBookmarkedFromPortable( - bookmarks: MWMediaMeta[], - media: MWMediaMeta -): boolean { - const bookmarked = getBookmarkIndexFromMedia(bookmarks, media); - return bookmarked !== -1; -} diff --git a/src/state/bookmark/index.ts b/src/state/bookmark/index.ts deleted file mode 100644 index 2edd280c..00000000 --- a/src/state/bookmark/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./context"; diff --git a/src/state/settings/context.tsx b/src/state/settings/context.tsx deleted file mode 100644 index 4810f925..00000000 --- a/src/state/settings/context.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { ReactNode, createContext, useContext, useMemo } from "react"; - -import { LangCode } from "@/setup/iso6391"; -import { useStore } from "@/utils/storage"; - -import { SettingsStore } from "./store"; -import { MWSettingsData } from "./types"; - -interface MWSettingsDataSetters { - setLanguage(language: LangCode): void; - setCaptionLanguage(language: LangCode): void; - setCaptionDelay(delay: number): void; - setCaptionColor(color: string): void; - setCaptionFontSize(size: number): void; - setCaptionBackgroundColor(backgroundColor: number): void; -} -type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters; -const SettingsContext = createContext(null as any); -export function SettingsProvider(props: { children: ReactNode }) { - function enforceRange(min: number, value: number, max: number) { - return Math.max(min, Math.min(value, max)); - } - const [settings, setSettings] = useStore(SettingsStore); - const context: MWSettingsDataWrapper = useMemo(() => { - const settingsContext: MWSettingsDataWrapper = { - ...settings, - setLanguage(language) { - setSettings((oldSettings) => { - return { - ...oldSettings, - language, - }; - }); - }, - setCaptionLanguage(language) { - setSettings((oldSettings) => { - const captionSettings = oldSettings.captionSettings; - captionSettings.language = language; - const newSettings = oldSettings; - return newSettings; - }); - }, - setCaptionDelay(delay: number) { - setSettings((oldSettings) => { - const captionSettings = oldSettings.captionSettings; - captionSettings.delay = enforceRange(-10, delay, 10); - const newSettings = oldSettings; - return newSettings; - }); - }, - setCaptionColor(color) { - setSettings((oldSettings) => { - const style = oldSettings.captionSettings.style; - style.color = color; - const newSettings = oldSettings; - return newSettings; - }); - }, - setCaptionFontSize(size) { - setSettings((oldSettings) => { - const style = oldSettings.captionSettings.style; - style.fontSize = enforceRange(10, size, 60); - const newSettings = oldSettings; - return newSettings; - }); - }, - setCaptionBackgroundColor(backgroundColor) { - setSettings((oldSettings) => { - const style = oldSettings.captionSettings.style; - style.backgroundColor = `${style.backgroundColor.substring( - 0, - 7 - )}${backgroundColor.toString(16).padStart(2, "0")}`; - const newSettings = oldSettings; - return newSettings; - }); - }, - }; - return settingsContext; - }, [settings, setSettings]); - return ( - - {props.children} - - ); -} - -export function useSettings() { - return useContext(SettingsContext); -} - -export default SettingsContext; diff --git a/src/state/settings/index.ts b/src/state/settings/index.ts deleted file mode 100644 index 2edd280c..00000000 --- a/src/state/settings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./context"; diff --git a/src/state/settings/store.ts b/src/state/settings/store.ts deleted file mode 100644 index 19d84e15..00000000 --- a/src/state/settings/store.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createVersionedStore } from "@/utils/storage"; - -import { MWSettingsData, MWSettingsDataV1 } from "./types"; - -export const SettingsStore = createVersionedStore() - .setKey("mw-settings") - .addVersion({ - version: 0, - create(): MWSettingsDataV1 { - return { - language: "en", - captionSettings: { - delay: 0, - style: { - color: "#ffffff", - fontSize: 25, - backgroundColor: "#00000096", - }, - }, - }; - }, - migrate(data: MWSettingsDataV1): MWSettingsData { - return { - language: data.language, - captionSettings: { - language: "none", - ...data.captionSettings, - }, - }; - }, - }) - .addVersion({ - version: 1, - create(): MWSettingsData { - return { - language: "en", - captionSettings: { - delay: 0, - language: "none", - style: { - color: "#ffffff", - fontSize: 25, - backgroundColor: "#00000096", - }, - }, - }; - }, - }) - .build(); diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx deleted file mode 100644 index 661b0ed3..00000000 --- a/src/state/watched/context.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { - ReactNode, - createContext, - useCallback, - useContext, - useMemo, - useRef, -} from "react"; - -import { DetailedMeta } from "@/backend/metadata/getmeta"; -import { MWMediaType } from "@/backend/metadata/types/mw"; -import { useStore } from "@/utils/storage"; - -import { VideoProgressStore } from "./store"; -import { StoreMediaItem, WatchedStoreData, WatchedStoreItem } from "./types"; - -const FIVETEEN_MINUTES = 15 * 60; -const FIVE_MINUTES = 5 * 60; - -function shouldSave( - time: number, - duration: number, - isSeries: boolean -): 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 && !isSeries) return false; - return true; -} - -interface WatchedStoreDataWrapper { - updateProgress(media: StoreMediaItem, progress: number, total: number): void; - getFilteredWatched(): WatchedStoreItem[]; - removeProgress(id: string): void; - watched: WatchedStoreData; -} - -const WatchedContext = createContext({ - updateProgress: () => {}, - getFilteredWatched: () => [], - removeProgress: () => {}, - watched: { - items: [], - }, -}); -WatchedContext.displayName = "WatchedContext"; - -function isSameEpisode(media: StoreMediaItem, v: StoreMediaItem) { - return ( - media.meta.id === v.meta.id && - (!media.series || - (media.series.seasonId === v.series?.seasonId && - media.series.episodeId === v.series?.episodeId)) - ); -} - -export function WatchedContextProvider(props: { children: ReactNode }) { - const [watched, setWatched] = useStore(VideoProgressStore); - - 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: StoreMediaItem, - progress: number, - total: number - ): void { - setWatched((data: WatchedStoreData) => { - const newData = { ...data }; - let item = newData.items.find((v) => isSameEpisode(media, v.item)); - if (!item) { - item = { - item: { - ...media, - meta: { ...media.meta }, - series: media.series ? { ...media.series } : undefined, - }, - progress: 0, - percentage: 0, - watchedAt: Date.now(), - }; - newData.items.push(item); - } - // update actual item - item.progress = progress; - item.percentage = Math.round((progress / total) * 100); - item.watchedAt = Date.now(); - - // remove item if shouldnt save - if (!shouldSave(progress, total, !!media.series)) { - newData.items = data.items.filter( - (v) => !isSameEpisode(v.item, media) - ); - } - - return newData; - }); - }, - getFilteredWatched() { - let filtered = watched.items; - - // get most recently watched for every single item - const alreadyFoundMedia: string[] = []; - filtered = filtered - .sort((a, b) => { - return b.watchedAt - a.watchedAt; - }) - .filter((item) => { - const mediaId = item.item.meta.id; - if (alreadyFoundMedia.includes(mediaId)) return false; - alreadyFoundMedia.push(mediaId); - return true; - }); - return filtered; - }, - watched, - }), - [watched, setWatched] - ); - - return ( - - {props.children} - - ); -} - -export function useWatchedContext() { - return useContext(WatchedContext); -} - -function isSameEpisodeMeta( - media: StoreMediaItem, - mediaTwo: DetailedMeta | null, - episodeId?: string -) { - if (mediaTwo?.meta.type === MWMediaType.SERIES && episodeId) { - return isSameEpisode(media, { - meta: mediaTwo.meta, - series: { - season: 0, - episode: 0, - episodeId, - seasonId: mediaTwo.meta.seasonData.id, - }, - }); - } - if (!mediaTwo) return () => false; - return isSameEpisode(media, { meta: mediaTwo.meta }); -} - -export function useWatchedItem(meta: DetailedMeta | null, episodeId?: string) { - const { watched, updateProgress } = useContext(WatchedContext); - const item = useMemo( - () => watched.items.find((v) => isSameEpisodeMeta(v.item, meta, episodeId)), - [watched, meta, episodeId] - ); - const lastCommitedTime = useRef([0, 0]); - - const callback = useCallback( - (progress: number, total: number) => { - const hasChanged = - lastCommitedTime.current[0] !== progress || - lastCommitedTime.current[1] !== total; - if (meta && hasChanged) { - lastCommitedTime.current = [progress, total]; - const obj = { - meta: meta.meta, - series: - meta.meta.type === MWMediaType.SERIES && episodeId - ? { - seasonId: meta.meta.seasonData.id, - episodeId, - season: meta.meta.seasonData.number, - episode: - meta.meta.seasonData.episodes.find( - (ep) => ep.id === episodeId - )?.number || 0, - } - : undefined, - }; - updateProgress(obj, progress, total); - } - }, - [meta, updateProgress, episodeId] - ); - - return { updateProgress: callback, watchedItem: item }; -} diff --git a/src/state/watched/index.ts b/src/state/watched/index.ts deleted file mode 100644 index 2edd280c..00000000 --- a/src/state/watched/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./context"; diff --git a/src/state/bookmark/store.ts b/src/stores/__old/bookmark/store.ts similarity index 96% rename from src/state/bookmark/store.ts rename to src/stores/__old/bookmark/store.ts index 3d68afec..8a1a0f02 100644 --- a/src/state/bookmark/store.ts +++ b/src/stores/__old/bookmark/store.ts @@ -1,8 +1,8 @@ import { MWMediaType } from "@/backend/metadata/types/mw"; import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks"; -import { createVersionedStore } from "@/utils/storage"; import { BookmarkStoreData } from "./types"; +import { createVersionedStore } from "../migrations"; import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2"; import { migrateV2Bookmarks } from "../watched/migrations/v3"; diff --git a/src/state/bookmark/types.ts b/src/stores/__old/bookmark/types.ts similarity index 100% rename from src/state/bookmark/types.ts rename to src/stores/__old/bookmark/types.ts diff --git a/src/utils/storage.ts b/src/stores/__old/migrations.ts similarity index 90% rename from src/utils/storage.ts rename to src/stores/__old/migrations.ts index 83057d54..e79113ca 100644 --- a/src/utils/storage.ts +++ b/src/stores/__old/migrations.ts @@ -1,5 +1,3 @@ -import { useEffect, useState } from "react"; - interface StoreVersion { version: number; migrate?(data: A): any; @@ -28,7 +26,7 @@ interface InternalStoreData { const storeCallbacks: Record void)[]> = {}; const stores: Record, InternalStoreData]> = {}; -export async function initializeStores() { +export async function initializeOldStores() { // migrate all stores for (const [store, internal] of Object.values(stores)) { const versions = internal.versions.sort((a, b) => a.version - b.version); @@ -177,24 +175,3 @@ export function createVersionedStore(): StoreBuilder { }, }; } - -export function useStore( - store: StoreRet -): [T, (cb: (old: T) => T) => void] { - const [data, setData] = useState(store.get()); - useEffect(() => { - const { destroy } = store.onChange((newData) => { - setData(newData); - }); - return () => { - destroy(); - }; - }, [store]); - - function setNewData(cb: (old: T) => T) { - const newData = cb(data); - store.save(newData); - } - - return [data, setNewData]; -} diff --git a/src/stores/__old/settings/store.ts b/src/stores/__old/settings/store.ts new file mode 100644 index 00000000..2f92d3ba --- /dev/null +++ b/src/stores/__old/settings/store.ts @@ -0,0 +1,68 @@ +import { useLanguageStore } from "@/stores/language"; +import { useSubtitleStore } from "@/stores/subtitles"; + +import { MWSettingsData, MWSettingsDataV1 } from "./types"; +import { createVersionedStore } from "../migrations"; + +export const SettingsStore = createVersionedStore>() + .setKey("mw-settings") + .addVersion({ + version: 0, + create(): MWSettingsDataV1 { + return { + language: "en", + captionSettings: { + delay: 0, + style: { + color: "#ffffff", + fontSize: 25, + backgroundColor: "#00000096", + }, + }, + }; + }, + migrate(data: MWSettingsDataV1): MWSettingsData { + return { + language: data.language, + captionSettings: { + language: "none", + ...data.captionSettings, + }, + }; + }, + }) + .addVersion({ + version: 1, + migrate(old: MWSettingsData): Record { + const langStore = useLanguageStore.getState(); + const subtitleStore = useSubtitleStore.getState(); + + const backgroundColor = old.captionSettings.style.backgroundColor; + let backgroundOpacity = 0.5; + if (backgroundColor.length === 9) { + const opacitySplit = backgroundColor.slice(7); // '#' + 6 digits + backgroundOpacity = parseInt(opacitySplit, 16) / 255; // read as hex; + } + + langStore.setLanguage(old.language); + subtitleStore.updateStyling({ + backgroundOpacity, + color: old.captionSettings.style.color, + size: old.captionSettings.style.fontSize / 25, + }); + subtitleStore.importSubtitleLanguage( + old.captionSettings.language === "none" + ? null + : old.captionSettings.language + ); + + return {}; + }, + }) + .addVersion({ + version: 2, + create(): Record { + return {}; + }, + }) + .build(); diff --git a/src/state/settings/types.ts b/src/stores/__old/settings/types.ts similarity index 100% rename from src/state/settings/types.ts rename to src/stores/__old/settings/types.ts diff --git a/src/stores/__old/volume/store.ts b/src/stores/__old/volume/store.ts new file mode 100644 index 00000000..9c2eeba1 --- /dev/null +++ b/src/stores/__old/volume/store.ts @@ -0,0 +1,29 @@ +import { useVolumeStore } from "@/stores/volume"; + +import { createVersionedStore } from "../migrations"; + +interface VolumeStoreData { + volume: number; +} + +export const volumeStore = createVersionedStore>() + .setKey("mw-volume") + .addVersion({ + version: 0, + create() { + return { + volume: 1, + }; + }, + migrate(data: VolumeStoreData): Record { + useVolumeStore.getState().setVolume(data.volume); + return {}; + }, + }) + .addVersion({ + version: 1, + create() { + return {}; + }, + }) + .build(); diff --git a/src/state/watched/migrations/v2.ts b/src/stores/__old/watched/migrations/v2.ts similarity index 100% rename from src/state/watched/migrations/v2.ts rename to src/stores/__old/watched/migrations/v2.ts diff --git a/src/state/watched/migrations/v3.ts b/src/stores/__old/watched/migrations/v3.ts similarity index 100% rename from src/state/watched/migrations/v3.ts rename to src/stores/__old/watched/migrations/v3.ts diff --git a/src/state/watched/migrations/v4.ts b/src/stores/__old/watched/migrations/v4.ts similarity index 100% rename from src/state/watched/migrations/v4.ts rename to src/stores/__old/watched/migrations/v4.ts diff --git a/src/state/watched/store.ts b/src/stores/__old/watched/store.ts similarity index 95% rename from src/state/watched/store.ts rename to src/stores/__old/watched/store.ts index 75d05b93..7d5739ad 100644 --- a/src/state/watched/store.ts +++ b/src/stores/__old/watched/store.ts @@ -1,10 +1,10 @@ import { useProgressStore } from "@/stores/progress"; -import { createVersionedStore } from "@/utils/storage"; import { OldData, migrateV2Videos } from "./migrations/v2"; import { migrateV3Videos } from "./migrations/v3"; import { migrateV4Videos } from "./migrations/v4"; import { WatchedStoreData } from "./types"; +import { createVersionedStore } from "../migrations"; export const VideoProgressStore = createVersionedStore() .setKey("video-progress") diff --git a/src/state/watched/types.ts b/src/stores/__old/watched/types.ts similarity index 100% rename from src/state/watched/types.ts rename to src/stores/__old/watched/types.ts diff --git a/src/stores/language/index.ts b/src/stores/language/index.ts new file mode 100644 index 00000000..9ff91476 --- /dev/null +++ b/src/stores/language/index.ts @@ -0,0 +1,29 @@ +import { useEffect } from "react"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +import i18n from "@/setup/i18n"; + +export interface LanguageStore { + language: string; + setLanguage(v: string): void; +} + +export const useLanguageStore = create( + immer((set) => ({ + language: "en", + setLanguage(v) { + set((s) => { + s.language = v; + }); + }, + })) +); + +export function useLanguageListener() { + const language = useLanguageStore((s) => s.language); + + useEffect(() => { + i18n.changeLanguage(language); + }, [language]); +} diff --git a/src/stores/subtitles/index.ts b/src/stores/subtitles/index.ts index 9a7f1e86..6bf93737 100644 --- a/src/stores/subtitles/index.ts +++ b/src/stores/subtitles/index.ts @@ -30,9 +30,9 @@ export interface SubtitleStore { setCustomSubs(): void; setOverrideCasing(enabled: boolean): void; setDelay(delay: number): void; + importSubtitleLanguage(lang: string | null): void; } -// TODO add migration from previous stored settings export const useSubtitleStore = create( persist( immer((set) => ({ @@ -77,6 +77,11 @@ export const useSubtitleStore = create( s.delay = Math.max(Math.min(500, delay), -500); }); }, + importSubtitleLanguage(lang) { + set((s) => { + s.lastSelectedLanguage = lang; + }); + }, })), { name: "__MW::subtitles",