Skip to content

Commit 43d7b69

Browse files
committed
add upgrade route
1 parent 6ae988d commit 43d7b69

File tree

5 files changed

+336
-0
lines changed

5 files changed

+336
-0
lines changed

routes.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,10 @@
3333
"path": "guides/reader-settings",
3434
"title": "Reader settings | LNReader",
3535
"description": "This section relates to the reading experience in the app and navigating the reader."
36+
},
37+
{
38+
"path": "guides/upgrade",
39+
"title": "Upgrade helper | LNReader",
40+
"description": "Instructions to migrate app from old version to the latest."
3641
}
3742
]

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Changelogs from "@routes/changelogs";
1111
import Contribute from "@routes/contribute";
1212
import Plugins from "@routes/plugins";
1313
import NotFound from "./404";
14+
import Upgrade from "@routes/guides/upgrade";
1415

1516
function App() {
1617
const theme = useTheme();
@@ -84,6 +85,7 @@ function App() {
8485
<Route path="getting-started" element={<GettingStarted />} />
8586
<Route path="backups" element={<Backups />} />
8687
<Route path="reader-settings" element={<ReaderSettings />} />
88+
<Route path="upgrade" element={<Upgrade />} />
8789
</Route>
8890
</Route>
8991
<Route path="*" Component={NotFound} />

src/components/SideBar/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const guideNavs = [
3131
{ title: "Getting started", link: "/guides/getting-started" },
3232
{ title: "Backups", link: "/guides/backups" },
3333
{ title: "Reader settings", link: "/guides/reader-settings" },
34+
{ title: "Upgrade", link: "/guides/upgrade" },
3435
];
3536

3637
export default function SideBar(props: Props) {

src/routes/guides/upgrade/index.tsx

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import Layout from "@components/Layout";
2+
import Page from "@components/Page";
3+
import {
4+
Alert,
5+
Box,
6+
Button,
7+
IconButton,
8+
styled,
9+
Typography,
10+
} from "@mui/material";
11+
import UploadFileRoundedIcon from "@mui/icons-material/UploadFileRounded";
12+
import SimCardDownloadIcon from "@mui/icons-material/SimCardDownload";
13+
import { useTheme } from "@hooks/useTheme";
14+
import { useEffect, useState } from "react";
15+
import { NovelInfo, PluginItem } from "../../../types";
16+
import PublicIcon from "@mui/icons-material/Public";
17+
18+
interface OldNovelInfo {
19+
novelId: number;
20+
sourceUrl: string;
21+
novelUrl: string;
22+
sourceId: number;
23+
source: string;
24+
novelName: string;
25+
novelCover?: string;
26+
novelSummary?: string;
27+
genre?: string;
28+
author?: string;
29+
status?: string;
30+
followed: number;
31+
categoryIds: string;
32+
}
33+
34+
const VisuallyHiddenInput = styled("input")({
35+
clip: "rect(0 0 0 0)",
36+
clipPath: "inset(50%)",
37+
height: 1,
38+
overflow: "hidden",
39+
position: "absolute",
40+
bottom: 0,
41+
left: 0,
42+
whiteSpace: "nowrap",
43+
width: 1,
44+
});
45+
46+
const isUrlAbsolute = (url: string) => {
47+
if (url) {
48+
if (url.indexOf("//") === 0) {
49+
return true;
50+
} // URL is protocol-relative (= absolute)
51+
if (url.indexOf("://") === -1) {
52+
return false;
53+
} // URL has no protocol (= relative)
54+
if (url.indexOf(".") === -1) {
55+
return false;
56+
} // URL does not contain a dot, i.e. no TLD (= relative, possibly REST)
57+
if (url.indexOf("/") === -1) {
58+
return false;
59+
} // URL does not contain a single slash (= relative)
60+
if (url.indexOf(":") > url.indexOf("/")) {
61+
return false;
62+
} // The first colon comes after the first slash (= relative)
63+
if (url.indexOf("://") < url.indexOf(".")) {
64+
return true;
65+
} // Protocol is defined before first dot (= absolute)
66+
}
67+
return false; // Anything else must be relative
68+
};
69+
70+
export default function Upgrade() {
71+
const theme = useTheme();
72+
const [plugins, setPlugins] = useState<PluginItem[]>([]);
73+
const [loading, setLoading] = useState(true);
74+
const [migratedNovels, setMigratedNovel] = useState<NovelInfo[]>([]);
75+
const [requiredPlugins, setRequiredPlugins] = useState<PluginItem[]>([]);
76+
const [showAlert, setShowAlert] = useState(false);
77+
useEffect(() => {
78+
fetch(
79+
"https://raw.githubusercontent.com/LNReader/lnreader-plugins/plugins/v3.0.0/.dist/plugins.min.json"
80+
)
81+
.then((res) => res.json())
82+
.then((plugins) => {
83+
setPlugins(plugins);
84+
setLoading(false);
85+
});
86+
}, []);
87+
88+
const findSuitedPlugin = (novel: OldNovelInfo) => {
89+
let novelSiteUrl;
90+
try {
91+
novelSiteUrl = new URL(novel.sourceUrl);
92+
} catch {
93+
return undefined;
94+
}
95+
const novelSiteDomain = novelSiteUrl.hostname.replace(/www\./, "");
96+
for (const plugin of plugins) {
97+
const pluginSiteUrl = new URL(plugin.site);
98+
const pluginSiteDomain = pluginSiteUrl.hostname.replace(/www\./, "");
99+
if (pluginSiteDomain === novelSiteDomain) {
100+
return plugin;
101+
}
102+
}
103+
104+
return undefined;
105+
};
106+
107+
const migrateNovels = (oldNovels: OldNovelInfo[]) => {
108+
const migratedNovels: NovelInfo[] = [];
109+
const requiredPlugins = new Set<PluginItem>();
110+
for (const oldNovel of oldNovels) {
111+
const plugin = findSuitedPlugin(oldNovel);
112+
let novelUrl = oldNovel.novelUrl;
113+
if (plugin) {
114+
if (isUrlAbsolute(novelUrl)) {
115+
novelUrl = oldNovel.novelUrl.replace(plugin.site, "");
116+
}
117+
if (plugin.id === "boxnovel") {
118+
novelUrl = "novel/" + novelUrl + "/";
119+
}
120+
migratedNovels.push({
121+
id: oldNovel.novelId,
122+
path: novelUrl,
123+
pluginId: plugin.id,
124+
name: oldNovel.novelName,
125+
cover: oldNovel.novelCover,
126+
summary: oldNovel.novelSummary,
127+
author: oldNovel.author,
128+
status: oldNovel.status,
129+
genres: oldNovel.genre,
130+
inLibrary: Boolean(oldNovel.followed),
131+
isLocal: false,
132+
totalPages: 0,
133+
});
134+
requiredPlugins.add(plugin);
135+
}
136+
}
137+
138+
setMigratedNovel(migratedNovels);
139+
setRequiredPlugins(Array.from(requiredPlugins));
140+
};
141+
142+
const PluginCard = ({ plugin }: { plugin: PluginItem }) => {
143+
return (
144+
<Button
145+
sx={{
146+
my: 1,
147+
display: "flex",
148+
backgroundColor: theme.surfaceVariant,
149+
p: 1,
150+
borderRadius: 2,
151+
width: "100%",
152+
justifyContent: "left",
153+
textTransform: "none",
154+
}}
155+
onClick={() => {
156+
navigator.clipboard
157+
.writeText(plugin.name)
158+
.then(() => setShowAlert(true));
159+
}}
160+
>
161+
<img src={plugin.iconUrl} alt="icon" height={30} width={30} />
162+
<Box sx={{ ml: 2, textAlign: "left" }}>
163+
<Typography>{plugin.name}</Typography>
164+
<Typography variant="caption">{plugin.id}</Typography>
165+
</Box>
166+
<Box sx={{ flex: 1 }}></Box>
167+
<IconButton sx={{ height: "100%" }} href={plugin.site} target="_blank">
168+
<PublicIcon />
169+
</IconButton>
170+
</Button>
171+
);
172+
};
173+
174+
useEffect(() => {
175+
if (showAlert) {
176+
setTimeout(() => setShowAlert(false), 1000);
177+
}
178+
}, [showAlert]);
179+
180+
return (
181+
<Layout>
182+
<Page
183+
title="Upgrade 1.x.x to 2.0.0"
184+
content={
185+
<Box
186+
sx={{
187+
pt: 2,
188+
}}
189+
>
190+
{showAlert ? (
191+
<Alert
192+
variant="filled"
193+
sx={{
194+
position: "fixed",
195+
top: 70,
196+
right: 10,
197+
zIndex: 1000,
198+
}}
199+
severity="success"
200+
>
201+
Copied name.
202+
</Alert>
203+
) : null}
204+
<Box
205+
sx={{
206+
display: "flex",
207+
flexDirection: { xs: "column", md: "row" },
208+
gap: 4,
209+
}}
210+
>
211+
<Button
212+
component="label"
213+
variant="contained"
214+
tabIndex={-1}
215+
startIcon={
216+
<UploadFileRoundedIcon sx={{ fill: theme.onSecondary }} />
217+
}
218+
disabled={loading}
219+
sx={{ textTransform: "none" }}
220+
>
221+
Upload 1.x.x backup file
222+
<VisuallyHiddenInput
223+
type="file"
224+
onChange={(ev) => {
225+
try {
226+
setLoading(true);
227+
const file = ev.target.files?.[0];
228+
if (file) {
229+
const reader = new FileReader();
230+
reader.onload = (e) => {
231+
try {
232+
if (typeof e.target?.result === "string") {
233+
const oldNovels = JSON.parse(e.target.result);
234+
migrateNovels(oldNovels);
235+
}
236+
} catch (e) {
237+
alert(e);
238+
}
239+
};
240+
reader.readAsText(file);
241+
}
242+
} finally {
243+
setLoading(false);
244+
}
245+
}}
246+
/>
247+
</Button>
248+
{!loading && migratedNovels.length ? (
249+
<Button
250+
component="label"
251+
variant="contained"
252+
tabIndex={-1}
253+
startIcon={
254+
<SimCardDownloadIcon sx={{ fill: theme.onTertiary }} />
255+
}
256+
disabled={loading}
257+
sx={{
258+
textTransform: "none",
259+
bgcolor: theme.tertiary,
260+
color: theme.onTertiary,
261+
}}
262+
onClick={() => {
263+
if (migratedNovels.length) {
264+
const migratedBlob = new Blob(
265+
[JSON.stringify(migratedNovels)],
266+
{
267+
type: "application/json",
268+
}
269+
);
270+
const link = document.createElement("a");
271+
link.href = window.URL.createObjectURL(migratedBlob);
272+
link.setAttribute("download", `migrated-backup.json`);
273+
document.body.appendChild(link);
274+
link.click();
275+
link.parentNode?.removeChild(link);
276+
}
277+
}}
278+
>
279+
Download ({migratedNovels.length} novels)
280+
</Button>
281+
) : null}
282+
</Box>
283+
<Box sx={{ mt: 2 }}>
284+
<Typography variant="h6">Required plugins</Typography>
285+
{requiredPlugins.map((plugin) => (
286+
<PluginCard key={plugin.id} plugin={plugin} />
287+
))}
288+
</Box>
289+
</Box>
290+
}
291+
/>
292+
</Layout>
293+
);
294+
}

src/types/index.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export interface PluginItem {
2+
id: string;
3+
name: string;
4+
version: string;
5+
iconUrl: string;
6+
site: string;
7+
lang: string;
8+
}
9+
10+
export enum NovelStatus {
11+
Unknown = "Unknown",
12+
Ongoing = "Ongoing",
13+
Completed = "Completed",
14+
Licensed = "Licensed",
15+
PublishingFinished = "Publishing Finished",
16+
Cancelled = "Cancelled",
17+
OnHiatus = "On Hiatus",
18+
}
19+
20+
export interface NovelInfo {
21+
id: number;
22+
path: string;
23+
pluginId: string;
24+
name: string;
25+
cover?: string;
26+
summary?: string;
27+
author?: string;
28+
artist?: string;
29+
status?: NovelStatus | string;
30+
genres?: string;
31+
inLibrary: boolean;
32+
isLocal: boolean;
33+
totalPages: number;
34+
}

0 commit comments

Comments
 (0)