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;