diff --git a/src/screens/novel/NovelScreen.tsx b/src/screens/novel/NovelScreen.tsx
index 14ffdab8..aacba960 100644
--- a/src/screens/novel/NovelScreen.tsx
+++ b/src/screens/novel/NovelScreen.tsx
@@ -1,6 +1,5 @@
import React, { Suspense, useCallback, useMemo, useRef, useState } from 'react';
import { StyleSheet, View, StatusBar, Text, Share } from 'react-native';
-import { Drawer } from 'react-native-drawer-layout';
import Animated, {
SlideInUp,
SlideOutUp,
@@ -19,7 +18,6 @@ import NovelScreenLoading from './components/LoadingAnimation/NovelScreenLoading
import { NovelScreenProps } from '@navigators/types';
import { ChapterInfo } from '@database/types';
import { getString } from '@strings/translations';
-import NovelDrawer from './components/NovelDrawer';
import { isNumber, noop } from 'lodash-es';
import NovelAppbar from './components/NovelAppbar';
import { resolveUrl } from '@services/plugin/fetch';
@@ -37,14 +35,11 @@ import { LegendListRef } from '@legendapp/list';
const Novel = ({ route, navigation }: NovelScreenProps) => {
const {
- pageIndex,
- pages,
novel,
chapters,
fetching,
batchInformation,
getNextChapterBatch,
- openPage,
setNovel,
bookmarkChapters,
markChaptersRead,
@@ -66,11 +61,6 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
const deleteDownloadsSnackbar = useBoolean();
const headerOpacity = useSharedValue(0);
- const {
- value: drawerOpen,
- setTrue: openDrawer,
- setFalse: closeDrawer,
- } = useBoolean();
// TODO: fix this
// useEffect(() => {
@@ -242,141 +232,117 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
const styles = useMemo(() => createStyles(theme), [theme]);
return (
- 1}
- hideStatusBarOnOpen={true}
- swipeMinVelocity={1000}
- drawerStyle={styles.drawer}
- renderDrawerContent={() =>
- (novel?.totalPages ?? 0) > 1 || pages.length > 1 ? (
-
- ) : (
- <>>
- )
- }
- >
-
-
-
- {selected.length === 0 ? (
-
- ) : (
-
- setSelected([])}
- />
-
- {
- setSelected(chapters);
- }}
- />
-
- )}
-
-
- }>
-
-
-
-
-
- 0} actions={actions} />
- {
- deleteChapters(chapters.filter(c => c.isDownloaded));
- },
- }}
- theme={{ colors: { primary: theme.primary } }}
- style={styles.snackbar}
+
+
+
+ {selected.length === 0 ? (
+
+ ) : (
+
-
- {getString('novelScreen.deleteMessage')}
-
-
-
-
- {novel && (
- <>
- showJumpToChapterModal(false)}
- chapters={chapters}
- novel={novel}
- chapterListRef={chapterListRef}
- navigation={navigation}
- />
- showEditInfoModal(false)}
- novel={novel}
- setNovel={setNovel}
- theme={theme}
- />
-
- >
- )}
-
-
-
-
+ setSelected([])}
+ />
+
+ {
+ setSelected(chapters);
+ }}
+ />
+
+ )}
+
+
+ }>
+
+
+
+
+
+ 0} actions={actions} />
+ {
+ deleteChapters(chapters.filter(c => c.isDownloaded));
+ },
+ }}
+ theme={{ colors: { primary: theme.primary } }}
+ style={styles.snackbar}
+ >
+
+ {getString('novelScreen.deleteMessage')}
+
+
+
+
+ {novel && (
+ <>
+ showJumpToChapterModal(false)}
+ chapters={chapters}
+ novel={novel}
+ chapterListRef={chapterListRef}
+ navigation={navigation}
+ />
+ showEditInfoModal(false)}
+ novel={novel}
+ setNovel={setNovel}
+ theme={theme}
+ />
+
+ >
+ )}
+
+
+
);
};
@@ -395,7 +361,6 @@ function createStyles(theme: ThemeColors) {
width: '100%',
},
container: { flex: 1 },
- drawer: { backgroundColor: 'transparent' },
rowBack: {
alignItems: 'center',
flex: 1,
diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx
index 8ab46dd2..27f3f5ea 100644
--- a/src/screens/novel/components/Info/NovelInfoHeader.tsx
+++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx
@@ -52,8 +52,10 @@ interface NovelInfoHeaderProps {
novel: NovelData | (Omit & { id: 'NO_ID' });
novelBottomSheetRef: React.RefObject;
onRefreshPage: (page: string) => void;
- openDrawer: () => void;
page?: string;
+ pageIndex: number;
+ pages: string[];
+ pageNavigationSheetRef: React.RefObject;
setCustomNovelCover: () => Promise;
saveNovelCover: () => Promise;
theme: ThemeColors;
@@ -82,9 +84,6 @@ const NovelInfoHeader = ({
navigation,
novel,
novelBottomSheetRef,
- onRefreshPage,
- openDrawer,
- page,
setCustomNovelCover,
saveNovelCover,
theme,
@@ -227,46 +226,29 @@ const NovelInfoHeader = ({
{isLoading && (!novel.genres || !novel.summary) ? (
) : (
-
- page ? openDrawer() : novelBottomSheetRef.current?.present()
- }
- android_ripple={{
- color: color(theme.primary).alpha(0.12).string(),
- }}
- >
-
- {page || novel.totalPages ? (
-
- Page: {page ?? ''}
-
- ) : null}
-
-
- {!fetching || totalChapters !== undefined
- ? `${totalChapters} ${getString('novelScreen.chapters')}`
- : getString('common.loading')}
-
-
- {page && Number(page) ? (
- onRefreshPage(page)}
- />
- ) : null}
-
+ novelBottomSheetRef.current?.present()}
- />
-
+ android_ripple={{
+ color: color(theme.primary).alpha(0.12).string(),
+ }}
+ >
+
+
+ {!fetching || totalChapters !== undefined
+ ? `${totalChapters} ${getString('novelScreen.chapters')}`
+ : getString('common.loading')}
+
+
+ novelBottomSheetRef.current?.present()}
+ />
+
+
)}
>
>
@@ -281,16 +263,16 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
paddingRight: 12,
- paddingVertical: 4,
+ paddingVertical: 8,
+ },
+ bottomsheetContainer: {
+ gap: 12,
},
chapters: {
fontSize: 14,
paddingHorizontal: 16,
},
flex: { flex: 1 },
- infoItem: {
- marginVertical: 2,
- },
marginRight: { marginRight: 4 },
novelDetails: {
flex: 1,
@@ -299,8 +281,4 @@ const styles = StyleSheet.create({
paddingBottom: 16,
paddingLeft: 12,
},
- pageTitle: {
- fontSize: 16,
- paddingHorizontal: 16,
- },
});
diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx
index 985e0168..e63b3b95 100644
--- a/src/screens/novel/components/NovelScreenList.tsx
+++ b/src/screens/novel/components/NovelScreenList.tsx
@@ -18,11 +18,13 @@ import {
NativeScrollEvent,
RefreshControl,
StyleSheet,
+ View,
} from 'react-native';
import { SharedValue } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import TrackSheet from './Tracker/TrackSheet';
import NovelBottomSheet from './NovelBottomSheet';
+import PageNavigationBottomSheet from './PageNavigationBottomSheet';
import * as Haptics from 'expo-haptics';
import { AnimatedFAB } from 'react-native-paper';
import { ChapterListSkeleton } from '@components/Skeleton/Skeleton';
@@ -32,12 +34,12 @@ import { LegendList, LegendListRef } from '@legendapp/list';
import FileManager from '@specs/NativeFile';
import { downloadFile } from '@plugins/helpers/fetch';
import { StorageAccessFramework } from 'expo-file-system/legacy';
+import PagePaginationControl from './PagePaginationControl';
type NovelScreenListProps = {
headerOpacity: SharedValue;
listRef: React.RefObject;
navigation: any;
- openDrawer: () => void;
selected: ChapterInfo[];
setSelected: React.Dispatch>;
getNextChapterBatch: () => void;
@@ -55,7 +57,6 @@ const NovelScreenList = ({
headerOpacity,
listRef,
navigation,
- openDrawer,
routeBaseNovel,
selected,
setSelected,
@@ -77,6 +78,7 @@ const NovelScreenList = ({
novel: fetchedNovel,
batchInformation,
pageIndex,
+ openPage,
} = useNovelContext();
const { pluginId } = routeBaseNovel;
@@ -113,6 +115,7 @@ const NovelScreenList = ({
const novelBottomSheetRef = useRef(null);
const trackerSheetRef = useRef(null);
+ const pageNavigationSheetRef = useRef(null);
const deleteDownloadsSnackbar = useBoolean();
@@ -306,6 +309,8 @@ const NovelScreenList = ({
}
};
+ const hasMultiplePages = pages.length > 1 || (novel?.totalPages ?? 0) > 1;
+
return (
<>
+ openPage(pageNum - 1)}
+ onOpenDrawer={() => pageNavigationSheetRef.current?.present()}
+ theme={theme}
+ />
+
+ ) : undefined
+ ) : (
+ ListEmptyComponent
+ )
+ }
renderItem={({ item, index }) => {
if (novel.id === 'NO_ID') {
return null;
@@ -369,8 +390,10 @@ const NovelScreenList = ({
novel={novel}
novelBottomSheetRef={novelBottomSheetRef}
onRefreshPage={onRefreshPage}
- openDrawer={openDrawer}
page={pages.length > 1 ? pages[pageIndex] : undefined}
+ pageIndex={pageIndex}
+ pages={pages}
+ pageNavigationSheetRef={pageNavigationSheetRef}
setCustomNovelCover={setCustomNovelCover}
saveNovelCover={saveNovelCover}
theme={theme}
@@ -391,6 +414,15 @@ const NovelScreenList = ({
showChapterTitles={showChapterTitles}
/>
+ {(novel.totalPages ?? 0) > 1 || pages.length > 1 ? (
+
+ ) : null}
{showScrollToTop && (
;
+ theme: ThemeColors;
+ pages: string[];
+ pageIndex: number;
+ openPage: (index: number) => void;
+}
+
+export default function PageNavigationBottomSheet({
+ bottomSheetRef,
+ theme,
+ pages,
+ pageIndex,
+ openPage,
+}: PageNavigationBottomSheetProps) {
+ const insets = useSafeAreaInsets();
+ const { left, right } = insets;
+
+ const renderItem = ({ item, index }: LegendListRenderItemProps) => {
+ const isSelected = index === pageIndex;
+ return (
+
+ {
+ openPage(index);
+ bottomSheetRef.current?.close();
+ }}
+ >
+
+
+ Page {item}
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ contentContainer: {
+ borderTopLeftRadius: 8,
+ borderTopRightRadius: 8,
+ flex: 1,
+ maxHeight: 400,
+ },
+ listContent: {
+ paddingBottom: 8,
+ paddingTop: 4,
+ },
+ pageItem: {
+ flex: 1,
+ justifyContent: 'center',
+ minHeight: 56,
+ paddingHorizontal: 24,
+ paddingVertical: 16,
+ },
+ pageItemContainer: {
+ borderRadius: 0,
+ marginHorizontal: 0,
+ overflow: 'hidden',
+ },
+ pageItemContent: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ pageText: {},
+ selectedIndicator: {
+ borderRadius: 2,
+ height: 20,
+ width: 3,
+ },
+ transparent: {
+ backgroundColor: 'transparent',
+ },
+});
diff --git a/src/screens/novel/components/PagePaginationControl.tsx b/src/screens/novel/components/PagePaginationControl.tsx
new file mode 100644
index 00000000..f08bd603
--- /dev/null
+++ b/src/screens/novel/components/PagePaginationControl.tsx
@@ -0,0 +1,232 @@
+import React, { useMemo } from 'react';
+import { View, Text, StyleSheet, Pressable } from 'react-native';
+import { IconButton } from 'react-native-paper';
+import color from 'color';
+import { ThemeColors } from '@theme/types';
+import { Row } from '@components/Common';
+import { borderColor } from '@theme/colors';
+
+interface PagePaginationControlProps {
+ currentPage: number;
+ totalPages: number;
+ onPageChange: (page: number) => void;
+ onOpenDrawer: () => void;
+ theme: ThemeColors;
+}
+
+const PagePaginationControl: React.FC = ({
+ currentPage,
+ totalPages,
+ onPageChange,
+ onOpenDrawer,
+ theme,
+}) => {
+ const pageNumbers = useMemo(() => {
+ const pages: (number | 'ellipsis')[] = [];
+
+ if (totalPages <= 3) {
+ for (let i = 1; i <= totalPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ if (currentPage !== 1) {
+ pages.push(1);
+ }
+
+ const leftPage = currentPage - 1;
+ if (leftPage > 1) {
+ if (leftPage > 2) {
+ pages.push('ellipsis');
+ }
+ pages.push(leftPage);
+ }
+
+ pages.push(currentPage);
+
+ if (currentPage < totalPages - 1) {
+ pages.push('ellipsis');
+ }
+
+ if (totalPages !== currentPage) {
+ pages.push(totalPages);
+ }
+ }
+
+ return pages;
+ }, [currentPage, totalPages]);
+
+ const canGoPrevious = currentPage > 1;
+ const canGoNext = currentPage < totalPages;
+
+ const handlePrevious = () => {
+ if (canGoPrevious) {
+ onPageChange(currentPage - 1);
+ }
+ };
+
+ const handleNext = () => {
+ if (canGoNext) {
+ onPageChange(currentPage + 1);
+ }
+ };
+
+ const handlePagePress = (page: number) => {
+ if (page !== currentPage) {
+ onPageChange(page);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {pageNumbers.map((page, index) => {
+ if (page === 'ellipsis') {
+ return (
+
+
+ ...
+
+
+ );
+ }
+
+ const isActive = page === currentPage;
+ return (
+ handlePagePress(page)}
+ android_ripple={{
+ color: isActive
+ ? color(theme.onPrimary).alpha(0.2).string()
+ : theme.rippleColor,
+ }}
+ >
+
+ {page}
+
+
+ );
+ })}
+
+
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ alignItems: 'center',
+ borderRadius: 8,
+ borderWidth: 1,
+ height: 40,
+ justifyContent: 'center',
+ minWidth: 40,
+ paddingHorizontal: 12,
+ },
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 8,
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ },
+ disabledButton: {
+ opacity: 0.5,
+ },
+ ellipsisButton: {
+ borderStyle: 'dashed',
+ },
+ ellipsisText: {
+ fontSize: 16,
+ fontWeight: '500',
+ },
+ iconButton: {
+ margin: 0,
+ },
+ navButton: {
+ paddingHorizontal: 0,
+ },
+ pageNumbersRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 8,
+ },
+ pageText: {
+ fontSize: 15,
+ letterSpacing: 0.15,
+ },
+});
+
+export default PagePaginationControl;