feat: Collapsible Updates and Downloads (#562)

* implemented extendable list for updates

* finished creating extendable list (buggy deleteChapter)

* fixed downloadbutton bug

* fixed ts problems

* set navigatetonovel to onLongPress

* removed

* key fixes

* implemented extendable list for updates

* finished creating extendable list (buggy deleteChapter)

* fixed downloadbutton bug

* fixed ts problems

* set navigatetonovel to onLongPress

* removed

* key fixes

* translation strings

* single chapter + style updates

* removed test method
This commit is contained in:
Patrick Loser
2023-03-01 09:59:48 +01:00
committed by GitHub
parent 3289f357d9
commit cf19eb6892
14 changed files with 555 additions and 675 deletions
+6
View File
@@ -30,6 +30,12 @@ export interface ChapterItem {
bookmark: number;
downloaded: number;
}
export interface ChapterItemExtended extends ChapterItem {
novelId: number;
novelUrl: string;
sourceId: number;
novelName: string;
}
export interface DownloadedChapter {
downloadId: number;
+32 -61
View File
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { Appbar as MaterialAppbar } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { ScreenContainer } from '../../components/Common';
import EmptyView from '../../components/EmptyView';
@@ -15,25 +15,31 @@ import {
import { useTheme } from '@hooks/useTheme';
import {
deleteChapterAction,
downloadChapterAction,
} from '../../redux/novel/novel.actions';
import UpdatesItem from '../updates/components/UpdatesItem';
import RemoveDownloadsDialog from './components/RemoveDownloadsDialog';
import { openChapter, openNovel } from '@utils/handleNavigateParams';
import UpdatesSkeletonLoading from '@screens/updates/components/UpdatesSkeletonLoading';
import UpdateNovelCard from '@screens/updates/components/UpdateNovelCard';
import { getString } from '@strings/translations';
const Downloads = ({ navigation }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { downloadQueue } = useSelector(state => state.downloadsReducer);
const [loading, setLoading] = useState(true);
const [chapters, setChapters] = useState([]);
const groupUpdatesByDate = chapters => {
const dateGroups = chapters.reduce((groups, item) => {
const novelId = item.novelId;
if (!groups[novelId]) {
groups[novelId] = [];
}
groups[novelId].push(item);
return groups;
}, {});
return Object.values(dateGroups);
};
/**
* Confirm Clear downloads Dialog
@@ -49,57 +55,25 @@ const Downloads = ({ navigation }) => {
};
const ListEmptyComponent = () =>
!loading && <EmptyView icon="(˘・_・˘)" description="No downloads" />;
!loading && (
<EmptyView
icon="(˘・_・˘)"
description={getString('downloadScreen.noDownloads')}
/>
);
useEffect(() => {
getChapters();
}, []);
const onPress = useCallback(
item => navigation.navigate('Chapter', openChapter(item, item)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const onPressCover = useCallback(
item => navigation.navigate('Novel', openNovel(item)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const downloadChapter = (
sourceId,
novelUrl,
novelId,
chapterUrl,
chapterName,
chapterId,
) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
novelId,
chapterUrl,
chapterName,
chapterId,
),
);
const deleteChapter = (sourceId, novelId, chapterId, chapterName) => {
dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
setChapters(chaps => chaps.filter(item => item.chapterId !== chapterId));
};
const renderItem = ({ item }) => (
<UpdatesItem
const renderItem = ({ item, index }) => (
<UpdateNovelCard
keyProp={index}
item={item}
dispatch={dispatch}
theme={theme}
onPress={() => onPress(item)}
onPressCover={() => onPressCover(item)}
downloadQueue={downloadQueue}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
descriptionText={getString('downloadScreen.downloadChapters')}
removeItemFromList
/>
);
@@ -114,17 +88,14 @@ const Downloads = ({ navigation }) => {
/>
)}
</Appbar>
<List.InfoItem
title="Downloads are saved in a SQLite Database."
theme={theme}
/>
<List.InfoItem title={getString('downloadScreen.dbInfo')} theme={theme} />
{loading ? (
<UpdatesSkeletonLoading theme={theme} />
) : (
<FlatList
contentContainerStyle={styles.flatList}
data={chapters}
keyExtractor={item => item.chapterId.toString()}
data={groupUpdatesByDate(chapters)}
//keyExtractor={item => item.chapterId.toString()}
renderItem={renderItem}
ListEmptyComponent={<ListEmptyComponent />}
/>
+13 -6
View File
@@ -140,15 +140,15 @@ const Novel = ({ route, navigation }) => {
const [jumpToChapterModal, showJumpToChapterModal] = useState(false);
const downloadChapter = (chapterUrl, chapterName, chapterId) =>
const downloadChapter = chapter =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
novelId,
chapterUrl,
chapterName,
chapterId,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId,
),
);
@@ -224,8 +224,15 @@ const Novel = ({ route, navigation }) => {
return list;
}, [selected]);
const deleteChapter = (chapterId, chapterName) =>
dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
const deleteChapter = chapter =>
dispatch(
deleteChapterAction(
sourceId,
novelId,
chapter.chapterId,
chapter.chapterName,
),
);
const isSelected = chapterId => {
return selected.some(obj => obj.chapterId === chapterId);
@@ -1,88 +0,0 @@
import React from 'react';
import { ActivityIndicator } from 'react-native';
import { IconButton, Menu, overlay } from 'react-native-paper';
export const DownloadButton = ({
downloadQueue,
chapter,
theme,
deleteChapter,
downloadChapter,
hideDeleteChapterMenu,
showDeleteChapterMenu,
deleteChapterMenu,
}) => {
if (downloadQueue.some(chap => chap.chapterId === chapter.chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<DeleteChapterButton theme={theme} onPress={showDeleteChapterMenu} />
}
contentStyle={{ backgroundColor: overlay(2, theme.surface) }}
>
<Menu.Item
onPress={() => deleteChapter(chapter.chapterId, chapter.chapterName)}
title="Delete"
titleStyle={{ color: theme.onSurface }}
/>
</Menu>
);
} else {
return (
<DownloadChapterButton
theme={theme}
onPress={() =>
downloadChapter(
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId,
)
}
/>
);
}
};
export const ChapterDownloadingButton = ({ theme }) => (
<ActivityIndicator
color={theme.outline}
size={25}
style={{ margin: 3.5, padding: 5 }}
/>
);
export const DownloadChapterButton = ({ theme, onPress }) => (
<IconButton
icon="arrow-down-circle-outline"
animated
iconColor={theme.outline}
size={25}
onPress={onPress}
style={{ margin: 2 }}
/>
);
export const DeleteChapterButton = ({ theme, onPress }) => (
<IconButton
icon="check-circle"
animated
iconColor={theme.onSurface}
size={25}
onPress={onPress}
style={{ margin: 2 }}
/>
);
export const ChapterBookmarkButton = ({ theme }) => (
<IconButton
icon="bookmark"
iconColor={theme.primary}
size={18}
style={{ marginLeft: 2 }}
/>
);
@@ -0,0 +1,120 @@
import { ChapterItemExtended } from '@database/types';
import { MD3ThemeType } from '@theme/types';
import React, { useState } from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { IconButton, Menu, overlay } from 'react-native-paper';
interface DownloadButtonProps {
downloadQueue: ChapterItemExtended[];
chapter: ChapterItemExtended;
theme: MD3ThemeType;
deleteChapterMenuVisible: boolean;
deleteChapter: (chapter: ChapterItemExtended) => void;
downloadChapter: (chapter: ChapterItemExtended) => (dispatch: any) => void;
hideDeleteChapterMenu: () => void;
showDeleteChapterMenu: () => void;
}
export const DownloadButton: React.FC<DownloadButtonProps> = ({
downloadQueue,
chapter,
theme,
deleteChapter,
downloadChapter,
hideDeleteChapterMenu,
showDeleteChapterMenu,
deleteChapterMenuVisible,
}) => {
const [downloaded, setDownloaded] = useState(chapter.downloaded);
if (downloadQueue.some(chap => chap.chapterId === chapter.chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (downloaded === 0) {
return (
<DownloadChapterButton
theme={theme}
onPress={() => {
downloadChapter(chapter);
setDownloaded(1);
}}
/>
);
} else {
return (
<Menu
visible={deleteChapterMenuVisible}
onDismiss={hideDeleteChapterMenu}
anchor={
<DeleteChapterButton theme={theme} onPress={showDeleteChapterMenu} />
}
contentStyle={{ backgroundColor: overlay(2, theme.surface) }}
>
<Menu.Item
onPress={() => {
deleteChapter(chapter);
setDownloaded(0);
}}
title="Delete"
titleStyle={{ color: theme.onSurface }}
/>
</Menu>
);
}
};
interface theme {
theme: MD3ThemeType;
}
type buttonPropType = theme & {
onPress: () => void;
};
export const ChapterDownloadingButton: React.FC<theme> = ({ theme }) => (
<ActivityIndicator
color={theme.outline}
size={25}
style={styles.activityIndicator}
/>
);
export const DownloadChapterButton: React.FC<buttonPropType> = ({
theme,
onPress,
}) => (
<IconButton
icon="arrow-down-circle-outline"
animated
iconColor={theme.outline}
size={25}
onPress={onPress}
style={styles.iconButton}
/>
);
export const DeleteChapterButton: React.FC<buttonPropType> = ({
theme,
onPress,
}) => (
<IconButton
icon="check-circle"
animated
iconColor={theme.onSurface}
size={25}
onPress={onPress}
style={styles.iconButton}
/>
);
export const ChapterBookmarkButton: React.FC<theme> = ({ theme }) => (
<IconButton
icon="bookmark"
iconColor={theme.primary}
size={18}
style={styles.iconButtonLeft}
/>
);
const styles = StyleSheet.create({
activityIndicator: { margin: 3.5, padding: 5 },
iconButton: { margin: 2 },
iconButtonLeft: { marginLeft: 2 },
});
-116
View File
@@ -1,116 +0,0 @@
import React, { memo, useState } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import color from 'color';
import { Row } from '../../../components/Common';
import {
ChapterBookmarkButton,
DownloadButton,
} from './Chapter/ChapterDownloadButtons';
import { parseChapterNumber } from '@utils/parseChapterNumber';
const ChapterItem = ({
chapter,
theme,
index,
showChapterTitles,
downloadQueue,
downloadChapter,
deleteChapter,
isSelected,
onSelectPress,
onSelectLongPress,
navigateToChapter,
showProgressPercentage,
}) => {
const { chapterId, chapterName, read, releaseDate, bookmark } = chapter;
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
const chapterNumber = parseChapterNumber(chapterName);
return (
<Pressable
style={[
styles.chapterCardContainer,
isSelected(chapterId) && {
backgroundColor: color(theme.primary).alpha(0.12).string(),
},
]}
onPress={() => onSelectPress(chapter, () => navigateToChapter(chapter))}
onLongPress={() => onSelectLongPress(chapter)}
android_ripple={{ color: theme.rippleColor }}
>
<Row style={{ flex: 1, overflow: 'hidden' }}>
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
<View>
<Text
style={{
color: read
? theme.outline
: bookmark
? theme.primary
: theme.onSurface,
}}
numberOfLines={1}
>
{showChapterTitles
? chapterNumber
? 'Chapter ' + chapterNumber
: 'Chapter ' + index
: chapterName}
</Text>
<View
style={{
flexDirection: 'row',
marginTop: 5,
}}
>
{releaseDate ? (
<Text
style={{
color: read
? theme.outline
: bookmark
? theme.primary
: theme.onSurfaceVariant,
fontSize: 12,
}}
numberOfLines={1}
>
{releaseDate}
</Text>
) : null}
{showProgressPercentage(chapter)}
</View>
</View>
</Row>
<DownloadButton
downloadQueue={downloadQueue}
chapter={chapter}
theme={theme}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
hideDeleteChapterMenu={hideDeleteChapterMenu}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenu={deleteChapterMenu}
/>
</Pressable>
);
};
export default memo(ChapterItem);
const styles = StyleSheet.create({
chapterCardContainer: {
height: 64,
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
});
@@ -0,0 +1,160 @@
import React, { memo, useState } from 'react';
import { View, Text, StyleSheet, Pressable, ViewStyle } from 'react-native';
import color from 'color';
import { Row } from '../../../components/Common';
import {
ChapterBookmarkButton,
DownloadButton,
} from './Chapter/ChapterDownloadButtons';
import { parseChapterNumber } from '@utils/parseChapterNumber';
import { ThemeColors } from '@theme/types';
import { ChapterItemExtended } from '@database/types';
interface ChapterItemProps {
chapter: ChapterItemExtended;
theme: ThemeColors;
index?: number;
downloadQueue: any;
showChapterTitles: boolean;
isSelected?: (chapterId: number) => boolean;
downloadChapter: (chapter: ChapterItemExtended) => (dispatch: any) => void;
deleteChapter: (chapter: ChapterItemExtended) => void;
onSelectPress?: (chapter: ChapterItemExtended, arg1: () => void) => void;
onSelectLongPress?: (chapter: ChapterItemExtended) => void;
navigateToChapter: (chapter: ChapterItemExtended) => void;
showProgressPercentage?: (chapter: ChapterItemExtended) => void;
containerStyle?: ViewStyle;
showDate?: boolean;
left?: Function;
textSize?: number;
}
const ChapterItem: React.FC<ChapterItemProps> = ({
chapter,
theme,
index,
showChapterTitles,
downloadQueue,
downloadChapter,
deleteChapter,
isSelected,
onSelectPress,
onSelectLongPress,
navigateToChapter,
showProgressPercentage,
containerStyle,
showDate,
left,
textSize,
}) => {
const { chapterId, chapterName, read, releaseDate, bookmark } = chapter;
const [deleteChapterMenuVisible, setDeleteChapterMenuVisible] =
useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenuVisible(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenuVisible(false);
const chapterNumber = parseChapterNumber(chapterName);
return (
<Pressable
key={chapterId.toString()}
style={[
styles.chapterCardContainer,
isSelected?.(chapterId) && {
backgroundColor: color(theme.primary).alpha(0.12).string(),
},
containerStyle,
]}
onPress={() => {
onSelectPress
? onSelectPress(chapter, () => navigateToChapter(chapter))
: navigateToChapter(chapter);
}}
onLongPress={() => onSelectLongPress?.(chapter)}
android_ripple={{ color: theme.rippleColor }}
>
<Row style={styles.row}>
{left?.()}
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
<View>
<Text
style={[
styles.title,
{
fontSize: textSize ?? 14,
color: read
? theme.outline
: bookmark
? theme.primary
: theme.onSurface,
},
]}
numberOfLines={1}
>
{showChapterTitles
? chapterNumber
? 'Chapter ' + chapterNumber
: 'Chapter ' + index
: chapterName}
</Text>
<View style={styles.textRow}>
{releaseDate && showDate ? (
<Text
style={[
{
color: read
? theme.outline
: bookmark
? theme.primary
: theme.onSurfaceVariant,
},
styles.text,
]}
numberOfLines={1}
>
{releaseDate}
</Text>
) : null}
{showProgressPercentage?.(chapter)}
</View>
</View>
</Row>
<DownloadButton
downloadQueue={downloadQueue}
chapter={chapter}
theme={theme}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
hideDeleteChapterMenu={hideDeleteChapterMenu}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenuVisible={deleteChapterMenuVisible}
/>
</Pressable>
);
};
export default memo(ChapterItem);
const styles = StyleSheet.create({
chapterCardContainer: {
height: 64,
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
title: {
marginTop: 5,
},
text: {
fontSize: 12,
},
textRow: {
flexDirection: 'row',
marginTop: 5,
},
row: { flex: 1, overflow: 'hidden' },
});
+17 -65
View File
@@ -1,5 +1,4 @@
import React from 'react';
import { useNavigation } from '@react-navigation/native';
import dayjs from 'dayjs';
import { RefreshControl, SectionList, StyleSheet, Text } from 'react-native';
@@ -7,32 +6,20 @@ import { EmptyView, ErrorScreenV2, SearchbarV2 } from '../../components';
import { convertDateToISOString } from '../../database/utils/convertDateToISOString';
import { ChapterItem, Update } from '../../database/types';
import { Update } from '../../database/types';
import { useSearch, useUpdates } from '../../hooks';
import { useAppDispatch } from '../../redux/hooks';
import UpdateCard from './components/UpdateCard/UpdateCard';
import { updateLibraryAction } from '../../redux/updates/updates.actions';
import {
deleteChapterAction,
downloadChapterAction,
} from '../../redux/novel/novel.actions';
import { getString } from '../../../strings/translations';
import { ThemeColors } from '../../theme/types';
import { useTheme } from '@hooks/useTheme';
import {
openChapter,
openChapterChapterTypes,
openChapterNovelTypes,
openChapterFunctionTypes,
openNovel,
openNovelProps,
} from '@utils/handleNavigateParams';
import UpdatesSkeletonLoading from './components/UpdatesSkeletonLoading';
import UpdateNovelCard from './components/UpdateNovelCard';
const UpdatesScreen = () => {
const theme = useTheme();
const { navigate } = useNavigation();
const dispatch = useAppDispatch();
const {
@@ -47,7 +34,6 @@ const UpdatesScreen = () => {
} = useUpdates();
const { searchText, setSearchText, clearSearchbar } = useSearch();
const onChangeText = (text: string) => {
setSearchText(text);
searchUpdates(text);
@@ -59,15 +45,19 @@ const UpdatesScreen = () => {
};
const groupUpdatesByDate = (rawHistory: Update[]) => {
const dateGroups = rawHistory.reduce<Record<string, Update[]>>(
const dateGroups = rawHistory.reduce<Record<string, Update[][]>>(
(groups, item) => {
const date = convertDateToISOString(item.updateTime);
const novelId = item.novelId;
if (!groups[date]) {
groups[date] = [];
}
if (!groups[date][novelId]) {
groups[date][novelId] = [];
}
groups[date].push(item);
groups[date][novelId].push(item);
return groups;
},
@@ -77,49 +67,12 @@ const UpdatesScreen = () => {
const groupedHistory = Object.keys(dateGroups).map(date => {
return {
date,
data: dateGroups[date],
data: dateGroups[date].filter(val => val !== null),
};
});
return groupedHistory;
};
const handleDownloadChapter = (
sourceId: number,
novelUrl: string,
novelId: number,
chapter: ChapterItem,
) =>
dispatch(
downloadChapterAction(
sourceId,
novelUrl,
novelId,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId,
),
);
const handleDeleteChapter = (
sourceId: number,
novelId: number,
chapterId: number,
chapterName: string,
) => dispatch(deleteChapterAction(sourceId, novelId, chapterId, chapterName));
const navigateToChapter = (
novel: openChapterNovelTypes,
chapter: openChapterChapterTypes,
) =>
navigate(
'Chapter' as never,
openChapter(novel, chapter) as openChapterFunctionTypes as never,
);
const navigateToNovel = (novel: openNovelProps) =>
navigate('Novel' as never, openNovel(novel) as openNovelProps as never);
return (
<>
<SearchbarV2
@@ -148,27 +101,25 @@ const UpdatesScreen = () => {
) : null
}
contentContainerStyle={styles.listContainer}
keyExtractor={item => item.chapterId.toString()}
sections={groupUpdatesByDate(searchText ? searchResults : updates)}
renderSectionHeader={({ section: { date } }) => (
<Text style={[styles.dateHeader, { color: theme.onSurface }]}>
{dayjs(date).calendar()}
</Text>
)}
keyExtractor={item => item[0].chapterId.toString()}
renderItem={({ item }) => (
<UpdateCard
<UpdateNovelCard
item={item}
navigateToChapter={navigateToChapter}
navigateToNovel={navigateToNovel}
handleDownloadChapter={handleDownloadChapter}
handleDeleteChapter={handleDeleteChapter}
dispatch={dispatch}
theme={theme}
descriptionText={getString('updatesScreen.newChapters')}
/>
)}
ListEmptyComponent={
<EmptyView
icon="(˘・_・˘)"
description="No recent updates"
description={getString('updatesScreen.emptyView')}
theme={theme}
/>
}
@@ -205,7 +156,8 @@ const styles = StyleSheet.create({
},
dateHeader: {
paddingHorizontal: 16,
paddingVertical: 8,
paddingTop: 8,
paddingBottom: 2,
},
lastUpdateTime: {
paddingHorizontal: 16,
@@ -1,64 +0,0 @@
import React from 'react';
import { Menu, overlay } from 'react-native-paper';
import {
ChapterDownloadingButton,
DeleteChapterButton,
DownloadChapterButton,
} from '../../novel/components/Chapter/ChapterDownloadButtons';
export const DownloadButton = ({
downloadQueue,
theme,
chapter,
downloadChapter,
deleteChapter,
deleteChapterMenu,
showDeleteChapterMenu,
hideDeleteChapterMenu,
}) => {
if (downloadQueue.some(chap => chap.chapterId === chapter.chapterId)) {
return <ChapterDownloadingButton theme={theme} />;
} else if (chapter.downloaded === 1) {
return (
<Menu
visible={deleteChapterMenu}
onDismiss={hideDeleteChapterMenu}
anchor={
<DeleteChapterButton theme={theme} onPress={showDeleteChapterMenu} />
}
contentStyle={{ backgroundColor: overlay(2, theme.surface) }}
>
<Menu.Item
onPress={() => {
deleteChapter(
chapter.sourceId,
chapter.novelId,
chapter.chapterId,
chapter.chapterName,
);
hideDeleteChapterMenu();
}}
title="Delete"
titleStyle={{ color: theme.onSurface }}
/>
</Menu>
);
} else {
return (
<DownloadChapterButton
theme={theme}
onPress={() =>
downloadChapter(
chapter.sourceId,
chapter.novelUrl,
chapter.novelId,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId,
)
}
/>
);
}
};
@@ -1,178 +0,0 @@
import {
ActivityIndicator,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';
import React, { useState } from 'react';
import { ChapterItem, Update } from '../../../../database/types';
import FastImage from 'react-native-fast-image';
import { IconButtonV2 } from '../../../../components';
import { useDownloadQueue } from '../../../../redux/hooks';
import { Menu } from 'react-native-paper';
import { ThemeColors } from '../../../../theme/types';
import { coverPlaceholderColor } from '../../../../theme/colors';
import {
openChapterChapterTypes,
openChapterNovelTypes,
openNovelProps,
} from '@utils/handleNavigateParams';
interface UpdateCardProps {
item: Update;
navigateToChapter: (
novel: openChapterNovelTypes,
chapter: openChapterChapterTypes,
) => void;
navigateToNovel: (novel: openNovelProps) => void;
handleDownloadChapter: (
sourceId: number,
novelUrl: string,
novelId: number,
chapter: ChapterItem,
) => void;
handleDeleteChapter: (
sourceId: number,
novelId: number,
chapterId: number,
chapterName: string,
) => void;
theme: ThemeColors;
}
const UpdateCard: React.FC<UpdateCardProps> = ({
item,
navigateToChapter,
navigateToNovel,
handleDownloadChapter,
handleDeleteChapter,
theme,
}) => {
const titleColor = item.read ? theme.outline : theme.onSurface;
const chapterNameColor = item.bookmark
? theme.primary
: item.read
? theme.outline
: theme.onSurfaceVariant;
const downloadQueue = useDownloadQueue();
const isDownloading = downloadQueue.some(
(chapter: ChapterItem) => chapter.chapterId === item.chapterId,
);
const [menu, setMenu] = useState(false);
return (
<Pressable
style={styles.container}
android_ripple={{ color: theme.rippleColor }}
onPress={() => {
navigateToChapter(item, item);
}}
>
<View style={styles.imageContainer}>
<Pressable onPress={() => navigateToNovel(item)}>
<FastImage source={{ uri: item.novelCover }} style={styles.cover} />
</Pressable>
<View style={styles.nameContainer}>
<Text style={[styles.name, { color: titleColor }]} numberOfLines={1}>
{item.novelName}
</Text>
<Text
style={[styles.chapterName, { color: chapterNameColor }]}
numberOfLines={1}
>
{item.chapterName}
</Text>
</View>
</View>
{isDownloading ? (
<ActivityIndicator
color={theme.outline}
size={26}
style={styles.downloading}
/>
) : !item.downloaded ? (
<IconButtonV2
name="arrow-down-circle-outline"
size={25}
onPress={() =>
handleDownloadChapter(
item.sourceId,
item.novelUrl,
item.novelId,
item as ChapterItem,
)
}
color={theme.outline}
theme={theme}
/>
) : (
<Menu
visible={menu}
onDismiss={() => setMenu(false)}
anchor={
<IconButtonV2
name="check-circle"
size={25}
onPress={() => setMenu(true)}
theme={theme}
/>
}
contentStyle={{ backgroundColor: theme.overlay3 }}
>
<Menu.Item
onPress={() =>
handleDeleteChapter(
item.sourceId,
item.novelId,
item.chapterId,
item.chapterName,
)
}
title="Delete"
titleStyle={{ color: theme.onSurface }}
/>
</Menu>
)}
</Pressable>
);
};
export default UpdateCard;
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
},
cover: {
height: 40,
width: 40,
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
imageContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
nameContainer: {
flex: 1,
marginLeft: 16,
paddingRight: 16,
},
name: {},
chapterName: {
marginTop: 4,
fontSize: 12,
},
downloading: {
margin: 8,
},
});
@@ -0,0 +1,194 @@
import { FlatList, StyleSheet, View } from 'react-native';
import React, { useState } from 'react';
import { ChapterItemExtended, Update } from '../../../database/types';
import FastImage from 'react-native-fast-image';
import { List } from 'react-native-paper';
import { ThemeColors } from '../../../theme/types';
import { coverPlaceholderColor } from '../../../theme/colors';
import {
openChapter,
openChapterFunctionTypes,
openNovel,
openNovelProps,
} from '@utils/handleNavigateParams';
import { Dispatch } from 'redux';
import {
deleteChapterAction,
downloadChapterAction,
} from '@redux/novel/novel.actions';
import { useNavigation } from '@react-navigation/native';
import ChapterItem from '@screens/novel/components/ChapterItem';
import { useSelector } from 'react-redux';
import { RootState } from '@redux/store';
interface UpdateCardProps {
item: Update[];
dispatch: Dispatch<any>;
theme: ThemeColors;
descriptionText: string;
removeItemFromList?: boolean;
}
const UpdateNovelCard: React.FC<UpdateCardProps> = ({
item,
dispatch,
theme,
descriptionText,
removeItemFromList,
}) => {
const { navigate } = useNavigation();
const [chapterList, setChapterList] = useState(item);
const handleDownloadChapter = (chapter: ChapterItemExtended) =>
dispatch(
downloadChapterAction(
chapter.sourceId,
chapter.novelUrl,
chapter.novelId,
chapter.chapterUrl,
chapter.chapterName,
chapter.chapterId,
),
);
const handleDeleteChapter = (chapter: ChapterItemExtended) => {
dispatch(
deleteChapterAction(
chapter.sourceId,
chapter.novelId,
chapter.chapterId,
chapter.chapterName,
),
);
if (removeItemFromList) {
//@ts-ignore
let index = item.indexOf(chapter);
item.splice(index, 1);
setChapterList(Array.from(item));
}
};
const navigateToChapter = (chapter: ChapterItemExtended) =>
navigate(
'Chapter' as never,
openChapter(chapter, chapter) as openChapterFunctionTypes as never,
);
const navigateToNovel = (novel: openNovelProps) =>
navigate('Novel' as never, openNovel(novel) as openNovelProps as never);
const { downloadQueue } = useSelector(
(state: RootState) => state.downloadsReducer,
);
const BookCover = () => {
return (
<FastImage
source={{ uri: chapterList[0].novelCover }}
style={styles.cover}
/>
);
};
if (chapterList.length > 1) {
return (
<View style={styles.relativ}>
<List.Accordion
title={chapterList[0].novelName}
left={BookCover}
theme={{ colors: theme }}
style={[styles.container, styles.padding]}
description={chapterList.length + ' ' + descriptionText}
onLongPress={() => navigateToNovel(chapterList[0])}
>
<FlatList
data={chapterList}
keyExtractor={it => it.chapterId.toString()}
style={styles.flatList}
renderItem={it => {
return (
<ChapterItem
chapter={it.item}
theme={theme}
showChapterTitles={false}
downloadQueue={downloadQueue}
downloadChapter={handleDownloadChapter}
deleteChapter={handleDeleteChapter}
navigateToChapter={navigateToChapter}
containerStyle={styles.chapterItem}
onSelectLongPress={() => navigateToNovel(chapterList[0])}
/>
);
}}
scrollEnabled={false}
/>
</List.Accordion>
</View>
);
} else if (chapterList.length > 0) {
return (
<View style={[styles.relativ]}>
<ChapterItem
chapter={chapterList[0]}
theme={theme}
showChapterTitles={false}
downloadQueue={downloadQueue}
downloadChapter={handleDownloadChapter}
deleteChapter={handleDeleteChapter}
navigateToChapter={navigateToChapter}
onSelectLongPress={() => navigateToNovel(chapterList[0])}
textSize={16}
left={() => (
<View style={styles.chapterCover}>
<BookCover />
</View>
)}
/>
</View>
);
}
return null;
};
export default UpdateNovelCard;
const styles = StyleSheet.create({
relativ: {
position: 'relative',
},
padding: {
paddingHorizontal: 16,
paddingVertical: 3,
height: 64,
},
container: {
justifyContent: 'space-between',
},
cover: {
height: 40,
width: 40,
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
chapterCover: {
marginRight: 8,
},
imageContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
},
nameContainer: {
flex: 1,
marginLeft: 16,
paddingRight: 16,
},
chapterName: {
marginTop: 4,
fontSize: 12,
},
downloading: {
margin: 8,
},
flatList: { marginLeft: -64 },
chapterItem: { paddingLeft: 48 },
});
@@ -1,96 +0,0 @@
import React, { memo, useState } from 'react';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import FastImage from 'react-native-fast-image';
import { coverPlaceholderColor } from '../../../theme/colors';
import { DownloadButton } from './DownloadButton';
const UpdateCard = ({
item,
theme,
onPress,
onPressCover,
downloadQueue,
downloadChapter,
deleteChapter,
}) => {
const [deleteChapterMenu, setDeleteChapterMenu] = useState(false);
const showDeleteChapterMenu = () => setDeleteChapterMenu(true);
const hideDeleteChapterMenu = () => setDeleteChapterMenu(false);
return (
<Pressable
style={styles.updateCard}
onPress={onPress}
android_ripple={{ color: theme.rippleColor }}
>
<>
<Pressable onPress={onPressCover}>
<FastImage
source={{ uri: item.novelCover }}
style={styles.updateIcon}
/>
</Pressable>
<View
style={{
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<View style={styles.chapterDetails}>
<Text
style={{
color: item.read ? theme.outline : theme.onSurface,
}}
numberOfLines={1}
>
{item.novelName}
</Text>
<Text
style={{
color: item.read ? theme.outline : theme.onSurfaceVariant,
fontSize: 12,
}}
numberOfLines={1}
>
{item.chapterName}
</Text>
</View>
<DownloadButton
chapter={item}
deleteChapter={deleteChapter}
downloadChapter={downloadChapter}
downloadQueue={downloadQueue}
showDeleteChapterMenu={showDeleteChapterMenu}
deleteChapterMenu={deleteChapterMenu}
hideDeleteChapterMenu={hideDeleteChapterMenu}
theme={theme}
/>
</View>
</>
</Pressable>
);
};
export default memo(UpdateCard);
const styles = StyleSheet.create({
updateCard: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 16,
},
updateIcon: {
width: 42,
height: 42,
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
chapterDetails: {
flex: 1,
marginLeft: 16,
},
});
+8 -1
View File
@@ -54,7 +54,9 @@
"updates": "Updates",
"updatesScreen": {
"searchbar": "Search updates",
"lastUpdatedAt": "Library last updated:"
"lastUpdatedAt": "Library last updated:",
"newChapters": "new Chapters",
"emptyView":"No recent updates"
},
"history": "History",
"historyScreen": {
@@ -224,5 +226,10 @@
"settings": {
"icognitoMode": "Incognito mode",
"downloadedOnly": "Downloaded only"
},
"downloadScreen": {
"dbInfo": "Downloads are saved in a SQLite Database.",
"downloadChapters": "downloaded Chapters",
"noDownloads": "No downloads"
}
}
+5
View File
@@ -46,6 +46,8 @@ export interface StringMap {
'updates': 'string';
'updatesScreen.searchbar': 'string';
'updatesScreen.lastUpdatedAt': 'string';
'updatesScreen.newChapters': 'string';
'updatesScreen.emptyView': 'string';
'history': 'string';
'historyScreen.searchbar': 'string';
'historyScreen.clearHistorWarning': 'string';
@@ -168,4 +170,7 @@ export interface StringMap {
'categories.defaultCategory': 'string';
'settings.icognitoMode': 'string';
'settings.downloadedOnly': 'string';
'downloadScreen.dbInfo': 'string';
'downloadScreen.downloadChapters': 'string';
'downloadScreen.noDownloads': 'string';
}