mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 18:21:53 +01: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 { CenterContainer } from "@/components/layout/ThinContainer";
|
||||||
|
import { useSettingsExport } from "@/hooks/useSettingsExport";
|
||||||
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
|
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
|
||||||
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
import { PageTitle } from "@/pages/parts/util/PageTitle";
|
||||||
|
|
||||||
export function MigrationDirectPage() {
|
export function MigrationDirectPage() {
|
||||||
|
const exportSettings = useSettingsExport();
|
||||||
|
|
||||||
|
const doDownload = useCallback(() => {
|
||||||
|
const data = exportSettings(false);
|
||||||
|
console.log(data);
|
||||||
|
}, [exportSettings]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MinimalPageLayout>
|
<MinimalPageLayout>
|
||||||
<PageTitle subpage k="global.pages.migration" />
|
<PageTitle subpage k="global.pages.migration" />
|
||||||
<CenterContainer>Hi</CenterContainer>
|
<CenterContainer>
|
||||||
|
<button onClick={doDownload} type="button">
|
||||||
|
Hello
|
||||||
|
</button>
|
||||||
|
</CenterContainer>
|
||||||
</MinimalPageLayout>
|
</MinimalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user