fix: Export to EPUB Improvements (#670)

* Test config

* update react-native-epub-creator

* updated deps

* option for export to epub

* added prettier to dev deps since I couldn't push

* removed white background when pulling up bottomsheet

* added options to the export to epub feature

* support for customCSS -JS and inchapter images

* package update

* fixed weird typing

* fixed weird KeyboardAvoidingView

* remove test config

* logic fix

* removed styles2

* fixed package problems

* fixed package problems

* notification icons

* package reinstallation

* package reinstallation

* customFileModal fix

* fixed ChapterItem alignment

* added github suggestions
This commit is contained in:
Patrick Loser
2023-07-15 09:44:11 +02:00
committed by GitHub
parent 0d4e16509c
commit 414ca51660
20 changed files with 2232 additions and 15910 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

+1978 -15736
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -16,7 +16,7 @@
"prepare": "husky install"
},
"dependencies": {
"@cd-z/react-native-epub-creator": "^2.0.0",
"@cd-z/react-native-epub-creator": "^2.0.4",
"@gorhom/bottom-sheet": "^4.4.5",
"@react-native-async-storage/async-storage": "^1.18.1",
"@react-native-community/clipboard": "^1.5.1",
@@ -91,10 +91,11 @@
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"babel-plugin-module-resolver": "^4.1.0",
"eslint": "^7.32.0",
"eslint-plugin-react-native": "^3.11.0",
"eslint": "^8.0.0",
"eslint-plugin-react-native": "^4.0.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.7",
"prettier": "^2.8.8",
"typescript": "^4.6.3"
},
"resolutions": {
@@ -6,6 +6,8 @@ import {
BottomSheetModalProps,
} from '@gorhom/bottom-sheet';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '@hooks/useTheme';
import { overlay } from 'react-native-paper';
interface BottomSheetProps extends Omit<BottomSheetModalProps, 'ref'> {
bottomSheetRef: Ref<BottomSheetModal> | null;
@@ -13,6 +15,7 @@ interface BottomSheetProps extends Omit<BottomSheetModalProps, 'ref'> {
const BottomSheet: React.FC<BottomSheetProps> = props => {
const { bottom } = useSafeAreaInsets();
const theme = useTheme();
const renderBackdrop = useCallback(
(backdropProps: BottomSheetBackdropProps) => (
@@ -31,6 +34,7 @@ const BottomSheet: React.FC<BottomSheetProps> = props => {
backdropComponent={renderBackdrop}
handleComponent={null}
containerStyle={{ paddingBottom: bottom }}
backgroundStyle={{ backgroundColor: overlay(2, theme.surface) }}
{...props}
>
{props.children}
+1 -2
View File
@@ -25,7 +25,7 @@ const SwitchSetting: React.FC<SwitchProps> = ({
paddingHorizontal: 16,
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: description ? 16 : 12,
paddingVertical: 12,
},
row: {
flexDirection: 'row',
@@ -39,7 +39,6 @@ const SwitchSetting: React.FC<SwitchProps> = ({
},
text: {
fontSize: 16,
flex: 1,
textAlignVertical: 'center',
},
switch: { marginLeft: 8 },
+2 -2
View File
@@ -1,6 +1,6 @@
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
interface ReturnType {
export interface BooleanHookType {
value: boolean;
setValue: Dispatch<SetStateAction<boolean>>;
setTrue: () => void;
@@ -8,7 +8,7 @@ interface ReturnType {
toggle: () => void;
}
const useBoolean = (defaultValue?: boolean): ReturnType => {
const useBoolean = (defaultValue?: boolean): BooleanHookType => {
const [value, setValue] = useState(!!defaultValue);
const setTrue = useCallback(() => setValue(true), []);
+33 -15
View File
@@ -1,5 +1,6 @@
import React, { memo, ReactNode, useState } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { View, StyleSheet, Pressable } from 'react-native';
import { Text } from 'react-native-paper';
import color from 'color';
import { Row } from '../../../components/Common';
@@ -73,10 +74,10 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
onLongPress={() => onSelectLongPress?.(chapter)}
android_ripple={{ color: theme.rippleColor }}
>
{left && <View style={styles.flex}>{left}</View>}
<Row style={styles.row}>
{left}
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
<View>
<View style={styles.textContainer}>
{isUpdateCard && (
<Text
style={[
@@ -131,16 +132,18 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
</View>
</View>
</Row>
<DownloadButton
downloadQueue={downloadQueue}
chapter={chapter}
theme={theme}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
hideDeleteChapterMenu={hideDeleteChapterMenu}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenuVisible={deleteChapterMenuVisible}
/>
<View style={styles.flex}>
<DownloadButton
downloadQueue={downloadQueue}
chapter={chapter}
theme={theme}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
hideDeleteChapterMenu={hideDeleteChapterMenu}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenuVisible={deleteChapterMenuVisible}
/>
</View>
</Pressable>
);
};
@@ -155,13 +158,28 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
columnGab: 8,
},
text: {
fontSize: 12,
},
textRow: {
flexDirection: 'row',
marginTop: 5,
},
row: { flex: 1, overflow: 'hidden' },
flex: {
flexBasis: 'auto',
flexShrink: 9,
},
textContainer: {
flex: 1,
marginVertical: '10%',
height: '80%',
justifyContent: 'space-evenly',
},
row: {
flexBasis: 10,
flexGrow: 1,
overflow: 'hidden',
height: '100%',
},
});
@@ -11,6 +11,7 @@ import { ChapterItem, LibraryNovelInfo } from '@database/types';
import { showToast } from '@hooks/showToast';
import { getChapterFromDb } from '@database/queries/DownloadQueries';
import { useReaderSettings } from '@redux/hooks';
import { useSettings } from '@hooks/reduxHooks';
interface extendedNovelInfo extends LibraryNovelInfo {
chapters: ChapterItem[];
@@ -28,7 +29,15 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
}) => {
const chooseEpubLocationModal = useBoolean(false);
const readerSettings = useReaderSettings();
const epubStyle = `html {
const {
epubUseAppTheme = false,
epubUseCustomCSS = false,
epubUseCustomJS = false,
} = useSettings();
const epubStyle = `${
epubUseAppTheme
? `html {
scroll-behavior: smooth;
overflow-x: hidden;
padding-top: ${StatusBar.currentHeight};
@@ -43,6 +52,7 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
text-align: ${readerSettings.textAlign};
line-height: ${readerSettings.lineHeight};
font-family: "${readerSettings.fontFamily}";
background-color: "${readerSettings.theme}";
}
hr {
margin-top: 20px;
@@ -56,9 +66,25 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
width: auto;
height: auto;
max-width: 100%;
}`
: ''
}
${readerSettings.customCSS}`;
${
epubUseCustomCSS
? readerSettings.customCSS
.replace(RegExp(`\#sourceId-${novel.sourceId}\\s*\\{`, 'g'), 'body {')
.replace(RegExp(`\#sourceId-${novel.sourceId}[^\.\#A-Z]*`, 'gi'), '')
: ''
}`;
const epubJS = `let novelName = "${novel.novelName}";
let chapterName = "";
let sourceId =${novel.sourceId};
let chapterId ="";
let novelId =${novel.novelId};
let html = document.querySelector("chapter").innerHTML;
${readerSettings.customJS}
`;
const createEpub = async (uri: string) => {
var epub = new EpubBuilder(
@@ -70,7 +96,8 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
description: novel.novelSummary,
author: novel.author,
bookId: novel.novelId.toString(),
stylesheet: epubStyle,
stylesheet: epubStyle || undefined,
js: epubUseCustomJS ? epubJS : undefined,
},
uri,
);
@@ -85,21 +112,17 @@ const EpubIconButton: React.FC<EpubIconButtonProps> = ({
await epub.addChapter({
title: downloaded.chapterName?.trim() ?? 'Chapter ' + i,
fileName: 'Chapter' + i,
htmlBody: downloaded.chapterText,
htmlBody: `<chapter data-novel-id='${chapter.novelId}' data-chapter-id='${chapter.chapterId}'>${downloaded.chapterText}</chapter>`,
});
}
}
var epubFilePath = await epub.save();
showToast('Epub file saved at: ' + epubFilePath);
} catch (error) {
// remove the temp created folder
showToast('Cannot create because: ' + error);
console.error(error);
await epub.discardChanges();
}
};
return (
<>
<IconButton
@@ -3,25 +3,42 @@ import { StyleSheet, View } from 'react-native';
import { getString } from '@strings/translations';
import { Button } from '@components';
import { Modal, Portal, TextInput, Text } from 'react-native-paper';
import { Modal, TextInput, Text } from 'react-native-paper';
import { useTheme } from '@hooks/useTheme';
import { useSettings } from '@hooks/reduxHooks';
import { openDocumentTree } from 'react-native-saf-x';
import SwitchSetting from '@components/Switch/Switch';
import useBoolean from '@hooks/useBoolean';
import { useDispatch } from 'react-redux';
import { setAppSettings } from '@redux/settings/settings.actions';
interface ChooseEpubLocationModalProps {
hideModal: () => void;
modalVisible: boolean;
onSubmit: (uri: string) => void;
onSubmit?: (uri: string) => void;
dispatchConfig?: boolean;
}
const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
hideModal,
modalVisible,
onSubmit: onSubmitProp,
dispatchConfig,
}) => {
const { epubLocation = '' } = useSettings();
const theme = useTheme();
const {
epubLocation = '',
epubUseAppTheme = false,
epubUseCustomCSS = false,
epubUseCustomJS = false,
} = useSettings();
const saveConfig = dispatchConfig || epubLocation === '';
const dispatch = useDispatch();
const [uri, setUri] = useState(epubLocation);
const useAppTheme = useBoolean(epubUseAppTheme);
const useCustomCSS = useBoolean(epubUseCustomCSS);
const useCustomJS = useBoolean(epubUseCustomJS);
const [error, setError] = useState('');
const onDismiss = () => {
@@ -30,12 +47,14 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
setUri(epubLocation);
};
const onChangeText = (txt: string) => {
setUri(txt);
};
const onSubmit = () => {
onSubmitProp(uri);
if (saveConfig) {
dispatch(setAppSettings('epubLocation', uri));
dispatch(setAppSettings('epubUseAppTheme', useAppTheme.value));
dispatch(setAppSettings('epubUseCustomCSS', useCustomCSS.value));
dispatch(setAppSettings('epubUseCustomJS', useCustomJS.value));
}
onSubmitProp?.(uri);
hideModal();
};
@@ -51,48 +70,69 @@ const ChooseEpubLocationModal: React.FC<ChooseEpubLocationModalProps> = ({
};
return (
<Portal>
<Modal
visible={modalVisible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: theme.overlay3 },
]}
>
<View style={styles.modalHeaderCtn}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('novelScreen.convertToEpubModal.chooseLocation')}
</Text>
<View style={styles.row}>
<TextInput
style={styles.textInput}
value={uri}
placeholder={getString(
'novelScreen.convertToEpubModal.pathToFolder',
)}
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
mode="outlined"
theme={{ colors: { ...theme } }}
underlineColor={theme.outline}
dense
error={error ? true : false}
right={
<TextInput.Icon
icon="folder-edit-outline"
onPress={openFolderPicker}
/>
}
/>
</View>
<Modal
visible={modalVisible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: theme.overlay3 },
]}
>
<View style={styles.modalHeaderCtn}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('novelScreen.convertToEpubModal.chooseLocation')}
</Text>
<View style={styles.row}>
<TextInput
style={styles.textInput}
onChangeText={setUri}
value={uri}
placeholder={getString(
'novelScreen.convertToEpubModal.pathToFolder',
)}
onSubmitEditing={onSubmit}
mode="outlined"
theme={{ colors: { ...theme } }}
underlineColor={theme.outline}
dense
error={error ? true : false}
right={
<TextInput.Icon
icon="folder-edit-outline"
onPress={openFolderPicker}
/>
}
/>
</View>
<View style={styles.modalFooterCtn}>
<Button title={getString('common.submit')} onPress={onSubmit} />
<Button title={getString('common.cancel')} onPress={hideModal} />
</View>
</Modal>
</Portal>
</View>
<View style={styles.settings}>
<SwitchSetting
label={getString('novelScreen.convertToEpubModal.useReaderTheme')}
value={useAppTheme.value}
onPress={useAppTheme.toggle}
theme={theme}
/>
<SwitchSetting
label={getString('novelScreen.convertToEpubModal.useCustomCSS')}
value={useCustomCSS.value}
onPress={useCustomCSS.toggle}
theme={theme}
/>
<SwitchSetting
label={getString('novelScreen.convertToEpubModal.useCustomJS')}
description={getString(
'novelScreen.convertToEpubModal.useCustomJSWarning',
)}
value={useCustomJS.value}
onPress={useCustomJS.toggle}
theme={theme}
/>
</View>
<View style={styles.modalFooterCtn}>
<Button title={getString('common.submit')} onPress={onSubmit} />
<Button title={getString('common.cancel')} onPress={hideModal} />
</View>
</Modal>
);
};
@@ -103,6 +143,10 @@ const styles = StyleSheet.create({
margin: 30,
borderRadius: 32,
},
settings: {
marginTop: 12,
paddingLeft: 5,
},
icon: { marginVertical: 0, marginRight: 0 },
textInput: { flex: 1 },
modalHeaderCtn: {
@@ -114,38 +158,15 @@ const styles = StyleSheet.create({
flexDirection: 'row-reverse',
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 32,
paddingBottom: 20,
},
modalTitle: {
fontSize: 24,
marginBottom: 16,
},
errorText: {
paddingTop: 12,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
marginVertical: {
marginVertical: 8,
},
flashlist: {
marginTop: 8,
height: 300,
borderTopWidth: 1,
borderBottomWidth: 1,
},
listContentCtn: {
paddingVertical: 8,
},
dateCtn: {
fontSize: 12,
marginTop: 2,
},
listElementContainer: {
paddingHorizontal: 20,
paddingVertical: 12,
},
});
@@ -316,7 +316,7 @@ const WebViewReader: React.FC<WebViewReaderProps> = props => {
let sourceId =${chapterInfo.sourceId};
let chapterId =${chapterInfo.chapterId};
let novelId =${chapterInfo.novelId};
let html = document.getElementsByTagName("chapter")[0]. innerHTML;
let html = document.querySelector("chapter").innerHTML;
${readerSettings.customJS}
}
document.addEventListener("DOMContentLoaded", fn);
@@ -345,9 +345,7 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
<ChooseEpubLocationModal
hideModal={convertToEpubModal.setFalse}
modalVisible={convertToEpubModal.value}
onSubmit={(uri: string) =>
dispatch(setAppSettings('epubLocation', uri))
}
dispatchConfig
/>
</>
);
@@ -1,14 +1,14 @@
import React from 'react';
import React, { useState } from 'react';
import { KeyboardAvoidingView, StyleSheet, Text, View } from 'react-native';
import { Portal, Modal, overlay, TextInput } from 'react-native-paper';
import { Modal, overlay, TextInput } from 'react-native-paper';
import { Button } from '@components/index';
import { MD3ThemeType } from '@theme/types';
import { getString } from '@strings/translations';
import { useAppDispatch } from '@redux/hooks';
import { setReaderSettings } from '@redux/settings/settings.actions';
import * as DocumentPicker from 'expo-document-picker';
import { StorageAccessFramework } from 'expo-file-system';
import { showToast } from '@hooks/showToast';
import { setReaderSettings } from '@redux/settings/settings.actions';
interface CustomCSSModalProps {
visible: boolean;
@@ -35,6 +35,7 @@ const CustomFileModal: React.FC<CustomCSSModalProps> = ({
),
placeholder = '',
}) => {
const [text, setText] = useState(customFile);
const dispatch = useAppDispatch();
let mimeType: string;
if (type === 'CSS') {
@@ -51,66 +52,68 @@ const CustomFileModal: React.FC<CustomCSSModalProps> = ({
});
if (rawCSS.type === 'success') {
let css = await StorageAccessFramework.readAsStringAsync(rawCSS.uri);
setCustomFile(css);
setText(css);
}
} catch (error) {
} catch (error: any) {
showToast(error.message);
}
};
const dismiss = () => {
setCustomFile(text.trim());
onDismiss();
};
return (
<Portal>
<Modal
visible={visible}
onDismiss={onDismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: overlay(2, theme.surface) },
]}
>
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={300}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{title}
</Text>
<Text style={[{ color: theme.onSurfaceVariant }]}>
{type === 'CSS'
? getString('moreScreen.settingsScreen.readerSettings.cssHint')
: getString('moreScreen.settingsScreen.readerSettings.jsHint')}
</Text>
<TextInput
theme={{ colors: { ...theme } }}
underlineColor={theme.outline}
defaultValue={customFile}
onChangeText={setCustomFile}
mode="outlined"
placeholder={placeholder}
placeholderTextColor={theme.onSurfaceVariant}
multiline
style={[
{ color: theme.onSurface },
styles.fontSizeL,
styles.customCSSContainer,
]}
<Modal
visible={visible}
onDismiss={dismiss}
contentContainerStyle={[
styles.modalContainer,
{ backgroundColor: overlay(2, theme.surface) },
]}
>
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={180}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{title}
</Text>
<Text style={[{ color: theme.onSurfaceVariant }]}>
{type === 'CSS'
? getString('moreScreen.settingsScreen.readerSettings.cssHint')
: getString('moreScreen.settingsScreen.readerSettings.jsHint')}
</Text>
<TextInput
theme={{ colors: { ...theme } }}
underlineColor={theme.outline}
value={text}
onChangeText={setText}
mode="outlined"
placeholder={placeholder}
placeholderTextColor={theme.onSurfaceVariant}
multiline
style={[
{ color: theme.onSurface },
styles.fontSizeL,
styles.customCSSContainer,
]}
/>
<View style={styles.customCSSButtons}>
<Button
onPress={() => {
dispatch(setReaderSettings(`custom${type}`, text.trim()));
dismiss();
}}
style={styles.button}
title={getString('common.save')}
mode="contained"
/>
<View style={styles.customCSSButtons}>
<Button
onPress={() => {
dispatch(setReaderSettings(`custom${type}`, customFile.trim()));
onDismiss();
}}
style={styles.button}
title={getString('common.save')}
mode="contained"
/>
<Button
style={styles.button}
onPress={openCSS}
title={openFileButtonLabel}
/>
</View>
</KeyboardAvoidingView>
</Modal>
</Portal>
<Button
style={styles.button}
onPress={openCSS}
title={openFileButtonLabel}
/>
</View>
</KeyboardAvoidingView>
</Modal>
);
};
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { Button, List } from '@components/index';
import { useAppDispatch, readerSettingType } from '@redux/hooks';
import { useAppDispatch, useReaderSettings } from '@redux/hooks';
import { useTheme } from '@hooks/useTheme';
import { getString } from '@strings/translations';
import { setReaderSettings } from '@redux/settings/settings.actions';
@@ -12,7 +12,7 @@ import CustomFileModal from '../Modals/CustomFileModal';
import useBoolean from '@hooks/useBoolean';
interface CustomJSSettingsProps {
readerSettings: readerSettingType;
readerSettings: ReturnType<typeof useReaderSettings>;
}
const CustomJSSettings: React.FC<CustomJSSettingsProps> = ({
@@ -111,10 +111,12 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
title={chapterList[0].novelName}
titleStyle={{ fontSize: 14, color: theme.onSurface }}
left={() => (
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
/>
<View style={styles.listNovelCover}>
<NovelCover
navigateToNovel={navigateToNovel}
uri={chapterList[0].novelCover}
/>
</View>
)}
descriptionStyle={{ fontSize: 12 }}
theme={{ colors: theme }}
@@ -196,8 +198,11 @@ const styles = StyleSheet.create({
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
listNovelCover: {
justifyContent: 'center',
},
novelCover: {
marginRight: 8,
marginRight: 16,
},
imageContainer: {
flex: 1,
@@ -217,6 +222,6 @@ const styles = StyleSheet.create({
margin: 8,
},
chapterList: {
marginLeft: -64,
marginLeft: -40,
},
});
+6 -2
View File
@@ -131,8 +131,12 @@
}
},
"convertToEpubModal": {
"chooseLocation": "Choose EPUB save location",
"pathToFolder": "Path to EPUB save folder"
"chooseLocation": "Select EPUB storage directory",
"pathToFolder": "Path to EPUB save folder",
"useReaderTheme": "Use reader theme",
"useCustomCSS": "Use customCSS",
"useCustomJS": "Use customJS",
"useCustomJSWarning": "May not be supported by all EPUB readers"
}
},
"more": "More",
+4
View File
@@ -105,6 +105,10 @@ export interface StringMap {
'novelScreen.jumpToChapterModal.error.validChapterNumber': 'string';
'novelScreen.convertToEpubModal.chooseLocation': 'string';
'novelScreen.convertToEpubModal.pathToFolder': 'string';
'novelScreen.convertToEpubModal.useReaderTheme': 'string';
'novelScreen.convertToEpubModal.useCustomCSS': 'string';
'novelScreen.convertToEpubModal.useCustomJS': 'string';
'novelScreen.convertToEpubModal.useCustomJSWarning': 'string';
'more': 'string';
'moreScreen.settings': 'string';
'moreScreen.settingsScreen.generalSettings': 'string';