fix: Refactor and Fix EPUB Export (Closes #1611)
This commit is contained in:
Generated
+30
-15
@@ -50,6 +50,7 @@
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-error-boundary": "^2.0.0",
|
||||
"react-native-file-access": "^3.2.0",
|
||||
"react-native-gesture-handler": "~2.25.0",
|
||||
"react-native-lottie-splash-screen": "^1.1.2",
|
||||
"react-native-mmkv": "^3.2.0",
|
||||
@@ -151,6 +152,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
|
||||
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
@@ -182,6 +184,7 @@
|
||||
"integrity": "sha512-q8rjOuadH0V6Zo4XLMkJ3RMQ9MSBqwaDByyYB0izsYdaIWGNLmEblbCOf1vyFHICcg16CD7Fsi51vcQnYxmt6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
@@ -2125,6 +2128,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
|
||||
"integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -3953,6 +3957,7 @@
|
||||
"integrity": "sha512-DyKptlG78XPFo7tDod+we5a3R+U9qjyhaVFbOPvH4pFNu5Dehewtol/srl44K6Cszq0aEMlAJZ3juk0W4WnOJA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-native-community/cli-clean": "18.0.0",
|
||||
"@react-native-community/cli-config": "18.0.0",
|
||||
@@ -4730,6 +4735,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.8.tgz",
|
||||
"integrity": "sha512-ryKd/qNigi1pUp6mBb2pq75ese7AZ/Cl3xEmTG6PcUGMfMqAMMrmmVbgiys0h8zCGY2tSBSqnDHbGW1/ZtOoKg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@react-navigation/core": "^7.9.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
@@ -5015,6 +5021,7 @@
|
||||
"integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -5083,6 +5090,7 @@
|
||||
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
@@ -5117,6 +5125,7 @@
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
@@ -5380,6 +5389,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -6268,6 +6278,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
@@ -7637,6 +7648,7 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@@ -8304,6 +8316,7 @@
|
||||
"resolved": "https://registry.npmjs.org/expo/-/expo-53.0.9.tgz",
|
||||
"integrity": "sha512-UFG68aVOpccg3s++S3pbtI3YCQCnlu/TFvhnQ5vaD3vhOox1Uk/f2O2T95jmwA/EvKvetqGj34lys3DNXvPqgQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.0",
|
||||
"@expo/cli": "0.24.13",
|
||||
@@ -8420,6 +8433,7 @@
|
||||
"resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.1.tgz",
|
||||
"integrity": "sha512-d+xrHYvSM9WB42wj8vP9OOFWyxed5R1evphfDb6zYBmC1dA9Hf89FpT7TNFtj2Bk3clTnpmVqQTCYbbA2P3CLg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fontfaceobserver": "^2.1.0"
|
||||
},
|
||||
@@ -8611,17 +8625,6 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/babel-plugin-react-compiler": {
|
||||
"version": "19.0.0-beta-ebf51a3-20250411",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.0.0-beta-ebf51a3-20250411.tgz",
|
||||
"integrity": "sha512-q84bNR9JG1crykAlJUt5Ud0/5BUyMFuQww/mrwIQDFBaxsikqBDj3f/FNDsVd2iR26A1HvXKWPEIfgJDv8/V2g==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/babel-preset-expo": {
|
||||
"version": "13.1.11",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.1.11.tgz",
|
||||
@@ -11461,7 +11464,8 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/lottie-ios/-/lottie-ios-3.2.3.tgz",
|
||||
"integrity": "sha512-mubYMN6+1HXa8z3EJKBvNBkl4UoVM4McjESeB2PgvRMSngmJtC5yUMRdhbbrIAn5Liu3hFGao/14s5hQIgtkRQ==",
|
||||
"license": "Apache-2.0"
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
@@ -12922,6 +12926,7 @@
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
@@ -13039,6 +13044,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -13223,6 +13229,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -13281,6 +13288,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.2.tgz",
|
||||
"integrity": "sha512-AnGzb56JvU5YCL7cAwg10+ewDquzvmgrMddiBM0GAWLwQM/6DJfGd2ZKrMuKKehHerpDDZgG+EY64gk3x3dEkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/create-cache-key-function": "^29.7.0",
|
||||
"@react-native/assets-registry": "0.79.2",
|
||||
@@ -13415,9 +13423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-file-access": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.1.1.tgz",
|
||||
"integrity": "sha512-4KUpBAsnWJa+AQf1tUbLdHO+1pyiZMTeq3NPf5XOGdz1O5CwIrVkrzl+gkN7ffmUa5JyoYHyXUtwScmA+z0Tlg==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.2.0.tgz",
|
||||
"integrity": "sha512-3G0Ma3FvV99Ne7lbwHyZKqLMZVrpIr6fAlIYZQxkRDSZZJ1UtJlGi0VqbJXnJFB26a7C7PEPyVBoctmKr7DzaA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
@@ -13429,6 +13437,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.25.0.tgz",
|
||||
"integrity": "sha512-NPjJi6mislXxvjxQPU9IYwBjb1Uejp8GvAbE1Lhh+xMIMEvmgAvVIp5cz1P+xAbV6uYcRRArm278+tEInGOqWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@egjs/hammerjs": "^2.0.17",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
@@ -13509,6 +13518,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.7.1.tgz",
|
||||
"integrity": "sha512-cBSr6xw4g5N7Kd3VGWcf+kmaH7iBWb0DXAf2bVo3bXkzBcBbTOmYSvc0LVLHhUPW8nEq5WjT9LCIYAzgF++EXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
@@ -13564,6 +13574,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz",
|
||||
"integrity": "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
|
||||
"@babel/plugin-transform-class-properties": "^7.0.0-0",
|
||||
@@ -13599,6 +13610,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.4.0.tgz",
|
||||
"integrity": "sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
@@ -13621,6 +13633,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.10.0.tgz",
|
||||
"integrity": "sha512-Tw21NGuXm3PbiUGtZd0AnXirUixaAbPXDjNR0baBH7/WJDaDTTELLcQ7QRXuqAWbmr/EVCrKj1348ei1KFIr8A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"react-freeze": "^1.0.0",
|
||||
"warn-once": "^0.1.0"
|
||||
@@ -13671,6 +13684,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.13.5.tgz",
|
||||
"integrity": "sha512-MfC2B+woL4Hlj2WCzcb1USySKk+SteXnUKmKktOk/H/AQy5+LuVdkPKm8SknJ0/RxaxhZ48WBoTRGaqgR137hw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"invariant": "2.2.4"
|
||||
@@ -15573,6 +15587,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-error-boundary": "^2.0.0",
|
||||
"react-native-file-access": "^3.2.0",
|
||||
"react-native-gesture-handler": "~2.25.0",
|
||||
"react-native-lottie-splash-screen": "^1.1.2",
|
||||
"react-native-mmkv": "^3.2.0",
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Portal } from 'react-native-paper';
|
||||
import ChooseEpubLocationModal from './ChooseEpubLocationModal';
|
||||
import { StatusBar, StyleProp, ViewStyle } from 'react-native';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
|
||||
import EpubBuilder from '@cd-z/react-native-epub-creator';
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
|
||||
import { useChapterReaderSettings } from '@hooks/persisted';
|
||||
import { useBoolean } from '@hooks/index';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { NOVEL_STORAGE } from '@utils/Storages';
|
||||
import NativeFile from '@specs/NativeFile';
|
||||
import { MaterialDesignIconName } from '@type/icon';
|
||||
|
||||
interface EpubIconButtonProps {
|
||||
theme: ThemeColors;
|
||||
novel?: NovelInfo;
|
||||
chapters: ChapterInfo[];
|
||||
anchor: (props: {
|
||||
icon: MaterialDesignIconName;
|
||||
onPress: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
size?: number;
|
||||
}) => React.JSX.Element;
|
||||
}
|
||||
|
||||
const EpubIconButton: React.FC<EpubIconButtonProps> = ({
|
||||
theme,
|
||||
novel,
|
||||
chapters,
|
||||
anchor: Anchor,
|
||||
}) => {
|
||||
const {
|
||||
value: isVisible,
|
||||
setTrue: showModal,
|
||||
setFalse: hideModal,
|
||||
} = useBoolean(false);
|
||||
const readerSettings = useChapterReaderSettings();
|
||||
const {
|
||||
epubUseAppTheme = false,
|
||||
epubUseCustomCSS = false,
|
||||
epubUseCustomJS = false,
|
||||
} = useChapterReaderSettings();
|
||||
|
||||
const epubStyle = useMemo(
|
||||
() =>
|
||||
!novel
|
||||
? ''
|
||||
: `${
|
||||
epubUseAppTheme
|
||||
? `
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
padding-bottom: 40px;
|
||||
font-size: ${readerSettings.textSize}px;
|
||||
color: ${readerSettings.textColor};
|
||||
text-align: ${readerSettings.textAlign};
|
||||
line-height: ${readerSettings.lineHeight};
|
||||
font-family: "${readerSettings.fontFamily}";
|
||||
background-color: "${readerSettings.theme}";
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.primary};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
epubUseCustomCSS
|
||||
? readerSettings.customCSS
|
||||
.replace(
|
||||
RegExp(`#sourceId-${novel.pluginId}\\s*\\{`, 'g'),
|
||||
'body {',
|
||||
)
|
||||
.replace(RegExp(`#sourceId-${novel.pluginId}[^.#A-Z]*`, 'gi'), '')
|
||||
: ''
|
||||
}`,
|
||||
[
|
||||
novel,
|
||||
epubUseAppTheme,
|
||||
readerSettings.padding,
|
||||
readerSettings.textSize,
|
||||
readerSettings.textColor,
|
||||
readerSettings.textAlign,
|
||||
readerSettings.lineHeight,
|
||||
readerSettings.fontFamily,
|
||||
readerSettings.theme,
|
||||
readerSettings.customCSS,
|
||||
theme.primary,
|
||||
epubUseCustomCSS,
|
||||
],
|
||||
);
|
||||
|
||||
const epubJS = useMemo(
|
||||
() =>
|
||||
!novel
|
||||
? ''
|
||||
: `
|
||||
let novelName = "${novel.name}";
|
||||
let chapterName = "";
|
||||
let sourceId =${novel.pluginId};
|
||||
let chapterId ="";
|
||||
let novelId =${novel.id};
|
||||
let html = document.querySelector("chapter").innerHTML;
|
||||
|
||||
${readerSettings.customJS}
|
||||
`,
|
||||
[novel, readerSettings],
|
||||
);
|
||||
|
||||
const createEpub = async (uri: string) => {
|
||||
if (!novel) {
|
||||
return;
|
||||
}
|
||||
const epub = new EpubBuilder(
|
||||
{
|
||||
title: novel.name,
|
||||
fileName: novel.name.replace(/\s/g, ''),
|
||||
language: 'en',
|
||||
cover: novel.cover,
|
||||
description: novel.summary,
|
||||
author: novel.author,
|
||||
bookId: novel.pluginId.toString(),
|
||||
stylesheet: epubStyle || undefined,
|
||||
js: epubUseCustomJS ? epubJS : undefined,
|
||||
},
|
||||
uri,
|
||||
);
|
||||
|
||||
try {
|
||||
await epub.prepare();
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const chapter = chapters[i];
|
||||
const filePath = `${NOVEL_STORAGE}/${novel.pluginId}/${novel.id}/${chapter.id}/index.html`;
|
||||
if (NativeFile.exists(filePath)) {
|
||||
const downloaded = NativeFile.readFile(filePath);
|
||||
|
||||
await epub.addChapter({
|
||||
title:
|
||||
chapter.name?.trim() ?? 'Chapter ' + (chapter.chapterNumber || i),
|
||||
fileName: 'Chapter' + i,
|
||||
htmlBody: `<chapter data-novel-id='${novel.pluginId}' data-chapter-id='${chapter.id}'>${downloaded}</chapter>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const epubFilePath = await epub.save();
|
||||
showToast('Epub file saved at: ' + epubFilePath);
|
||||
} catch (error) {
|
||||
showToast('Cannot create because: ' + error);
|
||||
await epub.discardChanges();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Anchor icon="book-arrow-down-outline" onPress={showModal} />
|
||||
<Portal>
|
||||
<ChooseEpubLocationModal
|
||||
isVisible={isVisible}
|
||||
hideModal={hideModal}
|
||||
onSubmit={createEpub}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default EpubIconButton;
|
||||
+10
-12
@@ -10,13 +10,13 @@ import { getString } from '@strings/translations';
|
||||
import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
|
||||
import { showToast } from '@utils/showToast';
|
||||
|
||||
interface ChooseEpubLocationModalProps {
|
||||
interface ExportEpubModalProps {
|
||||
isVisible: boolean;
|
||||
onSubmit?: (uri: string) => void;
|
||||
hideModal: () => void;
|
||||
}
|
||||
|
||||
const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
|
||||
isVisible,
|
||||
onSubmit: onSubmitProp,
|
||||
hideModal,
|
||||
@@ -67,12 +67,12 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
<Modal visible={isVisible} onDismiss={onDismiss}>
|
||||
<View>
|
||||
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
|
||||
{getString('novelScreen.convertToEpubModal.chooseLocation')}
|
||||
{getString('novelScreen.exportEpubModal.title')}
|
||||
</Text>
|
||||
<TextInput
|
||||
onChangeText={setUri}
|
||||
value={uri}
|
||||
placeholder={getString('novelScreen.convertToEpubModal.pathToFolder')}
|
||||
placeholder={getString('novelScreen.exportEpubModal.selectFolder')}
|
||||
onSubmitEditing={onSubmit}
|
||||
mode="outlined"
|
||||
theme={{ colors: { ...theme } }}
|
||||
@@ -88,22 +88,20 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
</View>
|
||||
<View style={styles.settings}>
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.convertToEpubModal.useReaderTheme')}
|
||||
label={getString('novelScreen.exportEpubModal.applyReaderTheme')}
|
||||
value={useAppTheme.value}
|
||||
onPress={useAppTheme.toggle}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.convertToEpubModal.useCustomCSS')}
|
||||
label={getString('novelScreen.exportEpubModal.includeCustomCSS')}
|
||||
value={useCustomCSS.value}
|
||||
onPress={useCustomCSS.toggle}
|
||||
theme={theme}
|
||||
/>
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.convertToEpubModal.useCustomJS')}
|
||||
description={getString(
|
||||
'novelScreen.convertToEpubModal.useCustomJSWarning',
|
||||
)}
|
||||
label={getString('novelScreen.exportEpubModal.includeCustomJS')}
|
||||
description={getString('novelScreen.exportEpubModal.customJSWarning')}
|
||||
value={useCustomJS.value}
|
||||
onPress={useCustomJS.toggle}
|
||||
theme={theme}
|
||||
@@ -111,7 +109,7 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
</View>
|
||||
<List.InfoItem
|
||||
style={styles.infoItem}
|
||||
title={getString('novelScreen.convertToEpubModal.chaptersWarning')}
|
||||
title={getString('novelScreen.exportEpubModal.downloadedChaptersOnly')}
|
||||
theme={theme}
|
||||
/>
|
||||
<View style={styles.modalFooterCtn}>
|
||||
@@ -122,7 +120,7 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ChooseEpubLocationModal;
|
||||
export default ExportEpubModal;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
infoItem: {
|
||||
@@ -0,0 +1,187 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Appbar, Portal } from 'react-native-paper';
|
||||
import { StatusBar } from 'react-native';
|
||||
|
||||
import EpubBuilder from '@cd-z/react-native-epub-creator';
|
||||
import NativeFile from '@specs/NativeFile';
|
||||
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
import { useChapterReaderSettings, useTheme } from '@hooks/persisted';
|
||||
import { useBoolean } from '@hooks/index';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { NOVEL_STORAGE } from '@utils/Storages';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import ExportEpubModal from './ExportEpubModal';
|
||||
|
||||
interface ExportNovelAsEpubButtonProps {
|
||||
novel?: NovelInfo;
|
||||
chapters: ChapterInfo[];
|
||||
}
|
||||
|
||||
const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
|
||||
novel,
|
||||
chapters,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
value: isModalVisible,
|
||||
setTrue: showModal,
|
||||
setFalse: hideModal,
|
||||
} = useBoolean(false);
|
||||
|
||||
const readerSettings = useChapterReaderSettings();
|
||||
const {
|
||||
epubUseAppTheme = false,
|
||||
epubUseCustomCSS = false,
|
||||
epubUseCustomJS = false,
|
||||
} = readerSettings;
|
||||
|
||||
const epubStylesheet = useMemo(() => {
|
||||
if (!novel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const appThemeStyles = epubUseAppTheme
|
||||
? `
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overflow-x: hidden;
|
||||
padding-top: ${StatusBar.currentHeight};
|
||||
word-wrap: break-word;
|
||||
}
|
||||
body {
|
||||
padding-left: ${readerSettings.padding}%;
|
||||
padding-right: ${readerSettings.padding}%;
|
||||
padding-bottom: 40px;
|
||||
font-size: ${readerSettings.textSize}px;
|
||||
color: ${readerSettings.textColor};
|
||||
text-align: ${readerSettings.textAlign};
|
||||
line-height: ${readerSettings.lineHeight};
|
||||
font-family: "${readerSettings.fontFamily}";
|
||||
background-color: "${readerSettings.theme}";
|
||||
}
|
||||
hr {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
color: ${theme.primary};
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}`
|
||||
: '';
|
||||
|
||||
const customStyles = epubUseCustomCSS
|
||||
? readerSettings.customCSS
|
||||
.replace(RegExp(`#sourceId-${novel.pluginId}\\s*\\{`, 'g'), 'body {')
|
||||
.replace(RegExp(`#sourceId-${novel.pluginId}[^.#A-Z]*`, 'gi'), '')
|
||||
: '';
|
||||
|
||||
return appThemeStyles + customStyles;
|
||||
}, [novel, epubUseAppTheme, epubUseCustomCSS, readerSettings, theme.primary]);
|
||||
|
||||
const epubJavaScript = useMemo(() => {
|
||||
if (!novel) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `
|
||||
let novelName = "${novel.name}";
|
||||
let chapterName = "";
|
||||
let sourceId = ${novel.pluginId};
|
||||
let chapterId = "";
|
||||
let novelId = ${novel.id};
|
||||
let html = document.querySelector("chapter").innerHTML;
|
||||
|
||||
${readerSettings.customJS}
|
||||
`;
|
||||
}, [novel, readerSettings]);
|
||||
|
||||
const exportNovelAsEpub = async (destinationUri: string) => {
|
||||
if (!novel) {
|
||||
showToast(getString('novelScreen.epub.noNovelSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
let epub: EpubBuilder | undefined;
|
||||
|
||||
try {
|
||||
epub = new EpubBuilder(
|
||||
{
|
||||
title: novel.name,
|
||||
fileName: novel.name.replace(/\s/g, ''),
|
||||
language: 'en',
|
||||
cover: novel.cover,
|
||||
description: novel.summary,
|
||||
author: novel.author,
|
||||
bookId: novel.pluginId.toString(),
|
||||
stylesheet: epubStylesheet || undefined,
|
||||
js: epubUseCustomJS ? epubJavaScript : undefined,
|
||||
},
|
||||
destinationUri,
|
||||
);
|
||||
|
||||
await epub.prepare();
|
||||
|
||||
let addedChapters = 0;
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const chapter = chapters[i];
|
||||
const chapterFilePath = `${NOVEL_STORAGE}/${novel.pluginId}/${novel.id}/${chapter.id}/index.html`;
|
||||
|
||||
if (NativeFile.exists(chapterFilePath)) {
|
||||
const chapterContent = NativeFile.readFile(chapterFilePath);
|
||||
|
||||
await epub.addChapter({
|
||||
title:
|
||||
chapter.name?.trim() || `Chapter ${chapter.chapterNumber || i}`,
|
||||
fileName: `Chapter${i}`,
|
||||
htmlBody: `<chapter data-novel-id='${novel.pluginId}' data-chapter-id='${chapter.id}'>${chapterContent}</chapter>`,
|
||||
});
|
||||
|
||||
addedChapters++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedChapters === 0) {
|
||||
showToast(getString('novelScreen.epub.noDownloadedChapters'));
|
||||
await epub.discardChanges();
|
||||
return;
|
||||
}
|
||||
|
||||
await epub.save();
|
||||
showToast(
|
||||
getString('novelScreen.epub.exportSuccess', {
|
||||
chapters: addedChapters.toString(),
|
||||
}),
|
||||
);
|
||||
} catch (error: any) {
|
||||
showToast(
|
||||
getString('novelScreen.epub.exportFailed', {
|
||||
error: error.message || error,
|
||||
}),
|
||||
);
|
||||
await epub?.discardChanges();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Action icon="book-arrow-down-outline" onPress={showModal} />
|
||||
<Portal>
|
||||
<ExportEpubModal
|
||||
isVisible={isModalVisible}
|
||||
hideModal={hideModal}
|
||||
onSubmit={exportNovelAsEpub}
|
||||
/>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportNovelAsEpubButton;
|
||||
@@ -11,7 +11,7 @@ import Animated, {
|
||||
interpolateColor,
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated';
|
||||
import EpubIconButton from './EpubIconButton';
|
||||
import ExportNovelAsEpubButton from './ExportNovelAsEpubButton';
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import { MaterialDesignIconName } from '@type/icon';
|
||||
@@ -165,13 +165,7 @@ const NovelAppbar = ({
|
||||
<Appbar.BackAction onPress={goBack} />
|
||||
|
||||
<View style={styles.row}>
|
||||
<EpubIconButton
|
||||
theme={theme}
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
anchor={AppbarAction}
|
||||
/>
|
||||
|
||||
<ExportNovelAsEpubButton novel={novel} chapters={chapters} />
|
||||
<AppbarAction icon="share-variant" onPress={shareNovel} />
|
||||
<AppbarAction
|
||||
icon="text-box-search-outline"
|
||||
|
||||
@@ -2,3 +2,5 @@ export const coverPlaceholderColor = '#8888881F';
|
||||
|
||||
export const filterColor: (isDark: boolean) => string = isDark =>
|
||||
isDark ? '#FFC107' : '#FFC107';
|
||||
|
||||
export const borderColor = 'rgba(0, 0, 0, 0.12)';
|
||||
|
||||
@@ -361,14 +361,20 @@
|
||||
"chapterChapnum": "Chapter %{num}",
|
||||
"chapters": "chapters",
|
||||
"continueReading": "Continue reading",
|
||||
"convertToEpubModal": {
|
||||
"chaptersWarning": "Only downloaded chapters are included in the EPUB",
|
||||
"chooseLocation": "Select EPUB storage directory",
|
||||
"pathToFolder": "Path to EPUB save folder",
|
||||
"useCustomCSS": "Use custom CSS",
|
||||
"useCustomJS": "Use custom JS",
|
||||
"useCustomJSWarning": "May not be supported by all EPUB readers",
|
||||
"useReaderTheme": "Use reader theme"
|
||||
"exportEpubModal": {
|
||||
"applyReaderTheme": "Apply reader theme to EPUB",
|
||||
"customJSWarning": "Custom JavaScript may not be supported by all EPUB readers",
|
||||
"downloadedChaptersOnly": "Only downloaded chapters will be included in the EPUB file",
|
||||
"includeCustomCSS": "Include Custom CSS",
|
||||
"includeCustomJS": "Include Custom JS",
|
||||
"selectFolder": "Select destination folder for EPUB file",
|
||||
"title": "Export Novel as EPUB"
|
||||
},
|
||||
"epub": {
|
||||
"exportFailed": "Failed to export EPUB: %{error}",
|
||||
"exportSuccess": "Successfully exported %{chapters} chapters as EPUB",
|
||||
"noDownloadedChapters": "No downloaded chapters found. Please download chapters before exporting.",
|
||||
"noNovelSelected": "No novel selected for export"
|
||||
},
|
||||
"coverSaved": "Cover saved",
|
||||
"coverNotSaved": "Cover not saved",
|
||||
|
||||
+11
-7
@@ -296,13 +296,17 @@ export interface StringMap {
|
||||
'novelScreen.chapterChapnum': 'string';
|
||||
'novelScreen.chapters': 'string';
|
||||
'novelScreen.continueReading': 'string';
|
||||
'novelScreen.convertToEpubModal.chaptersWarning': 'string';
|
||||
'novelScreen.convertToEpubModal.chooseLocation': 'string';
|
||||
'novelScreen.convertToEpubModal.pathToFolder': 'string';
|
||||
'novelScreen.convertToEpubModal.useCustomCSS': 'string';
|
||||
'novelScreen.convertToEpubModal.useCustomJS': 'string';
|
||||
'novelScreen.convertToEpubModal.useCustomJSWarning': 'string';
|
||||
'novelScreen.convertToEpubModal.useReaderTheme': 'string';
|
||||
'novelScreen.exportEpubModal.applyReaderTheme': 'string';
|
||||
'novelScreen.exportEpubModal.customJSWarning': 'string';
|
||||
'novelScreen.exportEpubModal.downloadedChaptersOnly': 'string';
|
||||
'novelScreen.exportEpubModal.includeCustomCSS': 'string';
|
||||
'novelScreen.exportEpubModal.includeCustomJS': 'string';
|
||||
'novelScreen.exportEpubModal.selectFolder': 'string';
|
||||
'novelScreen.exportEpubModal.title': 'string';
|
||||
'novelScreen.epub.exportFailed': 'string';
|
||||
'novelScreen.epub.exportSuccess': 'string';
|
||||
'novelScreen.epub.noDownloadedChapters': 'string';
|
||||
'novelScreen.epub.noNovelSelected': 'string';
|
||||
'novelScreen.coverSaved': 'string';
|
||||
'novelScreen.coverNotSaved': 'string';
|
||||
'novelScreen.deleteChapterError': 'string';
|
||||
|
||||
Reference in New Issue
Block a user