refactor: Move to Pagination Component and Replace Drawer with BottomSheet
This commit is contained in:
+110
-145
@@ -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 (
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onOpen={openDrawer}
|
||||
onClose={closeDrawer}
|
||||
swipeEnabled={pages.length > 1}
|
||||
hideStatusBarOnOpen={true}
|
||||
swipeMinVelocity={1000}
|
||||
drawerStyle={styles.drawer}
|
||||
renderDrawerContent={() =>
|
||||
(novel?.totalPages ?? 0) > 1 || pages.length > 1 ? (
|
||||
<NovelDrawer
|
||||
theme={theme}
|
||||
pages={pages}
|
||||
pageIndex={pageIndex}
|
||||
openPage={openPage}
|
||||
closeDrawer={closeDrawer}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Portal.Host>
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Portal>
|
||||
{selected.length === 0 ? (
|
||||
<NovelAppbar
|
||||
novel={novel}
|
||||
deleteChapters={deleteChs}
|
||||
downloadChapters={downloadChs}
|
||||
showEditInfoModal={showEditInfoModal}
|
||||
setCustomNovelCover={setCustomNovelCover}
|
||||
downloadCustomChapterModal={openDlChapterModal}
|
||||
showJumpToChapterModal={showJumpToChapterModal}
|
||||
shareNovel={shareNovel}
|
||||
theme={theme}
|
||||
isLocal={novel?.isLocal ?? route.params?.isLocal}
|
||||
goBack={navigation.goBack}
|
||||
headerOpacity={headerOpacity}
|
||||
/>
|
||||
) : (
|
||||
<Animated.View
|
||||
entering={SlideInUp.duration(250)}
|
||||
exiting={SlideOutUp.duration(250)}
|
||||
style={styles.appbar}
|
||||
>
|
||||
<Appbar.Action
|
||||
icon="close"
|
||||
iconColor={theme.onBackground}
|
||||
onPress={() => setSelected([])}
|
||||
/>
|
||||
<Appbar.Content
|
||||
title={`${selected.length}`}
|
||||
titleStyle={{ color: theme.onSurface }}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="select-all"
|
||||
iconColor={theme.onBackground}
|
||||
onPress={() => {
|
||||
setSelected(chapters);
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Portal>
|
||||
<SafeAreaView excludeTop>
|
||||
<Suspense fallback={<NovelScreenLoading theme={theme} />}>
|
||||
<NovelScreenList
|
||||
headerOpacity={headerOpacity}
|
||||
listRef={chapterListRef}
|
||||
navigation={navigation}
|
||||
openDrawer={openDrawer}
|
||||
routeBaseNovel={route.params}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
getNextChapterBatch={
|
||||
batchInformation.batch < batchInformation.total && !fetching
|
||||
? getNextChapterBatch
|
||||
: noop
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</SafeAreaView>
|
||||
|
||||
<Portal>
|
||||
<Actionbar active={selected.length > 0} actions={actions} />
|
||||
<Snackbar
|
||||
visible={deleteDownloadsSnackbar.value}
|
||||
onDismiss={deleteDownloadsSnackbar.setFalse}
|
||||
action={{
|
||||
label: getString('common.delete'),
|
||||
onPress: () => {
|
||||
deleteChapters(chapters.filter(c => c.isDownloaded));
|
||||
},
|
||||
}}
|
||||
theme={{ colors: { primary: theme.primary } }}
|
||||
style={styles.snackbar}
|
||||
<Portal.Host>
|
||||
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
||||
<Portal>
|
||||
{selected.length === 0 ? (
|
||||
<NovelAppbar
|
||||
novel={novel}
|
||||
deleteChapters={deleteChs}
|
||||
downloadChapters={downloadChs}
|
||||
showEditInfoModal={showEditInfoModal}
|
||||
setCustomNovelCover={setCustomNovelCover}
|
||||
downloadCustomChapterModal={openDlChapterModal}
|
||||
showJumpToChapterModal={showJumpToChapterModal}
|
||||
shareNovel={shareNovel}
|
||||
theme={theme}
|
||||
isLocal={novel?.isLocal ?? route.params?.isLocal}
|
||||
goBack={navigation.goBack}
|
||||
headerOpacity={headerOpacity}
|
||||
/>
|
||||
) : (
|
||||
<Animated.View
|
||||
entering={SlideInUp.duration(250)}
|
||||
exiting={SlideOutUp.duration(250)}
|
||||
style={styles.appbar}
|
||||
>
|
||||
<Text style={{ color: theme.onSurface }}>
|
||||
{getString('novelScreen.deleteMessage')}
|
||||
</Text>
|
||||
</Snackbar>
|
||||
</Portal>
|
||||
<Portal>
|
||||
{novel && (
|
||||
<>
|
||||
<JumpToChapterModal
|
||||
modalVisible={jumpToChapterModal}
|
||||
hideModal={() => showJumpToChapterModal(false)}
|
||||
chapters={chapters}
|
||||
novel={novel}
|
||||
chapterListRef={chapterListRef}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<EditInfoModal
|
||||
modalVisible={editInfoModal}
|
||||
hideModal={() => showEditInfoModal(false)}
|
||||
novel={novel}
|
||||
setNovel={setNovel}
|
||||
theme={theme}
|
||||
/>
|
||||
<DownloadCustomChapterModal
|
||||
modalVisible={dlChapterModalVisible}
|
||||
hideModal={closeDlChapterModal}
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
theme={theme}
|
||||
downloadChapters={downloadChapters}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Portal>
|
||||
</View>
|
||||
</Portal.Host>
|
||||
</Drawer>
|
||||
<Appbar.Action
|
||||
icon="close"
|
||||
iconColor={theme.onBackground}
|
||||
onPress={() => setSelected([])}
|
||||
/>
|
||||
<Appbar.Content
|
||||
title={`${selected.length}`}
|
||||
titleStyle={{ color: theme.onSurface }}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="select-all"
|
||||
iconColor={theme.onBackground}
|
||||
onPress={() => {
|
||||
setSelected(chapters);
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Portal>
|
||||
<SafeAreaView excludeTop>
|
||||
<Suspense fallback={<NovelScreenLoading theme={theme} />}>
|
||||
<NovelScreenList
|
||||
headerOpacity={headerOpacity}
|
||||
listRef={chapterListRef}
|
||||
navigation={navigation}
|
||||
routeBaseNovel={route.params}
|
||||
selected={selected}
|
||||
setSelected={setSelected}
|
||||
getNextChapterBatch={
|
||||
batchInformation.batch < batchInformation.total && !fetching
|
||||
? getNextChapterBatch
|
||||
: noop
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</SafeAreaView>
|
||||
|
||||
<Portal>
|
||||
<Actionbar active={selected.length > 0} actions={actions} />
|
||||
<Snackbar
|
||||
visible={deleteDownloadsSnackbar.value}
|
||||
onDismiss={deleteDownloadsSnackbar.setFalse}
|
||||
action={{
|
||||
label: getString('common.delete'),
|
||||
onPress: () => {
|
||||
deleteChapters(chapters.filter(c => c.isDownloaded));
|
||||
},
|
||||
}}
|
||||
theme={{ colors: { primary: theme.primary } }}
|
||||
style={styles.snackbar}
|
||||
>
|
||||
<Text style={{ color: theme.onSurface }}>
|
||||
{getString('novelScreen.deleteMessage')}
|
||||
</Text>
|
||||
</Snackbar>
|
||||
</Portal>
|
||||
<Portal>
|
||||
{novel && (
|
||||
<>
|
||||
<JumpToChapterModal
|
||||
modalVisible={jumpToChapterModal}
|
||||
hideModal={() => showJumpToChapterModal(false)}
|
||||
chapters={chapters}
|
||||
novel={novel}
|
||||
chapterListRef={chapterListRef}
|
||||
navigation={navigation}
|
||||
/>
|
||||
<EditInfoModal
|
||||
modalVisible={editInfoModal}
|
||||
hideModal={() => showEditInfoModal(false)}
|
||||
novel={novel}
|
||||
setNovel={setNovel}
|
||||
theme={theme}
|
||||
/>
|
||||
<DownloadCustomChapterModal
|
||||
modalVisible={dlChapterModalVisible}
|
||||
hideModal={closeDlChapterModal}
|
||||
novel={novel}
|
||||
chapters={chapters}
|
||||
theme={theme}
|
||||
downloadChapters={downloadChapters}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Portal>
|
||||
</View>
|
||||
</Portal.Host>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -395,7 +361,6 @@ function createStyles(theme: ThemeColors) {
|
||||
width: '100%',
|
||||
},
|
||||
container: { flex: 1 },
|
||||
drawer: { backgroundColor: 'transparent' },
|
||||
rowBack: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
|
||||
@@ -52,8 +52,10 @@ interface NovelInfoHeaderProps {
|
||||
novel: NovelData | (Omit<NovelData, 'id'> & { id: 'NO_ID' });
|
||||
novelBottomSheetRef: React.RefObject<BottomSheetModalMethods | null>;
|
||||
onRefreshPage: (page: string) => void;
|
||||
openDrawer: () => void;
|
||||
page?: string;
|
||||
pageIndex: number;
|
||||
pages: string[];
|
||||
pageNavigationSheetRef: React.RefObject<BottomSheetModalMethods | null>;
|
||||
setCustomNovelCover: () => Promise<void>;
|
||||
saveNovelCover: () => Promise<void>;
|
||||
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) ? (
|
||||
<VerticalBarSkeleton />
|
||||
) : (
|
||||
<Pressable
|
||||
style={styles.bottomsheet}
|
||||
onPress={() =>
|
||||
page ? openDrawer() : novelBottomSheetRef.current?.present()
|
||||
}
|
||||
android_ripple={{
|
||||
color: color(theme.primary).alpha(0.12).string(),
|
||||
}}
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
{page || novel.totalPages ? (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[{ color: theme.onSurface }, styles.pageTitle]}
|
||||
>
|
||||
Page: {page ?? ''}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Text style={[{ color: theme.onSurface }, styles.chapters]}>
|
||||
{!fetching || totalChapters !== undefined
|
||||
? `${totalChapters} ${getString('novelScreen.chapters')}`
|
||||
: getString('common.loading')}
|
||||
</Text>
|
||||
</View>
|
||||
{page && Number(page) ? (
|
||||
<IconButton
|
||||
icon="reload"
|
||||
iconColor={theme.onSurface}
|
||||
size={24}
|
||||
onPress={() => onRefreshPage(page)}
|
||||
/>
|
||||
) : null}
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
iconColor={filter ? filterColor(theme.isDark) : theme.onSurface}
|
||||
size={24}
|
||||
<View style={styles.bottomsheetContainer}>
|
||||
<Pressable
|
||||
style={styles.bottomsheet}
|
||||
onPress={() => novelBottomSheetRef.current?.present()}
|
||||
/>
|
||||
</Pressable>
|
||||
android_ripple={{
|
||||
color: color(theme.primary).alpha(0.12).string(),
|
||||
}}
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
<Text style={[{ color: theme.onSurface }, styles.chapters]}>
|
||||
{!fetching || totalChapters !== undefined
|
||||
? `${totalChapters} ${getString('novelScreen.chapters')}`
|
||||
: getString('common.loading')}
|
||||
</Text>
|
||||
</View>
|
||||
<IconButton
|
||||
icon="filter-variant"
|
||||
iconColor={filter ? filterColor(theme.isDark) : theme.onSurface}
|
||||
size={24}
|
||||
onPress={() => novelBottomSheetRef.current?.present()}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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<number>;
|
||||
listRef: React.RefObject<LegendListRef | null>;
|
||||
navigation: any;
|
||||
openDrawer: () => void;
|
||||
selected: ChapterInfo[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<ChapterInfo[]>>;
|
||||
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<BottomSheetModalMethods>(null);
|
||||
const trackerSheetRef = useRef<BottomSheetModalMethods>(null);
|
||||
const pageNavigationSheetRef = useRef<BottomSheetModalMethods>(null);
|
||||
|
||||
const deleteDownloadsSnackbar = useBoolean();
|
||||
|
||||
@@ -306,6 +309,8 @@ const NovelScreenList = ({
|
||||
}
|
||||
};
|
||||
|
||||
const hasMultiplePages = pages.length > 1 || (novel?.totalPages ?? 0) > 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<LegendList
|
||||
@@ -321,7 +326,23 @@ const NovelScreenList = ({
|
||||
downloadQueue.length,
|
||||
]}
|
||||
// ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={!fetching ? undefined : ListEmptyComponent}
|
||||
ListFooterComponent={
|
||||
!fetching ? (
|
||||
hasMultiplePages ? (
|
||||
<View style={styles.pageNavigationWrapper}>
|
||||
<PagePaginationControl
|
||||
currentPage={pageIndex + 1}
|
||||
totalPages={pages.length}
|
||||
onPageChange={pageNum => openPage(pageNum - 1)}
|
||||
onOpenDrawer={() => pageNavigationSheetRef.current?.present()}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
) : 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}
|
||||
/>
|
||||
<TrackSheet bottomSheetRef={trackerSheetRef} novel={novel} />
|
||||
{(novel.totalPages ?? 0) > 1 || pages.length > 1 ? (
|
||||
<PageNavigationBottomSheet
|
||||
bottomSheetRef={pageNavigationSheetRef}
|
||||
theme={theme}
|
||||
pages={pages}
|
||||
pageIndex={pageIndex}
|
||||
openPage={openPage}
|
||||
/>
|
||||
) : null}
|
||||
{showScrollToTop && (
|
||||
<AnimatedFAB
|
||||
style={[
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Pressable, Text } from 'react-native';
|
||||
import { BottomSheetScrollView, BottomSheetView } from '@gorhom/bottom-sheet';
|
||||
import { LegendList, LegendListRenderItemProps } from '@legendapp/list';
|
||||
import color from 'color';
|
||||
|
||||
import BottomSheet from '@components/BottomSheet/BottomSheet';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { overlay } from 'react-native-paper';
|
||||
|
||||
interface PageNavigationBottomSheetProps {
|
||||
bottomSheetRef: React.RefObject<BottomSheetModalMethods | null>;
|
||||
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<string>) => {
|
||||
const isSelected = index === pageIndex;
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.pageItemContainer,
|
||||
{
|
||||
backgroundColor: isSelected
|
||||
? theme.isDark
|
||||
? color(theme.primary).alpha(0.2).string()
|
||||
: color(theme.primaryContainer).alpha(0.5).string()
|
||||
: 'transparent',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{
|
||||
color: isSelected
|
||||
? color(theme.primary).alpha(0.2).string()
|
||||
: theme.rippleColor,
|
||||
}}
|
||||
style={styles.pageItem}
|
||||
onPress={() => {
|
||||
openPage(index);
|
||||
bottomSheetRef.current?.close();
|
||||
}}
|
||||
>
|
||||
<View style={styles.pageItemContent}>
|
||||
<Text
|
||||
style={[
|
||||
styles.pageText,
|
||||
{
|
||||
color: isSelected ? theme.primary : theme.onSurfaceVariant,
|
||||
},
|
||||
]}
|
||||
>
|
||||
Page {item}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
bottomSheetRef={bottomSheetRef}
|
||||
snapPoints={[Math.min(400, pages.length * 56 + 100)]}
|
||||
backgroundStyle={styles.transparent}
|
||||
>
|
||||
<BottomSheetView
|
||||
style={[
|
||||
styles.contentContainer,
|
||||
{
|
||||
backgroundColor: overlay(2, theme.surface),
|
||||
marginLeft: left,
|
||||
marginRight: right,
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<BottomSheetScrollView>
|
||||
<LegendList
|
||||
data={pages}
|
||||
recycleItems
|
||||
extraData={pageIndex}
|
||||
renderItem={renderItem}
|
||||
estimatedItemSize={56}
|
||||
contentContainerStyle={styles.listContent}
|
||||
/>
|
||||
</BottomSheetScrollView>
|
||||
</BottomSheetView>
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
@@ -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<PagePaginationControlProps> = ({
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.button,
|
||||
styles.navButton,
|
||||
{
|
||||
borderColor: borderColor,
|
||||
backgroundColor: theme.surface,
|
||||
},
|
||||
!canGoPrevious && styles.disabledButton,
|
||||
]}
|
||||
onPress={handlePrevious}
|
||||
disabled={!canGoPrevious}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
iconColor={canGoPrevious ? theme.onSurface : theme.onSurfaceDisabled}
|
||||
size={20}
|
||||
style={styles.iconButton}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Row style={styles.pageNumbersRow}>
|
||||
{pageNumbers.map((page, index) => {
|
||||
if (page === 'ellipsis') {
|
||||
return (
|
||||
<Pressable
|
||||
key={`ellipsis-${index}`}
|
||||
style={[
|
||||
styles.button,
|
||||
styles.ellipsisButton,
|
||||
{
|
||||
borderColor: borderColor,
|
||||
backgroundColor: theme.surface,
|
||||
},
|
||||
]}
|
||||
onPress={onOpenDrawer}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Text style={[styles.ellipsisText, { color: theme.onSurface }]}>
|
||||
...
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = page === currentPage;
|
||||
return (
|
||||
<Pressable
|
||||
key={page}
|
||||
style={[
|
||||
styles.button,
|
||||
{
|
||||
borderColor: isActive ? 'transparent' : borderColor,
|
||||
backgroundColor: isActive ? theme.primary : theme.surface,
|
||||
},
|
||||
]}
|
||||
onPress={() => handlePagePress(page)}
|
||||
android_ripple={{
|
||||
color: isActive
|
||||
? color(theme.onPrimary).alpha(0.2).string()
|
||||
: theme.rippleColor,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.pageText,
|
||||
{
|
||||
color: isActive ? theme.onPrimary : theme.onSurface,
|
||||
fontWeight: isActive ? '600' : '400',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{page}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
|
||||
<Pressable
|
||||
style={[
|
||||
styles.button,
|
||||
styles.navButton,
|
||||
{
|
||||
borderColor: borderColor,
|
||||
backgroundColor: theme.surface,
|
||||
},
|
||||
!canGoNext && styles.disabledButton,
|
||||
]}
|
||||
onPress={handleNext}
|
||||
disabled={!canGoNext}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
iconColor={canGoNext ? theme.onSurface : theme.onSurfaceDisabled}
|
||||
size={20}
|
||||
style={styles.iconButton}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user