3 changed files with 352 additions and 1 deletions
@ -0,0 +1,103 @@
@@ -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; |
||||
} |
@ -0,0 +1,234 @@
@@ -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 @@
@@ -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 new issue