Add import and export functions for settings to JSON

This commit is contained in:
William Oldham 2024-03-16 17:57:41 +00:00
parent bcfadc8f60
commit 51e9c4d758
3 changed files with 352 additions and 1 deletions

View 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;
}

View 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;
}

View file

@ -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>
);
}