From b2933a5af6ea4e6b2872eb1020b10a739031b48d Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:57:31 -0600 Subject: [PATCH] add migration upload --- src/assets/locales/en.json | 35 ++- src/pages/migration/Migration.tsx | 24 +- src/pages/migration/MigrationDownload.tsx | 49 ++-- src/pages/migration/MigrationUpload.tsx | 310 ++++++++++++++++++++++ src/setup/App.tsx | 2 + 5 files changed, 398 insertions(+), 22 deletions(-) create mode 100644 src/pages/migration/MigrationUpload.tsx diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index cb88b2a8..5459aaa6 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -216,6 +216,12 @@ "title": "Download data", "quality": "More technical", "action": "Download data" + }, + "upload": { + "title": "Upload Data", + "description": "Upload your previously exported data file to restore your bookmarks and progress on this account.", + "quality": "Restore from backup", + "action": "Upload Data" } } }, @@ -240,8 +246,7 @@ "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": "This will download your data including:", - "profile": "Profile information", + "description": "Download includes:", "bookmarks": "Bookmarked media", "progress": "Watch progress" }, @@ -255,6 +260,32 @@ "login": "Continue to login" } }, + "upload": { + "title": "Upload data", + "description": "Upload your previously exported data file to restore your bookmarks and progress on this account.", + "status": { + "processing": "Processing data...", + "error": "Failed to upload your data. 😿", + "success": "Your data has been uploaded successfully! 🎉" + }, + "file": { + "description": "Select the file you want to upload", + "select": "Select file", + "change": "Change file", + "name": "File name" + }, + "dataPreview": "Preview:", + "items": { + "bookmarks": "Bookmarked media", + "progress": "Watch progress" + }, + "exportedOn": "Exported on", + "button": { + "import": "Import data", + "success": "Import complete", + "home": "Continue to home" + } + }, "back": "Go back" }, "navigation": { diff --git a/src/pages/migration/Migration.tsx b/src/pages/migration/Migration.tsx index 95dab9e6..bd66622d 100644 --- a/src/pages/migration/Migration.tsx +++ b/src/pages/migration/Migration.tsx @@ -1,8 +1,7 @@ import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { Button } from "@/components/buttons/Button"; -import { Icon, Icons } from "@/components/Icon"; +import { Icons } from "@/components/Icon"; import { Stepper } from "@/components/layout/Stepper"; import { BiggerCenterContainer } from "@/components/layout/ThinContainer"; import { VerticalLine } from "@/components/layout/VerticalLine"; @@ -65,6 +64,27 @@ export function MigrationPage() { {t("migration.start.options.download.action")} +
+ + + {t("migration.start.options.or")} + + +
+ navigate("/migration/upload")} + className="flex-1" + > + + {t("migration.start.options.upload.action")} + + diff --git a/src/pages/migration/MigrationDownload.tsx b/src/pages/migration/MigrationDownload.tsx index d137a0a1..859dbb31 100644 --- a/src/pages/migration/MigrationDownload.tsx +++ b/src/pages/migration/MigrationDownload.tsx @@ -3,11 +3,12 @@ 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"; import { Divider } from "@/components/utils/Divider"; -import { Heading2, Paragraph } from "@/components/utils/Text"; +import { Heading2, Heading3, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; import { PageTitle } from "@/pages/parts/util/PageTitle"; import { useAuthStore } from "@/stores/auth"; @@ -40,7 +41,7 @@ export function MigrationDownloadPage() { const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`; // Create filename with current date - const exportFileDefaultName = `p-stream-data-${new Date().toISOString().split("T")[0]}.json`; + const exportFileDefaultName = `mw-account-data-${new Date().toISOString().split("T")[0]}.json`; // Create a temporary link element and click it to trigger download const linkElement = document.createElement("a"); @@ -71,24 +72,36 @@ export function MigrationDownloadPage() {
-

- {t("migration.download.title")} -

+ + {t("migration.download.items.description")}{" "} +
-
-

{t("migration.download.items.description")}

-
    -
  • {t("migration.download.items.profile")}
  • -
  • - {t("migration.download.items.bookmarks")} ( - {Object.keys(bookmarks).length} items) -
  • -
  • - {t("migration.download.items.progress")} ( - {Object.keys(progress).length} items) -
  • -
+ +
+
+
+ + + {t("migration.download.items.bookmarks")} + +
+
+ {Object.keys(bookmarks).length} +
+
+ +
+
+ + + {t("migration.download.items.progress")} + +
+
+ {Object.keys(progress).length} +
+
diff --git a/src/pages/migration/MigrationUpload.tsx b/src/pages/migration/MigrationUpload.tsx new file mode 100644 index 00000000..eaf5d475 --- /dev/null +++ b/src/pages/migration/MigrationUpload.tsx @@ -0,0 +1,310 @@ +import { ChangeEvent, useCallback, useRef, useState } from "react"; +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"; +import { Divider } from "@/components/utils/Divider"; +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 { BookmarkMediaItem, useBookmarkStore } from "@/stores/bookmarks"; +import { ProgressMediaItem, useProgressStore } from "@/stores/progress"; + +interface UploadedData { + account?: { + profile?: { + icon: string; + colorA: string; + colorB: string; + }; + deviceName?: string; + }; + bookmarks?: Record; + progress?: Record; + exportDate?: string; +} + +export function MigrationUploadPage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const user = useAuthStore(); + const fileInputRef = useRef(null); + const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks); + const replaceProgress = useProgressStore((s) => s.replaceItems); + const [selectedFile, setSelectedFile] = useState(null); + const [status, setStatus] = useState< + "idle" | "success" | "error" | "processing" + >("idle"); + const [uploadedData, setUploadedData] = useState(null); + + const handleFileButtonClick = () => { + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const readFile = async (file: File) => { + try { + setStatus("processing"); + const fileContent = await file.text(); + const parsedData = JSON.parse(fileContent); + + // Validate and ensure types match what we expect + const validatedData: UploadedData = { + ...parsedData, + bookmarks: parsedData.bookmarks + ? Object.entries(parsedData.bookmarks).reduce( + (acc, [id, item]: [string, any]) => { + // Ensure type is either "show" or "movie" + if ( + typeof item.type === "string" && + (item.type === "show" || item.type === "movie") + ) { + acc[id] = { + title: item.title || "", + year: typeof item.year === "number" ? item.year : undefined, + poster: item.poster, + type: item.type as "show" | "movie", + updatedAt: + typeof item.updatedAt === "number" + ? item.updatedAt + : Date.now(), + }; + } + return acc; + }, + {} as Record, + ) + : undefined, + + progress: parsedData.progress + ? Object.entries(parsedData.progress).reduce( + (acc, [id, item]: [string, any]) => { + // Ensure type is either "show" or "movie" + if ( + typeof item.type === "string" && + (item.type === "show" || item.type === "movie") + ) { + acc[id] = { + title: item.title || "", + poster: item.poster, + type: item.type as "show" | "movie", + updatedAt: + typeof item.updatedAt === "number" + ? item.updatedAt + : Date.now(), + year: typeof item.year === "number" ? item.year : undefined, + progress: item.progress, + episodes: item.episodes || {}, + seasons: item.seasons || {}, + }; + } + return acc; + }, + {} as Record, + ) + : undefined, + }; + + setUploadedData(validatedData); + setStatus("idle"); + } catch (error) { + console.error("Error parsing JSON file:", error); + setStatus("error"); + } + }; + + const handleFileChange = (e: ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + setSelectedFile(e.target.files[0]); + setStatus("idle"); + setUploadedData(null); + + // Auto-read the file when selected + const file = e.target.files[0]; + readFile(file); + } + }; + + const handleImport = useCallback(() => { + if (!uploadedData) return; + + if (uploadedData.bookmarks) { + replaceBookmarks(uploadedData.bookmarks); + } + + if (uploadedData.progress) { + replaceProgress(uploadedData.progress); + } + + // If user profile data is available in the uploaded data, we could update that too + + setStatus("success"); + }, [replaceBookmarks, replaceProgress, uploadedData]); + + return ( + + + + {user.account ? ( +
+ + + {t("migration.upload.title")} + + + {t("migration.upload.description")} + + + +
+
+

+ {t("migration.upload.file.description")}: +

+
+ + + + + {selectedFile && ( +
+ + {selectedFile.name} + {uploadedData?.exportDate && ( +
+ {t("migration.upload.exportedOn")}:{" "} + {new Date( + uploadedData?.exportDate || "", + ).toLocaleDateString()} +
+ )} +
+
+ )} + + {status === "processing" && ( +
+ + {t("migration.upload.status.processing")} +
+ )} + + {status === "error" && ( +
+ + {t("migration.upload.status.error")} +
+ )} +
+
+ + {uploadedData && ( + + + {t("migration.upload.dataPreview")} + + + +
+
+
+ + + {t("migration.upload.items.bookmarks")} + +
+
+ {uploadedData.bookmarks + ? Object.keys(uploadedData.bookmarks).length + : 0} +
+
+ +
+
+ + + {t("migration.upload.items.progress")} + +
+
+ {uploadedData.progress + ? Object.keys(uploadedData.progress).length + : 0} +
+
+
+ +
+ {status === "success" ? ( +
+ + {t("migration.upload.status.success")} +
+ ) : ( + + )} +
+
+ )} + +
+ + + {status === "success" && ( + + )} +
+
+ ) : ( +
+ + {t("migration.loginRequired")} + + +
+ )} +
+
+ ); +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index fca7a3ee..bbfa9bfd 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -25,6 +25,7 @@ import { LoginPage } from "@/pages/Login"; import { MigrationPage } from "@/pages/migration/Migration"; import { MigrationDirectPage } from "@/pages/migration/MigrationDirect"; import { MigrationDownloadPage } from "@/pages/migration/MigrationDownload"; +import { MigrationUploadPage } from "@/pages/migration/MigrationUpload"; import { OnboardingPage } from "@/pages/onboarding/Onboarding"; import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension"; import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy"; @@ -159,6 +160,7 @@ function App() { path="/migration/download" element={} /> + } /> } /> {/* Support page */}