feat: Fetch All Chapters for EPUB Export and Add Range Selection

Closes #1605
Closes #1606
This commit is contained in:
Rajarshee Chatterjee
2025-10-22 18:45:08 +05:30
parent 90160f7f40
commit 259dd6acc7
7 changed files with 121 additions and 15 deletions
+20
View File
@@ -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,
-1
View File
@@ -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,
+1 -7
View File
@@ -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"
+6 -1
View File
@@ -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": {
+5
View File
@@ -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';