3 changed files with 352 additions and 1 deletions
@ -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 @@ |
|||||||
|
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