pstream is dead; long live pstream
taciturnaxolotl.github.io/pstream-ng/
1import { useCallback, useState } from "react";
2import { useTranslation } from "react-i18next";
3import { useNavigate } from "react-router-dom";
4
5import { Button } from "@/components/buttons/Button";
6import { Icon, Icons } from "@/components/Icon";
7import { SettingsCard } from "@/components/layout/SettingsCard";
8import { Stepper } from "@/components/layout/Stepper";
9import { CenterContainer } from "@/components/layout/ThinContainer";
10import { Divider } from "@/components/utils/Divider";
11import { Heading2, Paragraph } from "@/components/utils/Text";
12import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout";
13import { PageTitle } from "@/pages/parts/util/PageTitle";
14import { useAuthStore } from "@/stores/auth";
15import { useBookmarkStore } from "@/stores/bookmarks";
16import { useGroupOrderStore } from "@/stores/groupOrder";
17import { useProgressStore } from "@/stores/progress";
18import { useSubtitleStore } from "@/stores/subtitles";
19import { useWatchHistoryStore } from "@/stores/watchHistory";
20
21export function MigrationDownloadPage() {
22 const { t } = useTranslation();
23 const user = useAuthStore();
24 const navigate = useNavigate();
25 const bookmarks = useBookmarkStore((s) => s.bookmarks);
26 const progress = useProgressStore((s) => s.items);
27 const watchHistory = useWatchHistoryStore((s) => s.items);
28 const groupOrder = useGroupOrderStore((s) => s.groupOrder);
29
30 // Get data from localStorage directly to ensure we have the persisted data
31 const getPersistedData = (key: string) => {
32 try {
33 const stored = localStorage.getItem(key);
34 return stored ? JSON.parse(stored).state : {};
35 } catch {
36 return {};
37 }
38 };
39
40 const persistedBookmarks = getPersistedData("__MW::bookmarks");
41 const persistedProgress = getPersistedData("__MW::progress");
42 const persistedWatchHistory = getPersistedData("__MW::watchHistory");
43 const persistedGroupOrder = getPersistedData("__MW::groupOrder");
44 const persistedPreferences = getPersistedData("__MW::preferences");
45 const persistedSubtitles = getPersistedData("__MW::subtitles");
46 const persistedTheme = getPersistedData("__MW::theme");
47 const persistedLocale = getPersistedData("__MW::locale");
48
49 const subtitleLanguage = useSubtitleStore((s) => s.lastSelectedLanguage);
50 const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
51
52 const handleDownload = useCallback(() => {
53 try {
54 const exportData = {
55 account: {
56 profile: user.account?.profile,
57 deviceName: user.account?.deviceName,
58 },
59 bookmarks: persistedBookmarks.bookmarks || bookmarks,
60 progress: persistedProgress.items || progress,
61 watchHistory: persistedWatchHistory.items || watchHistory,
62 groupOrder: persistedGroupOrder.groupOrder || groupOrder,
63 settings: {
64 ...persistedPreferences,
65 defaultSubtitleLanguage:
66 persistedSubtitles.lastSelectedLanguage || subtitleLanguage,
67 },
68 theme: persistedTheme.theme || null,
69 language: persistedLocale.language || null,
70 exportDate: new Date().toISOString(),
71 };
72
73 // Convert to JSON and create a downloadable link
74 const dataStr = JSON.stringify(exportData, null, 2);
75 const blob = new Blob([dataStr], {
76 type: "application/json;charset=utf-8",
77 });
78
79 // Create filename with current date
80 const exportFileDefaultName = `mw-account-data-${new Date().toISOString().split("T")[0]}.json`;
81
82 // Create download link using Blob URL
83 const url = URL.createObjectURL(blob);
84 const linkElement = document.createElement("a");
85 linkElement.href = url;
86 linkElement.download = exportFileDefaultName;
87
88 try {
89 // Add link to DOM temporarily and trigger download
90 document.body.appendChild(linkElement);
91 linkElement.click();
92
93 // Small delay to ensure download is initiated before cleanup
94 setTimeout(() => {
95 document.body.removeChild(linkElement);
96 URL.revokeObjectURL(url);
97 }, 100);
98
99 // Set success status (download is initiated)
100 setStatus("success");
101 } catch (downloadError) {
102 // Clean up on error
103 document.body.removeChild(linkElement);
104 URL.revokeObjectURL(url);
105 throw downloadError;
106 }
107 } catch (error) {
108 console.error("Error during data download:", error);
109 setStatus("error");
110 }
111 }, [
112 bookmarks,
113 progress,
114 watchHistory,
115 user.account,
116 groupOrder,
117 persistedBookmarks,
118 persistedProgress,
119 persistedWatchHistory,
120 persistedGroupOrder,
121 persistedPreferences,
122 persistedSubtitles,
123 persistedTheme,
124 persistedLocale,
125 subtitleLanguage,
126 ]);
127
128 return (
129 <MinimalPageLayout>
130 <PageTitle subpage k="global.pages.migration" />
131 <CenterContainer>
132 <div>
133 <Stepper steps={2} current={2} className="mb-12" />
134 <Heading2 className="!text-4xl">
135 {t("migration.download.title")}
136 </Heading2>
137 <div className="space-y-6 max-w-3xl mx-auto">
138 <Paragraph className="text-lg max-w-md">
139 {t("migration.download.description")}
140 </Paragraph>
141
142 <SettingsCard>
143 <div className="space-y-4">
144 <h3 className="font-bold text-white text-lg">
145 {t("migration.preview.downloadDescription")}
146 </h3>
147 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
148 <div className="p-4 bg-background rounded-lg">
149 <div className="flex items-center gap-2">
150 <Icon icon={Icons.CLOCK} className="text-xl" />
151 <span className="font-medium">
152 {t("migration.preview.items.progress")}
153 </span>
154 </div>
155 <div className="text-xl font-bold mt-2">
156 {Object.keys(persistedProgress.items || progress).length}
157 </div>
158 </div>
159
160 <div className="p-4 bg-background rounded-lg">
161 <div className="flex items-center gap-2">
162 <Icon icon={Icons.BOOKMARK} className="text-xl" />
163 <span className="font-medium">
164 {t("migration.preview.items.bookmarks")}
165 </span>
166 </div>
167 <div className="text-xl font-bold mt-2">
168 {
169 Object.keys(persistedBookmarks.bookmarks || bookmarks)
170 .length
171 }
172 </div>
173 </div>
174
175 <div className="p-4 bg-background rounded-lg">
176 <div className="flex items-center gap-2">
177 <Icon icon={Icons.CLOCK} className="text-xl" />
178 <span className="font-medium">
179 {t("migration.preview.items.progress")}
180 </span>
181 </div>
182 <div className="text-xl font-bold mt-2">
183 {
184 Object.keys(persistedWatchHistory.items || watchHistory)
185 .length
186 }
187 </div>
188 </div>
189
190 <div className="p-4 bg-background rounded-lg">
191 <div className="flex items-center gap-2">
192 <Icon icon={Icons.SETTINGS} className="text-xl" />
193 <span className="font-medium">
194 {t("migration.preview.items.settings")}
195 </span>
196 </div>
197 <div className="text-xl font-bold mt-2">✓</div>
198 </div>
199 </div>
200 </div>
201 </SettingsCard>
202 </div>
203 <Divider />
204 <div className="flex justify-between">
205 <Button theme="secondary" onClick={() => navigate("/migration")}>
206 {t("migration.back")}
207 </Button>
208 {status !== "success" && (
209 <Button theme="purple" onClick={handleDownload}>
210 {t("migration.download.button.download")}
211 </Button>
212 )}
213
214 {status === "success" && (
215 <div>
216 <Button theme="purple" onClick={() => navigate("/")}>
217 {t("migration.download.button.home")}
218 </Button>
219 </div>
220 )}
221 </div>
222 <div className="flex justify-center pt-4">
223 {status === "success" && (
224 <p className="text-green-600 mt-4">
225 {t("migration.download.status.success")}
226 </p>
227 )}
228 {status === "error" && (
229 <p className="text-red-600 mt-4">
230 {t("migration.download.status.error")}
231 </p>
232 )}
233 </div>
234 </div>
235 </CenterContainer>
236 </MinimalPageLayout>
237 );
238}