Generated
+15
@@ -47,6 +47,7 @@
|
||||
"react-native": "0.79.2",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-draggable-flatlist": "^4.0.2",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-error-boundary": "^2.0.0",
|
||||
@@ -13381,6 +13382,20 @@
|
||||
"@babel/runtime": "^7.20.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-draggable-flatlist": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.3.tgz",
|
||||
"integrity": "sha512-2F4x5BFieWdGq9SetD2nSAR7s7oQCSgNllYgERRXXtNfSOuAGAVbDb/3H3lP0y5f7rEyNwabKorZAD/SyyNbDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.17.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.64.0",
|
||||
"react-native-gesture-handler": ">=2.0.0",
|
||||
"react-native-reanimated": ">=2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-drawer-layout": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.1.8.tgz",
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"react-native": "0.79.2",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-draggable-flatlist": "^4.0.2",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-error-boundary": "^2.0.0",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import DraggableFlatList, {
|
||||
RenderItemParams,
|
||||
} from 'react-native-draggable-flatlist';
|
||||
|
||||
import { Appbar, EmptyView, SafeAreaView } from '@components/index';
|
||||
import AddCategoryModal from './components/AddCategoryModal';
|
||||
@@ -12,10 +15,10 @@ import { useTheme } from '@hooks/persisted';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
import CategoryCard from './components/CategoryCard';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import CategorySkeletonLoading from './components/CategorySkeletonLoading';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useLibraryContext } from '@components/Context/LibraryContext';
|
||||
import { ExtendedCategory } from '@screens/library/hooks/useLibrary';
|
||||
|
||||
const CategoriesScreen = () => {
|
||||
const { categories, setCategories, refreshCategories, isLoading } =
|
||||
@@ -35,35 +38,41 @@ const CategoriesScreen = () => {
|
||||
refreshCategories();
|
||||
}, [refreshCategories]);
|
||||
|
||||
const updateCategorySort = (currentIndex: number, newIndex: number) => {
|
||||
// Do not set local as default one
|
||||
if (
|
||||
(newIndex === 0 &&
|
||||
currentIndex === 1 &&
|
||||
categories?.[currentIndex].id === 2) ||
|
||||
(newIndex === 1 && currentIndex === 0 && categories?.[newIndex].id === 2)
|
||||
) {
|
||||
const userCategories = React.useMemo(() => {
|
||||
if (!categories || categories.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return categories.filter(cat => cat.id !== 1);
|
||||
}, [categories]);
|
||||
|
||||
const onDragEnd = ({ data }: { data: ExtendedCategory[] }) => {
|
||||
if (!categories || categories.length === 0) {
|
||||
return;
|
||||
}
|
||||
const updatedOrderCategories = orderBy(
|
||||
categories?.map((category, index) => {
|
||||
// + 1 because in db, sort start from 1
|
||||
|
||||
// swap adjacent cards
|
||||
if (index === currentIndex) {
|
||||
return { ...category, sort: newIndex + 1 };
|
||||
}
|
||||
if (index === newIndex) {
|
||||
return { ...category, sort: currentIndex + 1 };
|
||||
}
|
||||
return category;
|
||||
}),
|
||||
'sort',
|
||||
);
|
||||
setCategories(updatedOrderCategories);
|
||||
updateCategoryOrderInDb(updatedOrderCategories || []);
|
||||
const systemCategories = categories.filter(cat => cat.id === 1);
|
||||
|
||||
const updatedOrderCategories = [...systemCategories, ...data];
|
||||
requestAnimationFrame(() => {
|
||||
setCategories(updatedOrderCategories);
|
||||
updateCategoryOrderInDb(updatedOrderCategories);
|
||||
});
|
||||
};
|
||||
|
||||
const renderItem = ({
|
||||
item,
|
||||
drag,
|
||||
isActive,
|
||||
}: RenderItemParams<ExtendedCategory>) => (
|
||||
<CategoryCard
|
||||
category={item}
|
||||
getCategories={refreshCategories}
|
||||
drag={drag}
|
||||
isActive={isActive}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView excludeTop>
|
||||
<Appbar
|
||||
@@ -74,18 +83,14 @@ const CategoriesScreen = () => {
|
||||
{isLoading ? (
|
||||
<CategorySkeletonLoading width={360.7} height={89.5} theme={theme} />
|
||||
) : (
|
||||
<FlatList
|
||||
data={categories}
|
||||
<DraggableFlatList
|
||||
data={userCategories}
|
||||
contentContainerStyle={styles.contentCtn}
|
||||
renderItem={({ item, index }) => (
|
||||
<CategoryCard
|
||||
category={item}
|
||||
getCategories={refreshCategories}
|
||||
categoryIndex={index}
|
||||
updateCategorySort={updateCategorySort}
|
||||
totalCategories={categories?.length || 0}
|
||||
/>
|
||||
)}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item, index) => `${item.id}-${index}`}
|
||||
onDragEnd={onDragEnd}
|
||||
activationDistance={10}
|
||||
autoscrollSpeed={100}
|
||||
ListEmptyComponent={
|
||||
<EmptyView
|
||||
icon="Σ(ಠ_ಠ)"
|
||||
@@ -118,7 +123,7 @@ export default CategoriesScreen;
|
||||
const styles = StyleSheet.create({
|
||||
contentCtn: {
|
||||
flexGrow: 1,
|
||||
paddingBottom: 100,
|
||||
paddingBottom: 270,
|
||||
paddingVertical: 16,
|
||||
},
|
||||
fab: {
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
|
||||
import { Category } from '@database/types';
|
||||
import { useTheme } from '@hooks/persisted';
|
||||
import AddCategoryModal from './AddCategoryModal';
|
||||
import { useBoolean } from '@hooks';
|
||||
import { overlay, Portal } from 'react-native-paper';
|
||||
import { Badge, Portal } from 'react-native-paper';
|
||||
import IconButton from '@components/IconButtonV2/IconButtonV2';
|
||||
import DeleteCategoryModal from './DeleteCategoryModal';
|
||||
|
||||
interface CategoryCardProps {
|
||||
categoryIndex: number;
|
||||
category: Category;
|
||||
getCategories: () => Promise<void>;
|
||||
updateCategorySort: (currentIndex: number, newIndex: number) => void;
|
||||
totalCategories: number;
|
||||
drag: () => void;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const CategoryCard: React.FC<CategoryCardProps> = ({
|
||||
category,
|
||||
getCategories,
|
||||
categoryIndex,
|
||||
updateCategorySort,
|
||||
totalCategories,
|
||||
drag,
|
||||
isActive,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -38,80 +37,76 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
|
||||
setFalse: closeDeleteCategoryModal,
|
||||
} = useBoolean();
|
||||
|
||||
const fontWeight = category.sort === 1 ? '700' : '300';
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={[
|
||||
styles.cardCtn,
|
||||
{
|
||||
backgroundColor:
|
||||
category.sort === 1
|
||||
? theme.isDark
|
||||
? overlay(2, theme.surface2)
|
||||
: theme.primaryContainer
|
||||
: category.id === 2
|
||||
? theme.tertiaryContainer
|
||||
: theme.secondaryContainer,
|
||||
backgroundColor: theme.secondaryContainer,
|
||||
opacity: isActive ? 0.8 : 1,
|
||||
elevation: isActive ? 8 : 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={styles.nameCtn}>
|
||||
<IconButton
|
||||
name="label-outline"
|
||||
color={theme.onSurface}
|
||||
padding={0}
|
||||
theme={theme}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.name,
|
||||
{
|
||||
color: theme.onSurface,
|
||||
fontWeight,
|
||||
},
|
||||
]}
|
||||
onPress={showCategoryModal}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.buttonsCtn}>
|
||||
<IconButton
|
||||
name="menu-up"
|
||||
color={theme.onSurface}
|
||||
onPress={() => updateCategorySort(categoryIndex, categoryIndex - 1)}
|
||||
theme={theme}
|
||||
disabled={
|
||||
categoryIndex <= 0 || (categoryIndex === 1 && category.id === 2)
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
name="menu-down"
|
||||
color={theme.onSurface}
|
||||
style={styles.orderDownBtn}
|
||||
onPress={() => updateCategorySort(categoryIndex, categoryIndex + 1)}
|
||||
theme={theme}
|
||||
disabled={categoryIndex + 1 >= totalCategories}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
onLongPress={drag}
|
||||
style={styles.dragHandle}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<IconButton
|
||||
name="drag-horizontal-variant"
|
||||
color={theme.onSurface}
|
||||
theme={theme}
|
||||
padding={8}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.nameCtn}>
|
||||
<Text
|
||||
style={[
|
||||
styles.name,
|
||||
{
|
||||
color: theme.onSurface,
|
||||
},
|
||||
]}
|
||||
onPress={showCategoryModal}
|
||||
disabled={category.id === 2}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
{category.id === 2 && (
|
||||
<Badge
|
||||
style={{
|
||||
backgroundColor: theme.primary,
|
||||
paddingHorizontal: 8,
|
||||
}}
|
||||
>
|
||||
System
|
||||
</Badge>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.flex} />
|
||||
<IconButton
|
||||
name="pencil-outline"
|
||||
color={theme.onSurface}
|
||||
style={styles.manageBtn}
|
||||
onPress={showCategoryModal}
|
||||
theme={theme}
|
||||
/>
|
||||
{categoryIndex !== 0 && category.id !== 2 ? (
|
||||
<View style={{ opacity: category.id === 2 ? 0.4 : 1 }}>
|
||||
<IconButton
|
||||
name="pencil-outline"
|
||||
color={category.id === 2 ? theme.outline : theme.onSurface}
|
||||
style={styles.manageBtn}
|
||||
onPress={showCategoryModal}
|
||||
theme={theme}
|
||||
disabled={category.id === 2}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ opacity: category.id === 2 ? 0.4 : 1 }}>
|
||||
<IconButton
|
||||
name="delete-outline"
|
||||
color={theme.onSurface}
|
||||
color={category.id === 2 ? theme.outline : theme.onSurface}
|
||||
style={styles.manageBtn}
|
||||
onPress={showDeleteCategoryModal}
|
||||
theme={theme}
|
||||
disabled={category.id === 2}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Portal>
|
||||
@@ -142,12 +137,14 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
cardCtn: {
|
||||
borderRadius: 12,
|
||||
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.25)',
|
||||
marginBottom: 8,
|
||||
marginHorizontal: 16,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
dragHandle: {
|
||||
marginRight: 4,
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
@@ -155,17 +152,15 @@ const styles = StyleSheet.create({
|
||||
marginLeft: 16,
|
||||
},
|
||||
name: {
|
||||
marginHorizontal: 16,
|
||||
marginLeft: 16,
|
||||
marginRight: 8,
|
||||
},
|
||||
nameCtn: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 8,
|
||||
paddingRight: 16,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
orderDownBtn: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +45,18 @@ export const useLibrary = (): UseLibraryReturnType => {
|
||||
novelIds: (c.novelIds ?? '').split(',').map(Number),
|
||||
}));
|
||||
|
||||
setCategories(res);
|
||||
const filteredCategories = res.filter((cat, index) => {
|
||||
if (index !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hasUserCategories = res.length > 2;
|
||||
const hasNovels = cat.novelIds.length > 0 && cat.novelIds[0] !== 0;
|
||||
|
||||
return !hasUserCategories || hasNovels;
|
||||
});
|
||||
|
||||
setCategories(filteredCategories);
|
||||
}, []);
|
||||
|
||||
const getLibrary = useCallback(async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import Animated, {
|
||||
useAnimatedStyle,
|
||||
} from 'react-native-reanimated';
|
||||
import ExportNovelAsEpubButton from './ExportNovelAsEpubButton';
|
||||
import { ChapterInfo, NovelInfo } from '@database/types';
|
||||
import { NovelInfo } from '@database/types';
|
||||
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import { MaterialDesignIconName } from '@type/icon';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user