diff --git a/routes.json b/routes.json index 97fd2bc..1fb9273 100644 --- a/routes.json +++ b/routes.json @@ -33,5 +33,10 @@ "path": "guides/reader-settings", "title": "Reader settings | LNReader", "description": "This section relates to the reading experience in the app and navigating the reader." + }, + { + "path": "guides/upgrade", + "title": "Upgrade helper | LNReader", + "description": "Instructions to migrate app from old version to the latest." } ] \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 7e76e65..dd08d89 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,6 +11,7 @@ import Changelogs from "@routes/changelogs"; import Contribute from "@routes/contribute"; import Plugins from "@routes/plugins"; import NotFound from "./404"; +import Upgrade from "@routes/guides/upgrade"; function App() { const theme = useTheme(); @@ -84,6 +85,7 @@ function App() { } /> } /> } /> + } /> diff --git a/src/components/SideBar/index.tsx b/src/components/SideBar/index.tsx index 4a4c2ac..abcf378 100644 --- a/src/components/SideBar/index.tsx +++ b/src/components/SideBar/index.tsx @@ -31,6 +31,7 @@ const guideNavs = [ { title: "Getting started", link: "/guides/getting-started" }, { title: "Backups", link: "/guides/backups" }, { title: "Reader settings", link: "/guides/reader-settings" }, + { title: "Upgrade", link: "/guides/upgrade" }, ]; export default function SideBar(props: Props) { diff --git a/src/routes/guides/upgrade/index.tsx b/src/routes/guides/upgrade/index.tsx new file mode 100644 index 0000000..9a48b4d --- /dev/null +++ b/src/routes/guides/upgrade/index.tsx @@ -0,0 +1,294 @@ +import Layout from "@components/Layout"; +import Page from "@components/Page"; +import { + Alert, + Box, + Button, + IconButton, + styled, + Typography, +} from "@mui/material"; +import UploadFileRoundedIcon from "@mui/icons-material/UploadFileRounded"; +import SimCardDownloadIcon from "@mui/icons-material/SimCardDownload"; +import { useTheme } from "@hooks/useTheme"; +import { useEffect, useState } from "react"; +import { NovelInfo, PluginItem } from "../../../types"; +import PublicIcon from "@mui/icons-material/Public"; + +interface OldNovelInfo { + novelId: number; + sourceUrl: string; + novelUrl: string; + sourceId: number; + source: string; + novelName: string; + novelCover?: string; + novelSummary?: string; + genre?: string; + author?: string; + status?: string; + followed: number; + categoryIds: string; +} + +const VisuallyHiddenInput = styled("input")({ + clip: "rect(0 0 0 0)", + clipPath: "inset(50%)", + height: 1, + overflow: "hidden", + position: "absolute", + bottom: 0, + left: 0, + whiteSpace: "nowrap", + width: 1, +}); + +const isUrlAbsolute = (url: string) => { + if (url) { + if (url.indexOf("//") === 0) { + return true; + } // URL is protocol-relative (= absolute) + if (url.indexOf("://") === -1) { + return false; + } // URL has no protocol (= relative) + if (url.indexOf(".") === -1) { + return false; + } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST) + if (url.indexOf("/") === -1) { + return false; + } // URL does not contain a single slash (= relative) + if (url.indexOf(":") > url.indexOf("/")) { + return false; + } // The first colon comes after the first slash (= relative) + if (url.indexOf("://") < url.indexOf(".")) { + return true; + } // Protocol is defined before first dot (= absolute) + } + return false; // Anything else must be relative +}; + +export default function Upgrade() { + const theme = useTheme(); + const [plugins, setPlugins] = useState([]); + const [loading, setLoading] = useState(true); + const [migratedNovels, setMigratedNovel] = useState([]); + const [requiredPlugins, setRequiredPlugins] = useState([]); + const [showAlert, setShowAlert] = useState(false); + useEffect(() => { + fetch( + "https://raw.githubusercontent.com/LNReader/lnreader-plugins/plugins/v3.0.0/.dist/plugins.min.json" + ) + .then((res) => res.json()) + .then((plugins) => { + setPlugins(plugins); + setLoading(false); + }); + }, []); + + const findSuitedPlugin = (novel: OldNovelInfo) => { + let novelSiteUrl; + try { + novelSiteUrl = new URL(novel.sourceUrl); + } catch { + return undefined; + } + const novelSiteDomain = novelSiteUrl.hostname.replace(/www\./, ""); + for (const plugin of plugins) { + const pluginSiteUrl = new URL(plugin.site); + const pluginSiteDomain = pluginSiteUrl.hostname.replace(/www\./, ""); + if (pluginSiteDomain === novelSiteDomain) { + return plugin; + } + } + + return undefined; + }; + + const migrateNovels = (oldNovels: OldNovelInfo[]) => { + const migratedNovels: NovelInfo[] = []; + const requiredPlugins = new Set(); + for (const oldNovel of oldNovels) { + const plugin = findSuitedPlugin(oldNovel); + let novelUrl = oldNovel.novelUrl; + if (plugin) { + if (isUrlAbsolute(novelUrl)) { + novelUrl = oldNovel.novelUrl.replace(plugin.site, ""); + } + if (plugin.id === "boxnovel") { + novelUrl = "novel/" + novelUrl + "/"; + } + migratedNovels.push({ + id: oldNovel.novelId, + path: novelUrl, + pluginId: plugin.id, + name: oldNovel.novelName, + cover: oldNovel.novelCover, + summary: oldNovel.novelSummary, + author: oldNovel.author, + status: oldNovel.status, + genres: oldNovel.genre, + inLibrary: Boolean(oldNovel.followed), + isLocal: false, + totalPages: 0, + }); + requiredPlugins.add(plugin); + } + } + + setMigratedNovel(migratedNovels); + setRequiredPlugins(Array.from(requiredPlugins)); + }; + + const PluginCard = ({ plugin }: { plugin: PluginItem }) => { + return ( + + ); + }; + + useEffect(() => { + if (showAlert) { + setTimeout(() => setShowAlert(false), 1000); + } + }, [showAlert]); + + return ( + + + {showAlert ? ( + + Copied name. + + ) : null} + + + {!loading && migratedNovels.length ? ( + + ) : null} + + + Required plugins + {requiredPlugins.map((plugin) => ( + + ))} + + + } + /> + + ); +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..45bd33d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,34 @@ +export interface PluginItem { + id: string; + name: string; + version: string; + iconUrl: string; + site: string; + lang: string; +} + +export enum NovelStatus { + Unknown = "Unknown", + Ongoing = "Ongoing", + Completed = "Completed", + Licensed = "Licensed", + PublishingFinished = "Publishing Finished", + Cancelled = "Cancelled", + OnHiatus = "On Hiatus", +} + +export interface NovelInfo { + id: number; + path: string; + pluginId: string; + name: string; + cover?: string; + summary?: string; + author?: string; + artist?: string; + status?: NovelStatus | string; + genres?: string; + inLibrary: boolean; + isLocal: boolean; + totalPages: number; +}