diff --git a/src/components/ListView.tsx b/src/components/ListView.tsx index 56986b10..3f006cb7 100644 --- a/src/components/ListView.tsx +++ b/src/components/ListView.tsx @@ -6,10 +6,11 @@ import { coverPlaceholderColor } from '../theme/colors'; import color from 'color'; import { ThemeColors } from '@theme/types'; -import { NovelInfo } from '@database/types'; +import { NovelItem } from '@plugins/types'; +import { LibraryNovelInfo } from '@database/types'; interface ListViewProps { - item: NovelInfo; + item: NovelItem | LibraryNovelInfo; downloadBadge?: React.ReactNode; unreadBadge?: React.ReactNode; inLibraryBadge?: React.ReactNode; diff --git a/src/components/NovelCover.tsx b/src/components/NovelCover.tsx index e4ab132e..8243e6ca 100644 --- a/src/components/NovelCover.tsx +++ b/src/components/NovelCover.tsx @@ -14,12 +14,13 @@ import ListView from './ListView'; import { useDeviceOrientation } from '@hooks'; import { coverPlaceholderColor } from '../theme/colors'; import { DisplayModes } from '@screens/library/constants/constants'; -import { LibraryNovelInfo, NovelInfo } from '@database/types'; +import { LibraryNovelInfo } from '@database/types'; import { NovelItem } from '@plugins/types'; import { ThemeColors } from '@theme/types'; import { useLibrarySettings } from '@hooks/persisted'; import { getUserAgent } from '@hooks/persisted/useUserAgent'; import { getString } from '@strings/translations'; +import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading'; interface UnreadBadgeProps { chaptersDownloaded: number; @@ -35,23 +36,37 @@ interface DownloadBadgeProps { theme: ThemeColors; } -function NovelCover({ - item, - onPress, - libraryStatus, - theme, - isSelected, - onLongPress, - selectedNovelIds, -}: { +type CoverItemLibrary = + | LibraryNovelInfo & { + completeRow?: number; + }; + +type CoverItemPlugin = + | NovelItem & { + completeRow?: number; + }; + +interface INovelCover { item: TNovel; onPress: () => void; libraryStatus: boolean; theme: ThemeColors; isSelected: boolean; + addSkeletonLoading: boolean; onLongPress: (item: TNovel) => void; selectedNovelIds: number[]; -}) { +} + +function NovelCover({ + item, + onPress, + libraryStatus, + theme, + isSelected, + addSkeletonLoading, + onLongPress, + selectedNovelIds, +}: INovelCover) { const { displayMode = DisplayModes.Comfortable, showDownloadBadges = true, @@ -70,25 +85,35 @@ function NovelCover({ const coverHeight = useMemo( () => (window.width / numColumns) * (4 / 3), + // eslint-disable-next-line react-hooks/exhaustive-deps [numColumns], ); const selectNovel = () => onLongPress(item); const uri = item.cover; + if (item.completeRow) { + if (!addSkeletonLoading) { + return <>; + } + return ( + + ); + } return displayMode !== DisplayModes.List ? ( ({ } onLongPress={selectNovel} > - - {libraryStatus && } - {showDownloadBadges && item.chaptersDownloaded > 0 ? ( - - ) : null} - {showUnreadBadges && item.chaptersUnread > 0 ? ( - - ) : null} - + {item.id ? ( + + {libraryStatus && } + {showDownloadBadges && item.chaptersDownloaded > 0 ? ( + + ) : null} + {showUnreadBadges && item.chaptersUnread > 0 ? ( + + ) : null} + + ) : null} @@ -145,7 +172,7 @@ function NovelCover({ ({ ) : null } unreadBadge={ - showUnreadBadges && item.chaptersUnread ? ( + showUnreadBadges && item.id && item.chaptersUnread ? ( @@ -204,18 +231,7 @@ const CompactTitle = ({ novelName }: { novelName: string }) => ( colors={['transparent', 'rgba(0,0,0,0.7)']} style={styles.linearGradient} > - + {novelName} @@ -229,15 +245,27 @@ const InLibraryBadge = ({ theme }: { theme: ThemeColors }) => ( { backgroundColor: theme.primary, color: theme.onPrimary, - borderRadius: 4, }, + styles.standardBorderRadius, ]} > {getString('novelScreen.inLibaray')} ); -const UnreadBadge = ({ +interface BadgeProps { + chaptersDownloaded: number; + chaptersUnread: number; + theme: ThemeColors; +} +interface UnreadBadgeProps extends BadgeProps { + showDownloadBadges: boolean; +} +interface DownloadBadgeProps extends BadgeProps { + showUnreadBadges: boolean; +} + +const UnreadBadge: React.FC = ({ chaptersDownloaded, chaptersUnread, showDownloadBadges, @@ -246,13 +274,8 @@ const UnreadBadge = ({ ); -const DownloadBadge = ({ +const DownloadBadge: React.FC = ({ chaptersDownloaded, showUnreadBadges, chaptersUnread, @@ -272,13 +295,8 @@ const DownloadBadge = ({ ; +export type NovelListRenderItem = ListRenderItem; -const NovelList: React.FC< - FlatListProps -> = props => { +type listDataItem = + | (LibraryNovelInfo | NovelItem) & { + completeRow?: number; + }; + +interface NovelListProps extends FlatListProps { + inSource?: boolean; + data: Array; +} + +const NovelList: React.FC = props => { const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } = useLibrarySettings(); - const orientation = useDeviceOrientation(); const isListView = displayMode === DisplayModes.List; @@ -37,6 +42,30 @@ const NovelList: React.FC< } }, [isListView, orientation, novelsPerRow]); + var extendedNovelList: Array = props?.data; + if (props.data?.length && props.inSource) { + let remainder = numColumns - (props.data?.length % numColumns); + let extension: Array = []; + if (remainder !== 0 && remainder !== numColumns) { + for (let i = 0; i < remainder; i++) { + extension.push({ + cover: '', + name: '', + path: 'loading-' + remainder, + completeRow: 1, + } as listDataItem); + } + } + extension.push({ + cover: '', + name: '', + path: 'loading-' + remainder, + completeRow: 2, + } as listDataItem); + + extendedNovelList = [...props.data, ...extension]; + } + return ( index + '_' + item.path} {...props} + data={extendedNovelList} /> ); }; diff --git a/src/plugins/types/index.ts b/src/plugins/types/index.ts index 8c011925..ff835b82 100644 --- a/src/plugins/types/index.ts +++ b/src/plugins/types/index.ts @@ -1,6 +1,7 @@ import { FilterToValues, Filters } from './filterTypes'; export interface NovelItem { + id: undefined; name: string; path: string; cover?: string; diff --git a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx index 1c631afa..556dc25c 100644 --- a/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx +++ b/src/screens/BrowseSourceScreen/BrowseSourceScreen.tsx @@ -16,7 +16,7 @@ import { getString } from '@strings/translations'; import { StyleSheet } from 'react-native'; import { useLibraryNovels } from '@screens/library/hooks/useLibrary'; import { switchNovelToLibrary } from '@database/queries/NovelQueries'; -import { NovelInfo } from '@database/types'; +import { LibraryNovelInfo, NovelInfo } from '@database/types'; import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { BrowseSourceScreenProps } from '@navigators/types'; @@ -46,7 +46,6 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { clearSearchResults, searchError, } = useSearchSource(pluginId); - const novelList = searchResults.length > 0 ? searchResults : novels; const errorMessage = error || searchError; @@ -76,7 +75,7 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { ); const navigateToNovel = useCallback( - (item: NovelItem) => + (item: NovelItem | LibraryNovelInfo) => navigation.navigate('Novel', { ...item, pluginId: pluginId, @@ -121,6 +120,7 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { ) : ( { const inLibrary = novelInLibrary(item.path); @@ -131,6 +131,10 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { libraryStatus={inLibrary} onPress={() => navigateToNovel(item)} isSelected={false} + addSkeletonLoading={ + (hasNextPage && !searchText) || + (hasNextSearchPage && Boolean(searchText)) + } onLongPress={async () => { await switchNovelToLibrary(item.path, pluginId); setLibrary(prevValues => { @@ -164,15 +168,9 @@ const BrowseSourceScreen = ({ route, navigation }: BrowseSourceScreenProps) => { fetchNextPage(); } }} - ListFooterComponent={ - (hasNextPage && !searchText) || - (hasNextSearchPage && searchText) ? ( - - ) : undefined - } + onEndReachedThreshold={1.5} /> )} - {!showLatestNovels && filterValues && !searchText ? ( <> { borderRadius: 4, }, listLoadingContainer: { - marginHorizontal: 12, + marginHorizontal: 8, marginVertical: 8, flexDirection: 'row', alignItems: 'center', diff --git a/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx b/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx index 18f9349c..6bba1ae9 100644 --- a/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx +++ b/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx @@ -9,9 +9,13 @@ import { useDeviceOrientation } from '@hooks'; interface Props { theme: ThemeColors; + completeRow?: number; } -const SourceScreenSkeletonLoading: React.FC = ({ theme }) => { +const SourceScreenSkeletonLoading: React.FC = ({ + theme, + completeRow, +}) => { const [highlightColor, backgroundColor] = getLoadingColors(theme); const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } = @@ -31,23 +35,24 @@ const SourceScreenSkeletonLoading: React.FC = ({ theme }) => { let height = (window.width / numColumns) * (4 / 3); let width = (window.width - 12 - 9.6 * numColumns) / numColumns; return [height, width]; - }, [numColumns]); + }, [numColumns, window.width]); - const renderLoadingNovel = (item: number, index: number) => { + const renderLoadingNovel = (item: number) => { let randomNumber = Math.random(); randomNumber < 0.1 ? (randomNumber = 0) : null; return ( - + + + ); }; - const renderLoading = (item: number, index: number) => { + const renderLoading = (item: number) => { const offset = Math.pow(10, item); const items: Array = [1 * offset]; if (displayMode !== DisplayModes.List) { @@ -56,12 +61,16 @@ const SourceScreenSkeletonLoading: React.FC = ({ theme }) => { } } return ( - + {items.map(renderLoadingNovel)} ); }; let items: Array = []; + if (completeRow === 1) { + return renderLoadingNovel(completeRow); + } + if (displayMode === DisplayModes.List) { items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; } else { @@ -69,6 +78,7 @@ const SourceScreenSkeletonLoading: React.FC = ({ theme }) => { items.push(i); } } + return {items.map(renderLoading)}; }; @@ -76,15 +86,23 @@ const createStyleSheet = () => { return StyleSheet.create({ container: { flexGrow: 1, - marginHorizontal: 4, + marginHorizontal: 2, marginBottom: 8, marginTop: 2, overflow: 'visible', }, row: { + flexDirection: 'row', + paddingHorizontal: 1, + }, + completeRow: { flexDirection: 'row', justifyContent: 'space-around', paddingHorizontal: 1, + marginBottom: 8, + position: 'relative', + right: 0, + opacity: 0.8, }, }); };