feat: Fetch All Chapters for EPUB Export and Add Range Selection
Closes #1605 Closes #1606
This commit is contained in:
@@ -311,6 +311,26 @@ export const getDownloadedChapters = () =>
|
||||
WHERE Chapter.isDownloaded = 1
|
||||
`);
|
||||
|
||||
export const getNovelDownloadedChapters = (
|
||||
novelId: number,
|
||||
startPosition?: number,
|
||||
endPosition?: number,
|
||||
) => {
|
||||
if (startPosition !== undefined && endPosition !== undefined) {
|
||||
return db.getAllAsync<ChapterInfo>(
|
||||
'SELECT * FROM Chapter WHERE novelId = ? AND isDownloaded = 1 AND position >= ? AND position <= ? ORDER BY position ASC',
|
||||
novelId,
|
||||
startPosition - 1,
|
||||
endPosition - 1,
|
||||
);
|
||||
}
|
||||
|
||||
return db.getAllAsync<ChapterInfo>(
|
||||
'SELECT * FROM Chapter WHERE novelId = ? AND isDownloaded = 1 ORDER BY position ASC',
|
||||
novelId,
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdatedOverviewFromDb = () =>
|
||||
db.getAllAsync<UpdateOverview>(`SELECT
|
||||
Novel.id AS novelId,
|
||||
|
||||
@@ -250,7 +250,6 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
|
||||
{selected.length === 0 ? (
|
||||
<NovelAppbar
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
deleteChapters={deleteChs}
|
||||
downloadChapters={downloadChs}
|
||||
showEditInfoModal={showEditInfoModal}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { showToast } from '@utils/showToast';
|
||||
|
||||
interface ExportEpubModalProps {
|
||||
isVisible: boolean;
|
||||
onSubmit?: (uri: string) => void;
|
||||
onSubmit?: (uri: string, startChapter?: number, endChapter?: number) => void;
|
||||
hideModal: () => void;
|
||||
}
|
||||
|
||||
@@ -34,13 +34,39 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
|
||||
const useAppTheme = useBoolean(epubUseAppTheme);
|
||||
const useCustomCSS = useBoolean(epubUseCustomCSS);
|
||||
const useCustomJS = useBoolean(epubUseCustomJS);
|
||||
const exportAll = useBoolean(true);
|
||||
const [startChapter, setStartChapter] = useState('');
|
||||
const [endChapter, setEndChapter] = useState('');
|
||||
|
||||
const onDismiss = () => {
|
||||
hideModal();
|
||||
setUri(epubLocation);
|
||||
exportAll.setTrue();
|
||||
setStartChapter('');
|
||||
setEndChapter('');
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!exportAll.value) {
|
||||
const start = parseInt(startChapter, 10);
|
||||
const end = parseInt(endChapter, 10);
|
||||
|
||||
if (isNaN(start) || isNaN(end)) {
|
||||
showToast(getString('novelScreen.exportEpubModal.invalidRange'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (start < 1 || end < 1) {
|
||||
showToast(getString('novelScreen.exportEpubModal.invalidRange'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
showToast(getString('novelScreen.exportEpubModal.startGreaterThanEnd'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setChapterReaderSettings({
|
||||
epubLocation: uri,
|
||||
epubUseAppTheme: useAppTheme.value,
|
||||
@@ -48,7 +74,10 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
|
||||
epubUseCustomJS: useCustomJS.value,
|
||||
});
|
||||
|
||||
onSubmitProp?.(uri);
|
||||
const start = exportAll.value ? undefined : parseInt(startChapter, 10);
|
||||
const end = exportAll.value ? undefined : parseInt(endChapter, 10);
|
||||
|
||||
onSubmitProp?.(uri, start, end);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
@@ -87,6 +116,38 @@ const ExportEpubModal: React.FC<ExportEpubModalProps> = ({
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.settings}>
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.exportEpubModal.exportAll')}
|
||||
value={exportAll.value}
|
||||
onPress={exportAll.toggle}
|
||||
theme={theme}
|
||||
/>
|
||||
{!exportAll.value && (
|
||||
<View style={styles.rangeInputs}>
|
||||
<TextInput
|
||||
label={getString('novelScreen.exportEpubModal.startChapter')}
|
||||
value={startChapter}
|
||||
onChangeText={setStartChapter}
|
||||
keyboardType="numeric"
|
||||
mode="outlined"
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.outline}
|
||||
dense
|
||||
style={styles.rangeInput}
|
||||
/>
|
||||
<TextInput
|
||||
label={getString('novelScreen.exportEpubModal.endChapter')}
|
||||
value={endChapter}
|
||||
onChangeText={setEndChapter}
|
||||
keyboardType="numeric"
|
||||
mode="outlined"
|
||||
theme={{ colors: { ...theme } }}
|
||||
underlineColor={theme.outline}
|
||||
dense
|
||||
style={styles.rangeInput}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<SwitchItem
|
||||
label={getString('novelScreen.exportEpubModal.applyReaderTheme')}
|
||||
value={useAppTheme.value}
|
||||
@@ -140,4 +201,12 @@ const styles = StyleSheet.create({
|
||||
settings: {
|
||||
marginTop: 12,
|
||||
},
|
||||
rangeInputs: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
rangeInput: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,19 +5,19 @@ import { StatusBar, StyleProp, ViewStyle } from 'react-native';
|
||||
import EpubBuilder from '@cd-z/react-native-epub-creator';
|
||||
import NativeFile from '@specs/NativeFile';
|
||||
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
import { 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 { getNovelDownloadedChapters } from '@database/queries/ChapterQueries';
|
||||
|
||||
import ExportEpubModal from './ExportEpubModal';
|
||||
import { MaterialDesignIconName } from '@type/icon';
|
||||
|
||||
interface ExportNovelAsEpubButtonProps {
|
||||
novel?: NovelInfo;
|
||||
chapters: ChapterInfo[];
|
||||
iconComponent: (props: {
|
||||
icon: MaterialDesignIconName;
|
||||
onPress: () => void;
|
||||
@@ -28,7 +28,6 @@ interface ExportNovelAsEpubButtonProps {
|
||||
|
||||
const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
|
||||
novel,
|
||||
chapters,
|
||||
iconComponent: IconComponent,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@@ -111,7 +110,11 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
|
||||
`;
|
||||
}, [novel, readerSettings]);
|
||||
|
||||
const exportNovelAsEpub = async (destinationUri: string) => {
|
||||
const exportNovelAsEpub = async (
|
||||
destinationUri: string,
|
||||
startChapter?: number,
|
||||
endChapter?: number,
|
||||
) => {
|
||||
if (!novel) {
|
||||
showToast(getString('novelScreen.epub.noNovelSelected'));
|
||||
return;
|
||||
@@ -120,6 +123,17 @@ const ExportNovelAsEpubButton: React.FC<ExportNovelAsEpubButtonProps> = ({
|
||||
let epub: EpubBuilder | undefined;
|
||||
|
||||
try {
|
||||
const chapters = await getNovelDownloadedChapters(
|
||||
novel.id,
|
||||
startChapter,
|
||||
endChapter,
|
||||
);
|
||||
|
||||
if (chapters.length === 0) {
|
||||
showToast(getString('novelScreen.epub.noDownloadedChapters'));
|
||||
return;
|
||||
}
|
||||
|
||||
epub = new EpubBuilder(
|
||||
{
|
||||
title: novel.name,
|
||||
|
||||
@@ -57,7 +57,6 @@ const Menu = React.memo(
|
||||
|
||||
const NovelAppbar = ({
|
||||
novel,
|
||||
chapters,
|
||||
theme,
|
||||
isLocal,
|
||||
downloadChapters,
|
||||
@@ -71,7 +70,6 @@ const NovelAppbar = ({
|
||||
headerOpacity,
|
||||
}: {
|
||||
novel: NovelInfo | undefined;
|
||||
chapters: ChapterInfo[];
|
||||
theme: ThemeColors;
|
||||
isLocal: boolean | undefined;
|
||||
downloadChapters: (amount: number | 'all' | 'unread') => void;
|
||||
@@ -165,11 +163,7 @@ const NovelAppbar = ({
|
||||
<Appbar.BackAction onPress={goBack} />
|
||||
|
||||
<View style={styles.row}>
|
||||
<ExportNovelAsEpubButton
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
iconComponent={AppbarAction}
|
||||
/>
|
||||
<ExportNovelAsEpubButton novel={novel} iconComponent={AppbarAction} />
|
||||
<AppbarAction icon="share-variant" onPress={shareNovel} />
|
||||
<AppbarAction
|
||||
icon="text-box-search-outline"
|
||||
|
||||
@@ -365,11 +365,16 @@
|
||||
"continueReading": "Continue reading",
|
||||
"exportEpubModal": {
|
||||
"applyReaderTheme": "Apply reader theme to EPUB",
|
||||
"customJSWarning": "Custom JavaScript may not be supported by all EPUB readers",
|
||||
"customJSWarning": "Custom JS may not be supported by all EPUB readers",
|
||||
"downloadedChaptersOnly": "Only downloaded chapters will be included in the EPUB file",
|
||||
"endChapter": "End Chapter",
|
||||
"exportAll": "Export All Chapters",
|
||||
"includeCustomCSS": "Include Custom CSS",
|
||||
"includeCustomJS": "Include Custom JS",
|
||||
"invalidRange": "Please enter valid chapter numbers",
|
||||
"selectFolder": "Select destination folder for EPUB file",
|
||||
"startChapter": "Start Chapter",
|
||||
"startGreaterThanEnd": "Start chapter must be less than or equal to end chapter",
|
||||
"title": "Export Novel as EPUB"
|
||||
},
|
||||
"epub": {
|
||||
|
||||
@@ -303,9 +303,14 @@ export interface StringMap {
|
||||
'novelScreen.exportEpubModal.applyReaderTheme': 'string';
|
||||
'novelScreen.exportEpubModal.customJSWarning': 'string';
|
||||
'novelScreen.exportEpubModal.downloadedChaptersOnly': 'string';
|
||||
'novelScreen.exportEpubModal.endChapter': 'string';
|
||||
'novelScreen.exportEpubModal.exportAll': 'string';
|
||||
'novelScreen.exportEpubModal.includeCustomCSS': 'string';
|
||||
'novelScreen.exportEpubModal.includeCustomJS': 'string';
|
||||
'novelScreen.exportEpubModal.invalidRange': 'string';
|
||||
'novelScreen.exportEpubModal.selectFolder': 'string';
|
||||
'novelScreen.exportEpubModal.startChapter': 'string';
|
||||
'novelScreen.exportEpubModal.startGreaterThanEnd': 'string';
|
||||
'novelScreen.exportEpubModal.title': 'string';
|
||||
'novelScreen.epub.exportFailed': 'string';
|
||||
'novelScreen.epub.exportSuccess': 'string';
|
||||
|
||||
Reference in New Issue
Block a user