update migration to support settings

groups and favorite eps are still broken
This commit is contained in:
Pas 2025-11-29 14:41:52 -07:00
parent 2252ab9fed
commit 1e00777c64
6 changed files with 619 additions and 102 deletions

View file

@ -458,6 +458,15 @@
}
}
},
"preview": {
"downloadDescription": "Download includes:",
"uploadDescription": "Data to upload:",
"items": {
"bookmarks": "Bookmarked media",
"progress": "Watch progress",
"settings": "Settings & Preferences"
}
},
"direct": {
"title": "Direct migration",
"description": "Enter the destination backend URL to migrate your current account data to a new backend. This keeps your passphrase the same!",
@ -478,11 +487,6 @@
"download": {
"title": "Download data",
"description": "This will download your data to your device. You can then upload it to the new server or just keep it for safekeeping.",
"items": {
"description": "Download includes:",
"bookmarks": "Bookmarked media",
"progress": "Watch progress"
},
"status": {
"error": "Failed to download your data. 😿",
"success": "Your data has been downloaded successfully! 🎉"
@ -508,12 +512,8 @@
"change": "Change file",
"name": "File name"
},
"dataPreview": "Preview:",
"items": {
"bookmarks": "Bookmarked media",
"progress": "Watch progress"
},
"exportedOn": "Exported on",
"previewTitle": "Preview:",
"button": {
"import": "Import data",
"processing": "Processing...",

View file

@ -5,6 +5,7 @@ import { AccountWithToken } from "@/stores/auth";
import { BookmarkInput } from "./bookmarks";
import { ProgressInput } from "./progress";
import { SettingsInput } from "./settings";
export function importProgress(
url: string,
@ -31,3 +32,29 @@ export function importBookmarks(
headers: getAuthHeaders(account.token),
});
}
export function importGroupOrder(
url: string,
account: AccountWithToken,
groupOrder: string[],
) {
return ofetch<void>(`/users/${account.userId}/group-order`, {
method: "PUT",
body: groupOrder,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}
export function importSettings(
url: string,
account: AccountWithToken,
settings: SettingsInput,
) {
return ofetch<void>(`/users/${account.userId}/settings`, {
method: "PUT",
body: settings,
baseURL: url,
headers: getAuthHeaders(account.token),
});
}

View file

@ -11,7 +11,12 @@ import {
keysFromSeed,
signChallenge,
} from "@/backend/accounts/crypto";
import { importBookmarks, importProgress } from "@/backend/accounts/import";
import {
importBookmarks,
importGroupOrder,
importProgress,
importSettings,
} from "@/backend/accounts/import";
// import { getLoginChallengeToken, loginAccount } from "@/backend/accounts/login";
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
import {
@ -30,7 +35,10 @@ import { useAuthData } from "@/hooks/auth/useAuthData";
// import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
import { useGroupOrderStore } from "@/stores/groupOrder";
import { usePreferencesStore } from "@/stores/preferences";
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
export interface RegistrationData {
recaptchaToken?: string;
@ -56,39 +64,102 @@ export function useMigration() {
const currentAccount = useAuthStore((s) => s.account);
const progress = useProgressStore((s) => s.items);
const bookmarks = useBookmarkStore((s) => s.bookmarks);
const groupOrder = useGroupOrderStore((s) => s.groupOrder);
const preferences = usePreferencesStore.getState();
const subtitleLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const { login: userDataLogin } = useAuthData();
const importData = async (
backendUrl: string,
account: AccountWithToken,
progressItems: Record<string, ProgressMediaItem>,
bookmarkItems: Record<string, BookmarkMediaItem>,
) => {
if (
Object.keys(progressItems).length === 0 &&
Object.keys(bookmarkItems).length === 0
) {
return;
}
const progressInputs = Object.entries(progressItems).flatMap(
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
);
const bookmarkInputs = Object.entries(bookmarkItems).map(([tmdbId, item]) =>
bookmarkMediaToInput(tmdbId, item),
);
await Promise.all([
importProgress(backendUrl, account, progressInputs),
importBookmarks(backendUrl, account, bookmarkInputs),
]);
};
const migrate = useCallback(
async (backendUrl: string, recaptchaToken?: string) => {
if (!currentAccount) return;
const importData = async (
backendUrlInner: string,
account: AccountWithToken,
progressItems: Record<string, ProgressMediaItem>,
bookmarkItems: Record<string, BookmarkMediaItem>,
groupOrderItems: string[],
) => {
if (
Object.keys(progressItems).length === 0 &&
Object.keys(bookmarkItems).length === 0 &&
groupOrderItems.length === 0
) {
return;
}
const progressInputs = Object.entries(progressItems).flatMap(
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
);
const bookmarkInputs = Object.entries(bookmarkItems).map(
([tmdbId, item]) => bookmarkMediaToInput(tmdbId, item),
);
const importPromises = [
importProgress(backendUrlInner, account, progressInputs),
importBookmarks(backendUrlInner, account, bookmarkInputs),
];
// Import group order if it exists
if (groupOrderItems.length > 0) {
importPromises.push(
importGroupOrder(backendUrlInner, account, groupOrderItems),
);
}
// Import settings/preferences
importPromises.push(
importSettings(backendUrlInner, account, {
defaultSubtitleLanguage: subtitleLanguage || undefined,
febboxKey: preferences.febboxKey,
debridToken: preferences.debridToken,
debridService: preferences.debridService,
enableThumbnails: preferences.enableThumbnails,
enableAutoplay: preferences.enableAutoplay,
enableSkipCredits: preferences.enableSkipCredits,
enableDiscover: preferences.enableDiscover,
enableFeatured: preferences.enableFeatured,
enableDetailsModal: preferences.enableDetailsModal,
enableImageLogos: preferences.enableImageLogos,
enableCarouselView: preferences.enableCarouselView,
forceCompactEpisodeView: preferences.forceCompactEpisodeView,
sourceOrder:
preferences.sourceOrder.length > 0
? preferences.sourceOrder
: undefined,
enableSourceOrder: preferences.enableSourceOrder,
lastSuccessfulSource: preferences.lastSuccessfulSource,
enableLastSuccessfulSource: preferences.enableLastSuccessfulSource,
disabledSources:
preferences.disabledSources.length > 0
? preferences.disabledSources
: undefined,
embedOrder:
preferences.embedOrder.length > 0
? preferences.embedOrder
: undefined,
enableEmbedOrder: preferences.enableEmbedOrder,
disabledEmbeds:
preferences.disabledEmbeds.length > 0
? preferences.disabledEmbeds
: undefined,
proxyTmdb: preferences.proxyTmdb,
enableLowPerformanceMode: preferences.enableLowPerformanceMode,
enableNativeSubtitles: preferences.enableNativeSubtitles,
enableHoldToBoost: preferences.enableHoldToBoost,
homeSectionOrder:
preferences.homeSectionOrder.length > 0
? preferences.homeSectionOrder
: undefined,
manualSourceSelection: preferences.manualSourceSelection,
enableDoubleClickToSeek: preferences.enableDoubleClickToSeek,
}),
);
await Promise.all(importPromises);
};
const { challenge } = await getRegisterChallengeToken(
backendUrl,
recaptchaToken || undefined, // Pass undefined if token is not provided
@ -112,11 +183,19 @@ export function useMigration() {
bytesToBase64(keys.seed),
);
await importData(backendUrl, account, progress, bookmarks);
await importData(backendUrl, account, progress, bookmarks, groupOrder);
return account;
},
[currentAccount, userDataLogin, bookmarks, progress],
[
currentAccount,
userDataLogin,
bookmarks,
progress,
groupOrder,
preferences,
subtitleLanguage,
],
);
return {

View file

@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
import { Stepper } from "@/components/layout/Stepper";
import { CenterContainer } from "@/components/layout/ThinContainer";
@ -14,10 +15,14 @@ import { useMigration } from "@/hooks/auth/useMigration";
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { useAuthStore } from "@/stores/auth";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress";
export function MigrationDirectPage() {
const { t } = useTranslation();
const user = useAuthStore();
const bookmarks = useBookmarkStore((state) => state.bookmarks);
const progressItems = useProgressStore((state) => state.items);
const { logout } = useAuth();
const navigate = useNavigate();
const { migrate } = useMigration();
@ -76,6 +81,49 @@ export function MigrationDirectPage() {
<Paragraph className="text-lg max-w-md">
{t("migration.direct.description")}
</Paragraph>
<SettingsCard>
<div className="space-y-4">
<h3 className="font-bold text-white text-lg">
{t("migration.preview.downloadDescription")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.CLOCK} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.progress")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{Object.keys(progressItems).length}
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.BOOKMARK} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.bookmarks")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{Object.keys(bookmarks).length}
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.SETTINGS} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.settings")}
</span>
</div>
<div className="text-xl font-bold mt-2"></div>
</div>
</div>
</div>
</SettingsCard>
<SettingsCard>
<div className="flex justify-between items-center">
<p className="font-bold text-white">

View file

@ -8,12 +8,14 @@ import { SettingsCard } from "@/components/layout/SettingsCard";
import { Stepper } from "@/components/layout/Stepper";
import { CenterContainer } from "@/components/layout/ThinContainer";
import { Divider } from "@/components/utils/Divider";
import { Heading2, Heading3, Paragraph } from "@/components/utils/Text";
import { Heading2, Paragraph } from "@/components/utils/Text";
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { useAuthStore } from "@/stores/auth";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useGroupOrderStore } from "@/stores/groupOrder";
import { useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
export function MigrationDownloadPage() {
const { t } = useTranslation();
@ -21,6 +23,27 @@ export function MigrationDownloadPage() {
const navigate = useNavigate();
const bookmarks = useBookmarkStore((s) => s.bookmarks);
const progress = useProgressStore((s) => s.items);
const groupOrder = useGroupOrderStore((s) => s.groupOrder);
// Get data from localStorage directly to ensure we have the persisted data
const getPersistedData = (key: string) => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored).state : {};
} catch {
return {};
}
};
const persistedBookmarks = getPersistedData("__MW::bookmarks");
const persistedProgress = getPersistedData("__MW::progress");
const persistedGroupOrder = getPersistedData("__MW::groupOrder");
const persistedPreferences = getPersistedData("__MW::preferences");
const persistedSubtitles = getPersistedData("__MW::subtitles");
const persistedTheme = getPersistedData("__MW::theme");
const persistedLocale = getPersistedData("__MW::locale");
const subtitleLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
const handleDownload = useCallback(() => {
@ -30,29 +53,71 @@ export function MigrationDownloadPage() {
profile: user.account?.profile,
deviceName: user.account?.deviceName,
},
bookmarks,
progress,
bookmarks: persistedBookmarks.bookmarks || bookmarks,
progress: persistedProgress.items || progress,
groupOrder: persistedGroupOrder.groupOrder || groupOrder,
settings: {
...persistedPreferences,
defaultSubtitleLanguage:
persistedSubtitles.lastSelectedLanguage || subtitleLanguage,
},
theme: persistedTheme.theme || null,
language: persistedLocale.language || null,
exportDate: new Date().toISOString(),
};
// Convert to JSON and create a downloadable link
const dataStr = JSON.stringify(exportData, null, 2);
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`;
const blob = new Blob([dataStr], {
type: "application/json;charset=utf-8",
});
// Create filename with current date
const exportFileDefaultName = `mw-account-data-${new Date().toISOString().split("T")[0]}.json`;
// Create download link using Blob URL
const url = URL.createObjectURL(blob);
const linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute("download", exportFileDefaultName);
linkElement.click();
linkElement.href = url;
linkElement.download = exportFileDefaultName;
setStatus("success");
try {
// Add link to DOM temporarily and trigger download
document.body.appendChild(linkElement);
linkElement.click();
// Small delay to ensure download is initiated before cleanup
setTimeout(() => {
document.body.removeChild(linkElement);
URL.revokeObjectURL(url);
}, 100);
// Set success status (download is initiated)
setStatus("success");
} catch (downloadError) {
// Clean up on error
document.body.removeChild(linkElement);
URL.revokeObjectURL(url);
throw downloadError;
}
} catch (error) {
console.error("Error during data download:", error);
setStatus("error");
}
}, [bookmarks, progress, user.account]);
}, [
bookmarks,
progress,
user.account,
groupOrder,
persistedBookmarks,
persistedProgress,
persistedGroupOrder,
persistedPreferences,
persistedSubtitles,
persistedTheme,
persistedLocale,
subtitleLanguage,
]);
return (
<MinimalPageLayout>
@ -67,36 +132,48 @@ export function MigrationDownloadPage() {
<Paragraph className="text-lg max-w-md">
{t("migration.download.description")}
</Paragraph>
<SettingsCard>
<div className="flex justify-between items-center">
<Heading3 className="!my-0 !text-type-secondary">
{t("migration.download.items.description")}{" "}
</Heading3>
</div>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<div className="space-y-4">
<h3 className="font-bold text-white text-lg">
{t("migration.preview.downloadDescription")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.CLOCK} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.progress")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{Object.keys(persistedProgress.items || progress).length}
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.BOOKMARK} className="text-xl" />
<span className="font-medium">
{t("migration.download.items.bookmarks")}
</span>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.BOOKMARK} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.bookmarks")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{
Object.keys(persistedBookmarks.bookmarks || bookmarks)
.length
}
</div>
</div>
<div className="text-xl font-bold mt-2">
{Object.keys(bookmarks).length}
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.CLOCK} className="text-xl" />
<span className="font-medium">
{t("migration.download.items.progress")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{Object.keys(progress).length}
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.SETTINGS} className="text-xl" />
<span className="font-medium">
{t("migration.preview.items.settings")}
</span>
</div>
<div className="text-xl font-bold mt-2"></div>
</div>
</div>
</div>

View file

@ -2,6 +2,14 @@ import { ChangeEvent, useCallback, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { bookmarkMediaToInput } from "@/backend/accounts/bookmarks";
import {
importBookmarks,
importGroupOrder,
importProgress,
importSettings,
} from "@/backend/accounts/import";
import { progressMediaItemToInputs } from "@/backend/accounts/progress";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
@ -9,12 +17,17 @@ import { Stepper } from "@/components/layout/Stepper";
import { CenterContainer } from "@/components/layout/ThinContainer";
import { Divider } from "@/components/utils/Divider";
import { Heading2, Paragraph } from "@/components/utils/Text";
import { useAuth } from "@/hooks/auth/useAuth";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
import { PageTitle } from "@/pages/parts/util/PageTitle";
import { useAuthStore } from "@/stores/auth";
import { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks";
import { useGroupOrderStore } from "@/stores/groupOrder";
import { useLanguageStore } from "@/stores/language";
import { usePreferencesStore } from "@/stores/preferences";
import { ProgressMediaItem, useProgressStore } from "@/stores/progress";
import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme";
interface UploadedData {
account?: {
@ -27,6 +40,10 @@ interface UploadedData {
};
bookmarks?: Record<string, BookmarkMediaItem>;
progress?: Record<string, ProgressMediaItem>;
groupOrder?: string[];
settings?: any;
theme?: string | null;
language?: string;
exportDate?: string;
}
@ -34,10 +51,15 @@ export function MigrationUploadPage() {
const { t } = useTranslation();
const navigate = useNavigate();
const user = useAuthStore();
const { importData } = useAuth();
const backendUrl = useBackendUrl();
const fileInputRef = useRef<HTMLInputElement>(null);
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
const replaceProgress = useProgressStore((s) => s.replaceItems);
const setGroupOrder = useGroupOrderStore((s) => s.setGroupOrder);
const preferencesStore = usePreferencesStore();
const subtitleStore = useSubtitleStore();
const setTheme = useThemeStore((s) => s.setTheme);
const setLanguage = useLanguageStore((s) => s.setLanguage);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [status, setStatus] = useState<
"idle" | "success" | "error" | "processing"
@ -133,7 +155,69 @@ export function MigrationUploadPage() {
}
};
const handleImport = useCallback(() => {
const handleBackendImport = useCallback(async () => {
if (!uploadedData || !user.account || !backendUrl) return;
const importPromises = [];
// Import progress
if (
uploadedData.progress &&
Object.keys(uploadedData.progress).length > 0
) {
const progressInputs = Object.entries(uploadedData.progress).flatMap(
([tmdbId, item]) => progressMediaItemToInputs(tmdbId, item),
);
importPromises.push(
importProgress(backendUrl, user.account, progressInputs),
);
}
// Import bookmarks
if (
uploadedData.bookmarks &&
Object.keys(uploadedData.bookmarks).length > 0
) {
const bookmarkInputs = Object.entries(uploadedData.bookmarks).map(
([tmdbId, item]) => bookmarkMediaToInput(tmdbId, item),
);
importPromises.push(
importBookmarks(backendUrl, user.account, bookmarkInputs),
);
}
// Import group order
let groupOrderToImport = uploadedData.groupOrder;
if (!groupOrderToImport || groupOrderToImport.length === 0) {
// Create group order from bookmarks if not provided
const allGroups = new Set<string>();
if (uploadedData.bookmarks) {
Object.values(uploadedData.bookmarks).forEach((bookmark: any) => {
if (Array.isArray(bookmark.group)) {
bookmark.group.forEach((group: string) => allGroups.add(group));
}
});
}
groupOrderToImport = Array.from(allGroups);
}
if (groupOrderToImport && groupOrderToImport.length > 0) {
importPromises.push(
importGroupOrder(backendUrl, user.account, groupOrderToImport),
);
}
// Import settings
if (uploadedData.settings) {
importPromises.push(
importSettings(backendUrl, user.account, uploadedData.settings),
);
}
return Promise.all(importPromises);
}, [uploadedData, user.account, backendUrl]);
const handleImport = useCallback(async () => {
if (status === "processing") {
return;
}
@ -150,24 +234,20 @@ export function MigrationUploadPage() {
replaceProgress(uploadedData.progress);
}
importData(
user.account,
uploadedData.progress || {},
uploadedData.bookmarks || {},
)
.then(() => {
setStatus("success");
})
.catch((error) => {
console.error("Error importing data:", error);
setStatus("error");
});
// Import all data types to backend
try {
await handleBackendImport();
setStatus("success");
} catch (error) {
console.error("Error importing data:", error);
setStatus("error");
}
}, [
replaceBookmarks,
replaceProgress,
uploadedData,
user.account,
importData,
handleBackendImport,
status,
]);
@ -189,11 +269,189 @@ export function MigrationUploadPage() {
);
replaceProgress(uploadedData.progress);
}
if (uploadedData.groupOrder) {
localStorage.setItem(
"__MW::groupOrder",
JSON.stringify({ state: { groupOrder: uploadedData.groupOrder } }),
);
setGroupOrder(uploadedData.groupOrder);
} else {
// If no groupOrder in upload, create one from all groups found in bookmarks
const allGroups = new Set<string>();
if (uploadedData.bookmarks) {
Object.values(uploadedData.bookmarks).forEach((bookmark: any) => {
if (Array.isArray(bookmark.group)) {
bookmark.group.forEach((group: string) => allGroups.add(group));
}
});
}
const groupOrderArray = Array.from(allGroups);
if (groupOrderArray.length > 0) {
localStorage.setItem(
"__MW::groupOrder",
JSON.stringify({ state: { groupOrder: groupOrderArray } }),
);
setGroupOrder(groupOrderArray);
}
}
if (uploadedData.settings) {
// Apply subtitle settings
if (uploadedData.settings.defaultSubtitleLanguage) {
subtitleStore.setLanguage(
uploadedData.settings.defaultSubtitleLanguage,
);
}
if (uploadedData.settings.febboxKey !== undefined) {
preferencesStore.setFebboxKey(uploadedData.settings.febboxKey);
}
if (uploadedData.settings.debridToken !== undefined) {
preferencesStore.setdebridToken(uploadedData.settings.debridToken);
}
if (uploadedData.settings.debridService !== undefined) {
preferencesStore.setdebridService(
uploadedData.settings.debridService,
);
}
if (uploadedData.settings.enableThumbnails !== undefined) {
preferencesStore.setEnableThumbnails(
uploadedData.settings.enableThumbnails,
);
}
if (uploadedData.settings.enableAutoplay !== undefined) {
preferencesStore.setEnableAutoplay(
uploadedData.settings.enableAutoplay,
);
}
if (uploadedData.settings.enableSkipCredits !== undefined) {
preferencesStore.setEnableSkipCredits(
uploadedData.settings.enableSkipCredits,
);
}
if (uploadedData.settings.enableDiscover !== undefined) {
preferencesStore.setEnableDiscover(
uploadedData.settings.enableDiscover,
);
}
if (uploadedData.settings.enableFeatured !== undefined) {
preferencesStore.setEnableFeatured(
uploadedData.settings.enableFeatured,
);
}
if (uploadedData.settings.enableDetailsModal !== undefined) {
preferencesStore.setEnableDetailsModal(
uploadedData.settings.enableDetailsModal,
);
}
if (uploadedData.settings.enableImageLogos !== undefined) {
preferencesStore.setEnableImageLogos(
uploadedData.settings.enableImageLogos,
);
}
if (uploadedData.settings.enableCarouselView !== undefined) {
preferencesStore.setEnableCarouselView(
uploadedData.settings.enableCarouselView,
);
}
if (uploadedData.settings.forceCompactEpisodeView !== undefined) {
preferencesStore.setForceCompactEpisodeView(
uploadedData.settings.forceCompactEpisodeView,
);
}
if (uploadedData.settings.sourceOrder !== undefined) {
preferencesStore.setSourceOrder(uploadedData.settings.sourceOrder);
}
if (uploadedData.settings.enableSourceOrder !== undefined) {
preferencesStore.setEnableSourceOrder(
uploadedData.settings.enableSourceOrder,
);
}
if (uploadedData.settings.lastSuccessfulSource !== undefined) {
preferencesStore.setLastSuccessfulSource(
uploadedData.settings.lastSuccessfulSource,
);
}
if (uploadedData.settings.enableLastSuccessfulSource !== undefined) {
preferencesStore.setEnableLastSuccessfulSource(
uploadedData.settings.enableLastSuccessfulSource,
);
}
if (uploadedData.settings.disabledSources !== undefined) {
preferencesStore.setDisabledSources(
uploadedData.settings.disabledSources,
);
}
if (uploadedData.settings.embedOrder !== undefined) {
preferencesStore.setEmbedOrder(uploadedData.settings.embedOrder);
}
if (uploadedData.settings.enableEmbedOrder !== undefined) {
preferencesStore.setEnableEmbedOrder(
uploadedData.settings.enableEmbedOrder,
);
}
if (uploadedData.settings.disabledEmbeds !== undefined) {
preferencesStore.setDisabledEmbeds(
uploadedData.settings.disabledEmbeds,
);
}
if (uploadedData.settings.proxyTmdb !== undefined) {
preferencesStore.setProxyTmdb(uploadedData.settings.proxyTmdb);
}
if (uploadedData.settings.enableLowPerformanceMode !== undefined) {
preferencesStore.setEnableLowPerformanceMode(
uploadedData.settings.enableLowPerformanceMode,
);
}
if (uploadedData.settings.enableNativeSubtitles !== undefined) {
preferencesStore.setEnableNativeSubtitles(
uploadedData.settings.enableNativeSubtitles,
);
}
if (uploadedData.settings.enableHoldToBoost !== undefined) {
preferencesStore.setEnableHoldToBoost(
uploadedData.settings.enableHoldToBoost,
);
}
if (uploadedData.settings.homeSectionOrder !== undefined) {
preferencesStore.setHomeSectionOrder(
uploadedData.settings.homeSectionOrder,
);
}
if (uploadedData.settings.manualSourceSelection !== undefined) {
preferencesStore.setManualSourceSelection(
uploadedData.settings.manualSourceSelection,
);
}
if (uploadedData.settings.enableDoubleClickToSeek !== undefined) {
preferencesStore.setEnableDoubleClickToSeek(
uploadedData.settings.enableDoubleClickToSeek,
);
}
}
// Apply theme
if (uploadedData.theme !== undefined) {
setTheme(uploadedData.theme);
}
// Apply language
if (uploadedData.language) {
setLanguage(uploadedData.language);
}
setStatus("success");
} catch (e) {
setStatus("error");
}
}, [uploadedData, replaceBookmarks, replaceProgress]);
}, [
uploadedData,
replaceBookmarks,
replaceProgress,
setGroupOrder,
preferencesStore,
subtitleStore,
setTheme,
setLanguage,
]);
return (
<MinimalPageLayout>
@ -208,6 +466,34 @@ export function MigrationUploadPage() {
{t("migration.upload.description")}
</Paragraph>
<SettingsCard className="mb-6">
<div className="space-y-4">
<h3 className="font-bold text-white text-lg">
{t("migration.preview.uploadDescription")}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex flex-col items-center text-center p-4 bg-background rounded-lg">
<Icon icon={Icons.CLOCK} className="text-2xl mb-2" />
<span className="font-medium">
{t("migration.preview.items.progress")}
</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-background rounded-lg">
<Icon icon={Icons.BOOKMARK} className="text-2xl mb-2" />
<span className="font-medium">
{t("migration.preview.items.bookmarks")}
</span>
</div>
<div className="flex flex-col items-center text-center p-4 bg-background rounded-lg">
<Icon icon={Icons.SETTINGS} className="text-2xl mb-2" />
<span className="font-medium">
{t("migration.preview.items.settings")}
</span>
</div>
</div>
</div>
</SettingsCard>
<SettingsCard>
<div className="flex py-6 flex-col space-y-4 items-center justify-center">
<div className="flex flex-col space-y-2 w-full items-center">
@ -270,35 +556,35 @@ export function MigrationUploadPage() {
{uploadedData && (
<SettingsCard className="mt-6">
<Heading2 className="!my-0 !text-type-secondary">
{t("migration.upload.dataPreview")}
{t("migration.upload.previewTitle")}
</Heading2>
<Divider marginClass="my-6 px-8 box-content -mx-8" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.BOOKMARK} className="text-xl" />
<Icon icon={Icons.CLOCK} className="text-xl" />
<span className="font-medium">
{t("migration.upload.items.bookmarks")}
{t("migration.preview.items.progress")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{uploadedData.bookmarks
? Object.keys(uploadedData.bookmarks).length
{uploadedData.progress
? Object.keys(uploadedData.progress).length
: 0}
</div>
</div>
<div className="p-4 bg-background rounded-lg">
<div className="flex items-center gap-2">
<Icon icon={Icons.CLOCK} className="text-xl" />
<Icon icon={Icons.BOOKMARK} className="text-xl" />
<span className="font-medium">
{t("migration.upload.items.progress")}
{t("migration.preview.items.bookmarks")}
</span>
</div>
<div className="text-xl font-bold mt-2">
{uploadedData.progress
? Object.keys(uploadedData.progress).length
{uploadedData.bookmarks
? Object.keys(uploadedData.bookmarks).length
: 0}
</div>
</div>