mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-27 00:26:06 +00:00
Add import and export functions for settings to JSON
This commit is contained in:
parent
bcfadc8f60
commit
51e9c4d758
103
src/hooks/useSettingsExport.ts
Normal file
103
src/hooks/useSettingsExport.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { useCallback } from "react";
|
||||
|
||||
import { Settings } from "@/hooks/useSettingsImport";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useLanguageStore } from "@/stores/language";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { useQualityStore } from "@/stores/quality";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { useVolumeStore } from "@/stores/volume";
|
||||
|
||||
export function useSettingsExport() {
|
||||
const authStore = useAuthStore();
|
||||
const bookmarksStore = useBookmarkStore();
|
||||
const languageStore = useLanguageStore();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const progressStore = useProgressStore();
|
||||
const qualityStore = useQualityStore();
|
||||
const subtitleStore = useSubtitleStore();
|
||||
const themeStore = useThemeStore();
|
||||
const volumeStore = useVolumeStore();
|
||||
|
||||
const collect = useCallback(
|
||||
(includeAuth: boolean): Settings => {
|
||||
return {
|
||||
auth: {
|
||||
account: includeAuth ? authStore.account : undefined,
|
||||
backendUrl: authStore.backendUrl,
|
||||
proxySet: authStore.proxySet,
|
||||
},
|
||||
bookmarks: {
|
||||
bookmarks: bookmarksStore.bookmarks,
|
||||
},
|
||||
language: {
|
||||
language: languageStore.language,
|
||||
},
|
||||
preferences: {
|
||||
enableThumbnails: preferencesStore.enableThumbnails,
|
||||
},
|
||||
progress: {
|
||||
items: progressStore.items,
|
||||
},
|
||||
quality: {
|
||||
quality: {
|
||||
automaticQuality: qualityStore.quality.automaticQuality,
|
||||
lastChosenQuality: qualityStore.quality.lastChosenQuality,
|
||||
},
|
||||
},
|
||||
subtitles: {
|
||||
lastSelectedLanguage: subtitleStore.lastSelectedLanguage,
|
||||
styling: {
|
||||
backgroundBlur: subtitleStore.styling.backgroundBlur,
|
||||
backgroundOpacity: subtitleStore.styling.backgroundOpacity,
|
||||
color: subtitleStore.styling.color,
|
||||
size: subtitleStore.styling.size,
|
||||
},
|
||||
overrideCasing: subtitleStore.overrideCasing,
|
||||
delay: subtitleStore.delay,
|
||||
},
|
||||
theme: {
|
||||
theme: themeStore.theme,
|
||||
},
|
||||
volume: {
|
||||
volume: volumeStore.volume,
|
||||
},
|
||||
};
|
||||
},
|
||||
[
|
||||
authStore,
|
||||
bookmarksStore,
|
||||
languageStore,
|
||||
preferencesStore,
|
||||
progressStore,
|
||||
qualityStore,
|
||||
subtitleStore,
|
||||
themeStore,
|
||||
volumeStore,
|
||||
],
|
||||
);
|
||||
|
||||
const exportSettings = useCallback(
|
||||
(includeAuth: boolean) => {
|
||||
const output = JSON.stringify(collect(includeAuth), null, 2);
|
||||
|
||||
const blob = new Blob([output], { type: "application/json" });
|
||||
const elem = window.document.createElement("a");
|
||||
elem.href = window.URL.createObjectURL(blob);
|
||||
|
||||
const date = new Date();
|
||||
elem.download = `movie-web settings - ${
|
||||
date.toISOString().split("T")[0]
|
||||
}.json`;
|
||||
document.body.appendChild(elem);
|
||||
elem.click();
|
||||
document.body.removeChild(elem);
|
||||
},
|
||||
[collect],
|
||||
);
|
||||
|
||||
return exportSettings;
|
||||
}
|
234
src/hooks/useSettingsImport.ts
Normal file
234
src/hooks/useSettingsImport.ts
Normal file
|
@ -0,0 +1,234 @@
|
|||
import { useCallback } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||
import { useLanguageStore } from "@/stores/language";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { useQualityStore } from "@/stores/quality";
|
||||
import { useSubtitleStore } from "@/stores/subtitles";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
import { useVolumeStore } from "@/stores/volume";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
auth: z.object({
|
||||
account: z
|
||||
.object({
|
||||
profile: z.object({
|
||||
colorA: z.string(),
|
||||
colorB: z.string(),
|
||||
icon: z.string(),
|
||||
}),
|
||||
sessionId: z.string(),
|
||||
userId: z.string(),
|
||||
token: z.string(),
|
||||
seed: z.string(),
|
||||
deviceName: z.string(),
|
||||
})
|
||||
.nullish(),
|
||||
backendUrl: z.string().nullable(),
|
||||
proxySet: z.array(z.string()).nullable(),
|
||||
}),
|
||||
bookmarks: z.object({
|
||||
bookmarks: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
year: z.number().optional(),
|
||||
poster: z.string().optional(),
|
||||
type: z.enum(["show", "movie"]),
|
||||
updatedAt: z.number(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
language: z.object({
|
||||
language: z.string(),
|
||||
}),
|
||||
preferences: z.object({
|
||||
enableThumbnails: z.boolean(),
|
||||
}),
|
||||
progress: z.object({
|
||||
items: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
year: z.number().optional(),
|
||||
poster: z.string().optional(),
|
||||
type: z.enum(["show", "movie"]),
|
||||
updatedAt: z.number(),
|
||||
progress: z
|
||||
.object({
|
||||
watched: z.number(),
|
||||
duration: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
seasons: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
number: z.number(),
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
episodes: z.record(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
number: z.number(),
|
||||
id: z.string(),
|
||||
seasonId: z.string(),
|
||||
updatedAt: z.number(),
|
||||
progress: z.object({
|
||||
watched: z.number(),
|
||||
duration: z.number(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
quality: z.object({
|
||||
quality: z.object({
|
||||
automaticQuality: z.boolean(),
|
||||
lastChosenQuality: z
|
||||
.enum(["unknown", "360", "480", "720", "1080", "4k"])
|
||||
.nullable(),
|
||||
}),
|
||||
}),
|
||||
subtitles: z.object({
|
||||
lastSelectedLanguage: z.string().nullable(),
|
||||
styling: z.object({
|
||||
backgroundBlur: z.number(),
|
||||
backgroundOpacity: z.number(),
|
||||
color: z.string(),
|
||||
size: z.number(),
|
||||
}),
|
||||
overrideCasing: z.boolean(),
|
||||
delay: z.number(),
|
||||
}),
|
||||
theme: z.object({
|
||||
theme: z.string().nullable(),
|
||||
}),
|
||||
volume: z.object({
|
||||
volume: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
const settingsPartialSchema = settingsSchema.partial();
|
||||
|
||||
export type Settings = z.infer<typeof settingsSchema>;
|
||||
|
||||
export function useSettingsImport() {
|
||||
const authStore = useAuthStore();
|
||||
const bookmarksStore = useBookmarkStore();
|
||||
const languageStore = useLanguageStore();
|
||||
const preferencesStore = usePreferencesStore();
|
||||
const progressStore = useProgressStore();
|
||||
const qualityStore = useQualityStore();
|
||||
const subtitleStore = useSubtitleStore();
|
||||
const themeStore = useThemeStore();
|
||||
const volumeStore = useVolumeStore();
|
||||
|
||||
const importSettings = useCallback(
|
||||
async (file: File) => {
|
||||
const text = await file.text();
|
||||
|
||||
const data = settingsPartialSchema.parse(JSON.parse(text));
|
||||
if (data.auth?.account) authStore.setAccount(data.auth.account);
|
||||
if (data.auth?.backendUrl) authStore.setBackendUrl(data.auth.backendUrl);
|
||||
if (data.auth?.proxySet) authStore.setProxySet(data.auth.proxySet);
|
||||
if (data.bookmarks) {
|
||||
for (const [id, item] of Object.entries(data.bookmarks.bookmarks)) {
|
||||
bookmarksStore.setBookmark(id, {
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
year: item.year,
|
||||
poster: item.poster,
|
||||
updatedAt: item.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.language) languageStore.setLanguage(data.language.language);
|
||||
if (data.preferences) {
|
||||
preferencesStore.setEnableThumbnails(data.preferences.enableThumbnails);
|
||||
}
|
||||
if (data.quality) {
|
||||
qualityStore.setAutomaticQuality(data.quality.quality.automaticQuality);
|
||||
qualityStore.setLastChosenQuality(
|
||||
data.quality.quality.lastChosenQuality,
|
||||
);
|
||||
}
|
||||
if (data.subtitles) {
|
||||
subtitleStore.setLanguage(data.subtitles.lastSelectedLanguage);
|
||||
subtitleStore.updateStyling(data.subtitles.styling);
|
||||
subtitleStore.setOverrideCasing(data.subtitles.overrideCasing);
|
||||
subtitleStore.setDelay(data.subtitles.delay);
|
||||
}
|
||||
if (data.theme) themeStore.setTheme(data.theme.theme);
|
||||
if (data.volume) volumeStore.setVolume(data.volume.volume);
|
||||
|
||||
if (data.progress) {
|
||||
for (const [id, item] of Object.entries(data.progress.items)) {
|
||||
if (!progressStore.items[id]) {
|
||||
progressStore.setItem(id, item);
|
||||
}
|
||||
|
||||
// We want to preserve existing progress so we take the max of the updatedAt and the progress
|
||||
const storeItem = progressStore.items[id];
|
||||
storeItem.updatedAt = Math.max(storeItem.updatedAt, item.updatedAt);
|
||||
storeItem.title = item.title;
|
||||
storeItem.year = item.year;
|
||||
storeItem.poster = item.poster;
|
||||
storeItem.type = item.type;
|
||||
storeItem.progress = item.progress
|
||||
? {
|
||||
duration: item.progress.duration,
|
||||
watched: Math.max(
|
||||
storeItem.progress?.watched ?? 0,
|
||||
item.progress.watched,
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
for (const [seasonId, season] of Object.entries(item.seasons)) {
|
||||
storeItem.seasons[seasonId] = season;
|
||||
}
|
||||
|
||||
for (const [episodeId, episode] of Object.entries(item.episodes)) {
|
||||
if (!storeItem.episodes[episodeId]) {
|
||||
storeItem.episodes[episodeId] = episode;
|
||||
}
|
||||
|
||||
const storeEpisode = storeItem.episodes[episodeId];
|
||||
storeEpisode.updatedAt = Math.max(
|
||||
storeEpisode.updatedAt,
|
||||
episode.updatedAt,
|
||||
);
|
||||
storeEpisode.title = episode.title;
|
||||
storeEpisode.number = episode.number;
|
||||
storeEpisode.seasonId = episode.seasonId;
|
||||
storeEpisode.progress = {
|
||||
duration: episode.progress.duration,
|
||||
watched: Math.max(
|
||||
storeEpisode.progress.watched,
|
||||
episode.progress.watched,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
progressStore.setItem(id, storeItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
authStore,
|
||||
bookmarksStore,
|
||||
languageStore,
|
||||
preferencesStore,
|
||||
progressStore,
|
||||
qualityStore,
|
||||
subtitleStore,
|
||||
themeStore,
|
||||
volumeStore,
|
||||
],
|
||||
);
|
||||
|
||||
return importSettings;
|
||||
}
|
|
@ -1,12 +1,26 @@
|
|||
import { useCallback } from "react";
|
||||
|
||||
import { CenterContainer } from "@/components/layout/ThinContainer";
|
||||
import { useSettingsExport } from "@/hooks/useSettingsExport";
|
||||
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
|
||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||
|
||||
export function MigrationDirectPage() {
|
||||
const exportSettings = useSettingsExport();
|
||||
|
||||
const doDownload = useCallback(() => {
|
||||
const data = exportSettings(false);
|
||||
console.log(data);
|
||||
}, [exportSettings]);
|
||||
|
||||
return (
|
||||
<MinimalPageLayout>
|
||||
<PageTitle subpage k="global.pages.migration" />
|
||||
<CenterContainer>Hi</CenterContainer>
|
||||
<CenterContainer>
|
||||
<button onClick={doDownload} type="button">
|
||||
Hello
|
||||
</button>
|
||||
</CenterContainer>
|
||||
</MinimalPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue