mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-30 16:17:41 +01: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
22 changed files with 195 additions and 525 deletions
|
@ -10,12 +10,12 @@ import { ErrorBoundary } from "@/pages/errors/ErrorBoundary";
|
||||||
import App from "@/setup/App";
|
import App from "@/setup/App";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
import i18n from "@/setup/i18n";
|
import i18n from "@/setup/i18n";
|
||||||
|
|
||||||
import "@/setup/ga";
|
import "@/setup/ga";
|
||||||
import "@/setup/index.css";
|
import "@/setup/index.css";
|
||||||
|
import { useLanguageStore } from "@/stores/language";
|
||||||
|
|
||||||
import { initializeChromecast } from "./setup/chromecast";
|
import { initializeChromecast } from "./setup/chromecast";
|
||||||
import { SettingsStore } from "./state/settings/store";
|
import { initializeOldStores } from "./stores/__old/migrations";
|
||||||
import { initializeStores } from "./utils/storage";
|
|
||||||
|
|
||||||
// initialize
|
// initialize
|
||||||
const key =
|
const key =
|
||||||
|
@ -29,8 +29,8 @@ registerSW({
|
||||||
});
|
});
|
||||||
|
|
||||||
const LazyLoadedApp = React.lazy(async () => {
|
const LazyLoadedApp = React.lazy(async () => {
|
||||||
await initializeStores();
|
await initializeOldStores();
|
||||||
i18n.changeLanguage(SettingsStore.get().language ?? "en");
|
i18n.changeLanguage(useLanguageStore.getState().language);
|
||||||
return {
|
return {
|
||||||
default: App,
|
default: App,
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,10 +19,8 @@ import { HomePage } from "@/pages/HomePage";
|
||||||
import { PlayerView } from "@/pages/PlayerView";
|
import { PlayerView } from "@/pages/PlayerView";
|
||||||
import { SettingsPage } from "@/pages/Settings";
|
import { SettingsPage } from "@/pages/Settings";
|
||||||
import { Layout } from "@/setup/Layout";
|
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 { useHistoryListener } from "@/stores/history";
|
||||||
|
import { useLanguageListener } from "@/stores/language";
|
||||||
|
|
||||||
function LegacyUrlView({ children }: { children: ReactElement }) {
|
function LegacyUrlView({ children }: { children: ReactElement }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -60,11 +58,9 @@ function QuickSearch() {
|
||||||
function App() {
|
function App() {
|
||||||
useHistoryListener();
|
useHistoryListener();
|
||||||
useOnlineListener();
|
useOnlineListener();
|
||||||
|
useLanguageListener();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
|
||||||
<WatchedContextProvider>
|
|
||||||
<BookmarkContextProvider>
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Switch>
|
<Switch>
|
||||||
{/* functional routes */}
|
{/* functional routes */}
|
||||||
|
@ -78,29 +74,19 @@ function App() {
|
||||||
{({ match }) => {
|
{({ match }) => {
|
||||||
if (match?.params.query)
|
if (match?.params.query)
|
||||||
return (
|
return (
|
||||||
<Redirect
|
<Redirect to={`/browse/${match?.params.query}`} push={false} />
|
||||||
to={`/browse/${match?.params.query}`}
|
|
||||||
push={false}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
return <Redirect to="/browse" push={false} />;
|
return <Redirect to="/browse" push={false} />;
|
||||||
}}
|
}}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* pages */}
|
{/* pages */}
|
||||||
<Route
|
<Route exact path={["/media/:media", "/media/:media/:season/:episode"]}>
|
||||||
exact
|
|
||||||
path={["/media/:media", "/media/:media/:season/:episode"]}
|
|
||||||
>
|
|
||||||
<LegacyUrlView>
|
<LegacyUrlView>
|
||||||
<PlayerView />
|
<PlayerView />
|
||||||
</LegacyUrlView>
|
</LegacyUrlView>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route exact path={["/browse/:query?", "/"]} component={HomePage} />
|
||||||
exact
|
|
||||||
path={["/browse/:query?", "/"]}
|
|
||||||
component={HomePage}
|
|
||||||
/>
|
|
||||||
<Route exact path="/faq" component={AboutPage} />
|
<Route exact path="/faq" component={AboutPage} />
|
||||||
<Route exact path="/dmca" component={DmcaPage} />
|
<Route exact path="/dmca" component={DmcaPage} />
|
||||||
|
|
||||||
|
@ -119,9 +105,7 @@ function App() {
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/dev/video"
|
path="/dev/video"
|
||||||
component={lazy(
|
component={lazy(() => import("@/pages/developer/VideoTesterView"))}
|
||||||
() => import("@/pages/developer/VideoTesterView")
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{/* developer routes that can abuse workers are disabled in production */}
|
{/* developer routes that can abuse workers are disabled in production */}
|
||||||
{process.env.NODE_ENV === "development" ? (
|
{process.env.NODE_ENV === "development" ? (
|
||||||
|
@ -134,9 +118,6 @@ function App() {
|
||||||
<Route path="*" component={NotFoundPage} />
|
<Route path="*" component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Layout>
|
</Layout>
|
||||||
</BookmarkContextProvider>
|
|
||||||
</WatchedContextProvider>
|
|
||||||
</SettingsProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { MWMediaType } from "@/backend/metadata/types/mw";
|
||||||
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
|
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
|
||||||
import { createVersionedStore } from "@/utils/storage";
|
|
||||||
|
|
||||||
import { BookmarkStoreData } from "./types";
|
import { BookmarkStoreData } from "./types";
|
||||||
|
import { createVersionedStore } from "../migrations";
|
||||||
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
import { OldBookmarks, migrateV1Bookmarks } from "../watched/migrations/v2";
|
||||||
import { migrateV2Bookmarks } from "../watched/migrations/v3";
|
import { migrateV2Bookmarks } from "../watched/migrations/v3";
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface StoreVersion<A> {
|
interface StoreVersion<A> {
|
||||||
version: number;
|
version: number;
|
||||||
migrate?(data: A): any;
|
migrate?(data: A): any;
|
||||||
|
@ -28,7 +26,7 @@ interface InternalStoreData {
|
||||||
const storeCallbacks: Record<string, ((data: any) => void)[]> = {};
|
const storeCallbacks: Record<string, ((data: any) => void)[]> = {};
|
||||||
const stores: Record<string, [StoreRet<any>, InternalStoreData]> = {};
|
const stores: Record<string, [StoreRet<any>, InternalStoreData]> = {};
|
||||||
|
|
||||||
export async function initializeStores() {
|
export async function initializeOldStores() {
|
||||||
// migrate all stores
|
// migrate all stores
|
||||||
for (const [store, internal] of Object.values(stores)) {
|
for (const [store, internal] of Object.values(stores)) {
|
||||||
const versions = internal.versions.sort((a, b) => a.version - b.version);
|
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 { useProgressStore } from "@/stores/progress";
|
||||||
import { createVersionedStore } from "@/utils/storage";
|
|
||||||
|
|
||||||
import { OldData, migrateV2Videos } from "./migrations/v2";
|
import { OldData, migrateV2Videos } from "./migrations/v2";
|
||||||
import { migrateV3Videos } from "./migrations/v3";
|
import { migrateV3Videos } from "./migrations/v3";
|
||||||
import { migrateV4Videos } from "./migrations/v4";
|
import { migrateV4Videos } from "./migrations/v4";
|
||||||
import { WatchedStoreData } from "./types";
|
import { WatchedStoreData } from "./types";
|
||||||
|
import { createVersionedStore } from "../migrations";
|
||||||
|
|
||||||
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
export const VideoProgressStore = createVersionedStore<WatchedStoreData>()
|
||||||
.setKey("video-progress")
|
.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;
|
setCustomSubs(): void;
|
||||||
setOverrideCasing(enabled: boolean): void;
|
setOverrideCasing(enabled: boolean): void;
|
||||||
setDelay(delay: number): void;
|
setDelay(delay: number): void;
|
||||||
|
importSubtitleLanguage(lang: string | null): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add migration from previous stored settings
|
|
||||||
export const useSubtitleStore = create(
|
export const useSubtitleStore = create(
|
||||||
persist(
|
persist(
|
||||||
immer<SubtitleStore>((set) => ({
|
immer<SubtitleStore>((set) => ({
|
||||||
|
@ -77,6 +77,11 @@ export const useSubtitleStore = create(
|
||||||
s.delay = Math.max(Math.min(500, delay), -500);
|
s.delay = Math.max(Math.min(500, delay), -500);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
importSubtitleLanguage(lang) {
|
||||||
|
set((s) => {
|
||||||
|
s.lastSelectedLanguage = lang;
|
||||||
|
});
|
||||||
|
},
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: "__MW::subtitles",
|
name: "__MW::subtitles",
|
||||||
|
|
Loading…
Reference in a new issue