mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 23:36:06 +00:00
Settings and volume migrations + add language setting + move all old store data to /stores/__old
This commit is contained in:
parent
97c42eeb49
commit
f8bba7b27b
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<SettingsProvider>
|
||||
<WatchedContextProvider>
|
||||
<BookmarkContextProvider>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/s/:query">
|
||||
<QuickSearch />
|
||||
</Route>
|
||||
<Route exact path="/search/:type">
|
||||
<Redirect to="/browse" push={false} />
|
||||
</Route>
|
||||
<Route exact path="/search/:type/:query?">
|
||||
{({ match }) => {
|
||||
if (match?.params.query)
|
||||
return (
|
||||
<Redirect
|
||||
to={`/browse/${match?.params.query}`}
|
||||
push={false}
|
||||
/>
|
||||
);
|
||||
return <Redirect to="/browse" push={false} />;
|
||||
}}
|
||||
</Route>
|
||||
<Layout>
|
||||
<Switch>
|
||||
{/* functional routes */}
|
||||
<Route exact path="/s/:query">
|
||||
<QuickSearch />
|
||||
</Route>
|
||||
<Route exact path="/search/:type">
|
||||
<Redirect to="/browse" push={false} />
|
||||
</Route>
|
||||
<Route exact path="/search/:type/:query?">
|
||||
{({ match }) => {
|
||||
if (match?.params.query)
|
||||
return (
|
||||
<Redirect to={`/browse/${match?.params.query}`} push={false} />
|
||||
);
|
||||
return <Redirect to="/browse" push={false} />;
|
||||
}}
|
||||
</Route>
|
||||
|
||||
{/* pages */}
|
||||
<Route
|
||||
exact
|
||||
path={["/media/:media", "/media/:media/:season/:episode"]}
|
||||
>
|
||||
<LegacyUrlView>
|
||||
<PlayerView />
|
||||
</LegacyUrlView>
|
||||
</Route>
|
||||
<Route
|
||||
exact
|
||||
path={["/browse/:query?", "/"]}
|
||||
component={HomePage}
|
||||
/>
|
||||
<Route exact path="/faq" component={AboutPage} />
|
||||
<Route exact path="/dmca" component={DmcaPage} />
|
||||
{/* pages */}
|
||||
<Route exact path={["/media/:media", "/media/:media/:season/:episode"]}>
|
||||
<LegacyUrlView>
|
||||
<PlayerView />
|
||||
</LegacyUrlView>
|
||||
</Route>
|
||||
<Route exact path={["/browse/:query?", "/"]} component={HomePage} />
|
||||
<Route exact path="/faq" component={AboutPage} />
|
||||
<Route exact path="/dmca" component={DmcaPage} />
|
||||
|
||||
{/* Settings page */}
|
||||
<Route exact path="/settings" component={SettingsPage} />
|
||||
{/* Settings page */}
|
||||
<Route exact path="/settings" component={SettingsPage} />
|
||||
|
||||
{/* admin routes */}
|
||||
<Route exact path="/admin" component={AdminPage} />
|
||||
{/* admin routes */}
|
||||
<Route exact path="/admin" component={AdminPage} />
|
||||
|
||||
{/* other */}
|
||||
<Route
|
||||
exact
|
||||
path="/dev"
|
||||
component={lazy(() => import("@/pages/DeveloperPage"))}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/dev/video"
|
||||
component={lazy(
|
||||
() => import("@/pages/developer/VideoTesterView")
|
||||
)}
|
||||
/>
|
||||
{/* developer routes that can abuse workers are disabled in production */}
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<Route
|
||||
exact
|
||||
path="/dev/test"
|
||||
component={lazy(() => import("@/pages/developer/TestView"))}
|
||||
/>
|
||||
) : null}
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
</BookmarkContextProvider>
|
||||
</WatchedContextProvider>
|
||||
</SettingsProvider>
|
||||
{/* other */}
|
||||
<Route
|
||||
exact
|
||||
path="/dev"
|
||||
component={lazy(() => import("@/pages/DeveloperPage"))}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/dev/video"
|
||||
component={lazy(() => import("@/pages/developer/VideoTesterView"))}
|
||||
/>
|
||||
{/* developer routes that can abuse workers are disabled in production */}
|
||||
{process.env.NODE_ENV === "development" ? (
|
||||
<Route
|
||||
exact
|
||||
path="/dev/test"
|
||||
component={lazy(() => import("@/pages/developer/TestView"))}
|
||||
/>
|
||||
) : null}
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<BookmarkStoreDataWrapper>({
|
||||
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 (
|
||||
<BookmarkedContext.Provider value={contextValue}>
|
||||
{props.children}
|
||||
</BookmarkedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBookmarkContext() {
|
||||
return useContext(BookmarkedContext);
|
||||
}
|
||||
|
||||
export function getIfBookmarkedFromPortable(
|
||||
bookmarks: MWMediaMeta[],
|
||||
media: MWMediaMeta
|
||||
): boolean {
|
||||
const bookmarked = getBookmarkIndexFromMedia(bookmarks, media);
|
||||
return bookmarked !== -1;
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./context";
|
|
@ -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<MWSettingsDataWrapper>(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 (
|
||||
<SettingsContext.Provider value={context}>
|
||||
{props.children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
return useContext(SettingsContext);
|
||||
}
|
||||
|
||||
export default SettingsContext;
|
|
@ -1 +0,0 @@
|
|||
export * from "./context";
|
|
@ -1,49 +0,0 @@
|
|||
import { createVersionedStore } from "@/utils/storage";
|
||||
|
||||
import { MWSettingsData, MWSettingsDataV1 } from "./types";
|
||||
|
||||
export const SettingsStore = createVersionedStore<MWSettingsData>()
|
||||
.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();
|
|
@ -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<WatchedStoreDataWrapper>({
|
||||
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 (
|
||||
<WatchedContext.Provider value={contextValue as any}>
|
||||
{props.children}
|
||||
</WatchedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./context";
|
|
@ -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";
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
interface StoreVersion<A> {
|
||||
version: number;
|
||||
migrate?(data: A): any;
|
||||
|
@ -28,7 +26,7 @@ interface InternalStoreData {
|
|||
const storeCallbacks: Record<string, ((data: any) => void)[]> = {};
|
||||
const stores: Record<string, [StoreRet<any>, 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<T>(): StoreBuilder<T> {
|
|||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useStore<T>(
|
||||
store: StoreRet<T>
|
||||
): [T, (cb: (old: T) => T) => void] {
|
||||
const [data, setData] = useState<T>(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];
|
||||
}
|
68
src/stores/__old/settings/store.ts
Normal file
68
src/stores/__old/settings/store.ts
Normal file
|
@ -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<Record<never, never>>()
|
||||
.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<never, never> {
|
||||
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<never, never> {
|
||||
return {};
|
||||
},
|
||||
})
|
||||
.build();
|
29
src/stores/__old/volume/store.ts
Normal file
29
src/stores/__old/volume/store.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { useVolumeStore } from "@/stores/volume";
|
||||
|
||||
import { createVersionedStore } from "../migrations";
|
||||
|
||||
interface VolumeStoreData {
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export const volumeStore = createVersionedStore<Record<never, never>>()
|
||||
.setKey("mw-volume")
|
||||
.addVersion({
|
||||
version: 0,
|
||||
create() {
|
||||
return {
|
||||
volume: 1,
|
||||
};
|
||||
},
|
||||
migrate(data: VolumeStoreData): Record<never, never> {
|
||||
useVolumeStore.getState().setVolume(data.volume);
|
||||
return {};
|
||||
},
|
||||
})
|
||||
.addVersion({
|
||||
version: 1,
|
||||
create() {
|
||||
return {};
|
||||
},
|
||||
})
|
||||
.build();
|
|
@ -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<WatchedStoreData>()
|
||||
.setKey("video-progress")
|
29
src/stores/language/index.ts
Normal file
29
src/stores/language/index.ts
Normal file
|
@ -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<LanguageStore>((set) => ({
|
||||
language: "en",
|
||||
setLanguage(v) {
|
||||
set((s) => {
|
||||
s.language = v;
|
||||
});
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
export function useLanguageListener() {
|
||||
const language = useLanguageStore((s) => s.language);
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
}, [language]);
|
||||
}
|
|
@ -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<SubtitleStore>((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",
|
||||
|
|
Loading…
Reference in a new issue