pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
at main 238 lines 9.1 kB view raw
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}