mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-14 12:20:20 +00:00
update migration to support settings
groups and favorite eps are still broken
This commit is contained in:
parent
2252ab9fed
commit
1e00777c64
6 changed files with 619 additions and 102 deletions
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue