Feat: tailing skeletons (#1064)

* tailing loading covers

* fix type issues

* fixed compatibility issues

* show skeleton even if row complete

* cleanup

* replaced !! with Boolean()

* Update type names

Co-authored-by: nyagami <hoangquan05112002@gmail.com>

* updated type occurances

* fixed key issue

* deleted unused vars and comments

---------

Co-authored-by: nyagami <hoangquan05112002@gmail.com>
This commit is contained in:
Patrick Loser
2024-07-05 18:31:53 +02:00
committed by GitHub
parent b67d6f8088
commit 15aed61aa6
7 changed files with 198 additions and 105 deletions
+3 -2
View File
@@ -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;
+115 -70
View File
@@ -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<TNovel extends NovelItem | NovelInfo | LibraryNovelInfo>({
item,
onPress,
libraryStatus,
theme,
isSelected,
onLongPress,
selectedNovelIds,
}: {
type CoverItemLibrary =
| LibraryNovelInfo & {
completeRow?: number;
};
type CoverItemPlugin =
| NovelItem & {
completeRow?: number;
};
interface INovelCover<TNovel> {
item: TNovel;
onPress: () => void;
libraryStatus: boolean;
theme: ThemeColors;
isSelected: boolean;
addSkeletonLoading: boolean;
onLongPress: (item: TNovel) => void;
selectedNovelIds: number[];
}) {
}
function NovelCover<TNovel extends CoverItemLibrary | CoverItemPlugin>({
item,
onPress,
libraryStatus,
theme,
isSelected,
addSkeletonLoading,
onLongPress,
selectedNovelIds,
}: INovelCover<TNovel>) {
const {
displayMode = DisplayModes.Comfortable,
showDownloadBadges = true,
@@ -70,25 +85,35 @@ function NovelCover<TNovel extends NovelItem | NovelInfo | LibraryNovelInfo>({
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 (
<SourceScreenSkeletonLoading
theme={theme}
completeRow={item.completeRow}
/>
);
}
return displayMode !== DisplayModes.List ? (
<View
style={[
{
flex: 1 / numColumns,
borderRadius: 6,
overflow: 'hidden',
margin: 2,
},
styles.standardNovelCover,
isSelected && {
backgroundColor: theme.primary,
opacity: 0.8,
},
backgroundColor: theme.primary,
} &&
styles.selectedNovelCover,
]}
>
<Pressable
@@ -101,34 +126,36 @@ function NovelCover<TNovel extends NovelItem | NovelInfo | LibraryNovelInfo>({
}
onLongPress={selectNovel}
>
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded > 0 ? (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
theme={theme}
/>
) : null}
{showUnreadBadges && item.chaptersUnread > 0 ? (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
) : null}
</View>
{item.id ? (
<View style={styles.badgeContainer}>
{libraryStatus && <InLibraryBadge theme={theme} />}
{showDownloadBadges && item.chaptersDownloaded > 0 ? (
<DownloadBadge
showUnreadBadges={showUnreadBadges}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
theme={theme}
/>
) : null}
{showUnreadBadges && item.chaptersUnread > 0 ? (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
chaptersUnread={item.chaptersUnread}
showDownloadBadges={showDownloadBadges}
/>
) : null}
</View>
) : null}
<Image
source={{ uri, headers: { 'User-Agent': getUserAgent() } }}
style={[
{
height: coverHeight,
borderRadius: 4,
backgroundColor: coverPlaceholderColor,
},
libraryStatus && { opacity: 0.5 },
styles.standardBorderRadius,
libraryStatus && styles.opacityPoint5,
]}
/>
<View style={styles.compactTitleContainer}>
@@ -145,7 +172,7 @@ function NovelCover<TNovel extends NovelItem | NovelInfo | LibraryNovelInfo>({
<ListView
item={item}
downloadBadge={
showDownloadBadges && item.chaptersDownloaded ? (
showDownloadBadges && item.id && item.chaptersDownloaded ? (
<DownloadBadge
theme={theme}
showUnreadBadges={showUnreadBadges}
@@ -155,7 +182,7 @@ function NovelCover<TNovel extends NovelItem | NovelInfo | LibraryNovelInfo>({
) : null
}
unreadBadge={
showUnreadBadges && item.chaptersUnread ? (
showUnreadBadges && item.id && item.chaptersUnread ? (
<UnreadBadge
theme={theme}
chaptersDownloaded={item.chaptersDownloaded}
@@ -188,9 +215,9 @@ const ComfortableTitle = ({
numberOfLines={2}
style={[
styles.title,
styles.padding4,
{
color: theme.onSurface,
padding: 4,
},
]}
>
@@ -204,18 +231,7 @@ const CompactTitle = ({ novelName }: { novelName: string }) => (
colors={['transparent', 'rgba(0,0,0,0.7)']}
style={styles.linearGradient}
>
<Text
numberOfLines={2}
style={[
styles.title,
{
color: 'rgba(255,255,255,1)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
]}
>
<Text numberOfLines={2} style={[styles.title, styles.compactTitle]}>
{novelName}
</Text>
</LinearGradient>
@@ -229,15 +245,27 @@ const InLibraryBadge = ({ theme }: { theme: ThemeColors }) => (
{
backgroundColor: theme.primary,
color: theme.onPrimary,
borderRadius: 4,
},
styles.standardBorderRadius,
]}
>
{getString('novelScreen.inLibaray')}
</Text>
);
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<UnreadBadgeProps> = ({
chaptersDownloaded,
chaptersUnread,
showDownloadBadges,
@@ -246,13 +274,8 @@ const UnreadBadge = ({
<Text
style={[
styles.unreadBadge,
!chaptersDownloaded && {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
!showDownloadBadges && {
borderRadius: 4,
},
!chaptersDownloaded && styles.LeftBorderRadius,
!showDownloadBadges && styles.standardBorderRadius,
{
backgroundColor: theme.primary,
color: theme.onPrimary,
@@ -263,7 +286,7 @@ const UnreadBadge = ({
</Text>
);
const DownloadBadge = ({
const DownloadBadge: React.FC<DownloadBadgeProps> = ({
chaptersDownloaded,
showUnreadBadges,
chaptersUnread,
@@ -272,13 +295,8 @@ const DownloadBadge = ({
<Text
style={[
styles.downloadBadge,
!chaptersUnread && {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
!showUnreadBadges && {
borderRadius: 4,
},
!chaptersUnread && styles.RightBorderRadius,
!showUnreadBadges && styles.standardBorderRadius,
{
backgroundColor: theme.tertiary,
color: theme.onTertiary,
@@ -290,6 +308,19 @@ const DownloadBadge = ({
);
const styles = StyleSheet.create({
LeftBorderRadius: {
borderTopLeftRadius: 4,
borderBottomLeftRadius: 4,
},
RightBorderRadius: {
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
},
standardBorderRadius: {
borderRadius: 4,
},
opacityPoint5: { opacity: 0.5 },
padding4: { padding: 4 },
titleContainer: {
flex: 1,
borderRadius: 4,
@@ -353,4 +384,18 @@ const styles = StyleSheet.create({
left: 10,
flexDirection: 'row',
},
standardNovelCover: {
borderRadius: 6,
overflow: 'hidden',
margin: 2,
},
selectedNovelCover: {
opacity: 0.8,
},
compactTitle: {
color: 'rgba(255,255,255,1)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: -1, height: 1 },
textShadowRadius: 10,
},
});
+38 -8
View File
@@ -8,19 +8,24 @@ import {
ListRenderItem,
} from 'react-native';
import { NovelItem } from '@plugins/types';
import { LibraryNovelInfo, NovelInfo } from '../database/types';
import { LibraryNovelInfo } from '../database/types';
import { useDeviceOrientation } from '@hooks';
export type NovelListRenderItem = ListRenderItem<
LibraryNovelInfo | NovelInfo | NovelItem
>;
export type NovelListRenderItem = ListRenderItem<LibraryNovelInfo | NovelItem>;
const NovelList: React.FC<
FlatListProps<LibraryNovelInfo | NovelInfo | NovelItem>
> = props => {
type listDataItem =
| (LibraryNovelInfo | NovelItem) & {
completeRow?: number;
};
interface NovelListProps extends FlatListProps<LibraryNovelInfo | NovelItem> {
inSource?: boolean;
data: Array<listDataItem>;
}
const NovelList: React.FC<NovelListProps> = 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<listDataItem> = props?.data;
if (props.data?.length && props.inSource) {
let remainder = numColumns - (props.data?.length % numColumns);
let extension: Array<listDataItem> = [];
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 (
<FlatList
contentContainerStyle={[
@@ -47,6 +76,7 @@ const NovelList: React.FC<
key={numColumns}
keyExtractor={(item, index) => index + '_' + item.path}
{...props}
data={extendedNovelList}
/>
);
};
+1
View File
@@ -1,6 +1,7 @@
import { FilterToValues, Filters } from './filterTypes';
export interface NovelItem {
id: undefined;
name: string;
path: string;
cover?: string;
@@ -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) => {
) : (
<NovelList
data={novelList}
inSource
renderItem={({ item }) => {
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) ? (
<SourceScreenSkeletonLoading theme={theme} />
) : undefined
}
onEndReachedThreshold={1.5}
/>
)}
{!showLatestNovels && filterValues && !searchText ? (
<>
<FAB
@@ -100,7 +100,7 @@ const createStyleSheet = (pictureHeight: number, pictureWidth: number) => {
borderRadius: 4,
},
listLoadingContainer: {
marginHorizontal: 12,
marginHorizontal: 8,
marginVertical: 8,
flexDirection: 'row',
alignItems: 'center',
@@ -9,9 +9,13 @@ import { useDeviceOrientation } from '@hooks';
interface Props {
theme: ThemeColors;
completeRow?: number;
}
const SourceScreenSkeletonLoading: React.FC<Props> = ({ theme }) => {
const SourceScreenSkeletonLoading: React.FC<Props> = ({
theme,
completeRow,
}) => {
const [highlightColor, backgroundColor] = getLoadingColors(theme);
const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } =
@@ -31,23 +35,24 @@ const SourceScreenSkeletonLoading: React.FC<Props> = ({ 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 (
<LoadingNovel
key={index}
backgroundColor={backgroundColor}
highlightColor={highlightColor}
pictureHeight={pictureHeight}
pictureWidth={pictureWidth}
displayMode={displayMode}
/>
<View key={'sourceLoading' + item} style={{ flex: 1 / numColumns }}>
<LoadingNovel
backgroundColor={backgroundColor}
highlightColor={highlightColor}
pictureHeight={pictureHeight}
pictureWidth={pictureWidth}
displayMode={displayMode}
/>
</View>
);
};
const renderLoading = (item: number, index: number) => {
const renderLoading = (item: number) => {
const offset = Math.pow(10, item);
const items: Array<number> = [1 * offset];
if (displayMode !== DisplayModes.List) {
@@ -56,12 +61,16 @@ const SourceScreenSkeletonLoading: React.FC<Props> = ({ theme }) => {
}
}
return (
<View key={index} style={styles.row}>
<View key={'sourceSkeletonRow' + item} style={styles.row}>
{items.map(renderLoadingNovel)}
</View>
);
};
let items: Array<number> = [];
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<Props> = ({ theme }) => {
items.push(i);
}
}
return <View style={styles.container}>{items.map(renderLoading)}</View>;
};
@@ -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,
},
});
};