diff --git a/src/screens/anime/airing-schedule/screen.tsx b/src/screens/anime/airing-schedule/screen.tsx index 84fad51..d64130e 100644 --- a/src/screens/anime/airing-schedule/screen.tsx +++ b/src/screens/anime/airing-schedule/screen.tsx @@ -14,6 +14,7 @@ import { useGraphQL } from '@/hooks/use-graphql'; import DetailsCard from '@/screens/search/components/details-card'; import { ActivityIndicator, Button, groupBy, Text, View } from '@/ui'; import Pressable from '@/ui/core/pressable'; +import RefreshControl from '@/ui/core/refresh-control'; import colors from '@/ui/theme/colors'; dayjs.extend(duration); @@ -80,7 +81,12 @@ const AiringScheduleScreen = () => { return days; }, []); - const { data: schedules, isLoading } = useGraphQL(document, { + const { + data: schedules, + isLoading, + isRefetching, + refetch, + } = useGraphQL(document, { airingAt_greater, airingAt_lesser, }); @@ -205,7 +211,7 @@ const AiringScheduleScreen = () => { ItemSeparatorComponent={() => } /> - {isLoading ? ( + {isLoading || isRefetching ? ( { data={Object.entries(schedulesWithTime)?.filter(([, list]) => { return list.length > 0; })} + refreshControl={ + + } renderItem={({ item }) => { const [time, list] = item; diff --git a/src/screens/anime/details/screens/episode-screen/components/episode-container.tsx b/src/screens/anime/details/screens/episode-screen/components/episode-container.tsx index e06c6f6..a1b8ef2 100644 --- a/src/screens/anime/details/screens/episode-screen/components/episode-container.tsx +++ b/src/screens/anime/details/screens/episode-screen/components/episode-container.tsx @@ -1,4 +1,5 @@ import { useAtomValue, useSetAtom } from 'jotai/react'; +import { RotateCwIcon } from 'lucide-react-native'; import React, { useEffect, useMemo } from 'react'; import { Else, If, Then } from 'react-if'; @@ -6,6 +7,7 @@ import { type FragmentType, graphql, useFragment } from '@/gql'; import { getWatchedEpisode } from '@/storage/episode'; import { currentModuleIdAtom } from '@/store'; import { ActivityIndicator, colors, Text, View } from '@/ui'; +import Pressable from '@/ui/core/pressable'; import useEpisodes from '../hooks/use-episodes'; import { episodeChunkAtom, sectionEpisodesAtom } from '../store'; @@ -37,7 +39,7 @@ const EpisodeContainer: React.FC = ({ const media = useFragment(EpisodeContainerFragment, mediaFragment); const currentModuleId = useAtomValue(currentModuleIdAtom); - const { data, isLoading } = useEpisodes(media); + const { data, isLoading, isRefetching, refetch } = useEpisodes(media); const setSectionEpisodes = useSetAtom(sectionEpisodesAtom); const setEpisodeChunk = useSetAtom(episodeChunkAtom); @@ -89,12 +91,20 @@ const EpisodeContainer: React.FC = ({ + + { + refetch(); + }} + > + + {currentModuleId ? ( - + - + diff --git a/src/screens/anime/details/screens/episode-screen/components/episode-layout-container.tsx b/src/screens/anime/details/screens/episode-screen/components/episode-layout-container.tsx index 4a3291f..81abb56 100644 --- a/src/screens/anime/details/screens/episode-screen/components/episode-layout-container.tsx +++ b/src/screens/anime/details/screens/episode-screen/components/episode-layout-container.tsx @@ -1,4 +1,5 @@ import { useNavigation } from '@react-navigation/native'; +import { FlashList } from '@shopify/flash-list'; import { useAtomValue } from 'jotai/react'; import React, { useMemo } from 'react'; @@ -71,8 +72,10 @@ const EpisodeLayoutContainer: React.FC<{ ) : null} {layoutMode === 'details' ? ( - - {episodes.map((episode) => { + { const episodeNumber = parseInt(episode.number, 10); return ( @@ -91,23 +94,25 @@ const EpisodeLayoutContainer: React.FC<{ } /> ); - })} - + }} + keyExtractor={(item) => item.id} + ItemSeparatorComponent={() => } + /> ) : null} {layoutMode === 'card' ? ( - - {episodes.map((episode, index) => { + { const episodeNumber = parseInt(episode.number, 10); return ( { @@ -124,8 +129,17 @@ const EpisodeLayoutContainer: React.FC<{ /> ); - })} - + }} + keyExtractor={(item) => item.id} + ItemSeparatorComponent={() => ( + + )} + estimatedItemSize={130} + /> ) : null} ); diff --git a/src/screens/anime/recently-watched/screen.tsx b/src/screens/anime/recently-watched/screen.tsx index f03f050..8d98b50 100644 --- a/src/screens/anime/recently-watched/screen.tsx +++ b/src/screens/anime/recently-watched/screen.tsx @@ -1,11 +1,13 @@ import { FlashList } from '@shopify/flash-list'; import React from 'react'; +import { Else, If, Then } from 'react-if'; import { useFragment } from '@/gql'; import useScreenSize from '@/hooks/use-screen-size'; import { useWatched } from '@/hooks/use-watched'; import { ActivityIndicator, Text, View } from '@/ui'; import { Card, CardFragment } from '@/ui/card'; +import RefreshControl from '@/ui/core/refresh-control'; import colors from '@/ui/theme/colors'; const SPACE_BETWEEN = 32; @@ -15,62 +17,75 @@ const LIST_PADDING = 16; const RecentlyWatchedScreen = () => { const { width } = useScreenSize(); - const { data, isLoading } = useWatched(0); - - if (isLoading) - return ( - - - - ); - - if (!data?.length) { - return ( - - You haven't watched anything yet? - - ); - } + const { data, isLoading, refetch, isRefetching } = useWatched(0); return ( - - + + Recently watched - { - return ( + + + + + + + + + You haven't watched anything. + + + + - { + return ( + + + + ); + }} + refreshControl={ + + } + estimatedItemSize={294} + keyExtractor={(item) => + // eslint-disable-next-line react-hooks/rules-of-hooks + useFragment(CardFragment, item.media).id.toString() + } /> - ); - }} - estimatedItemSize={294} - keyExtractor={(item) => - // eslint-disable-next-line react-hooks/rules-of-hooks - useFragment(CardFragment, item.media).id.toString() - } - /> + + + ); }; diff --git a/src/screens/search/components/details-list.tsx b/src/screens/search/components/details-list.tsx index 1695861..f761d99 100644 --- a/src/screens/search/components/details-list.tsx +++ b/src/screens/search/components/details-list.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { type FragmentType, useFragment } from '@/gql'; import { View } from '@/ui'; +import RefreshControl from '@/ui/core/refresh-control'; import { DetailsCardFragment } from './details-card'; import DetailsCard from './details-card'; @@ -10,9 +11,16 @@ import DetailsCard from './details-card'; interface DetailsListProps { mediaList: readonly FragmentType[]; onLoadMore: () => void; + refetch: () => void; + isRefetching: boolean; } -const DetailsList: React.FC = ({ mediaList, onLoadMore }) => { +const DetailsList: React.FC = ({ + mediaList, + onLoadMore, + isRefetching, + refetch, +}) => { return ( = ({ mediaList, onLoadMore }) => { return media.id.toString(); }} + refreshControl={ + + } onEndReached={onLoadMore} onEndReachedThreshold={0.05} ListFooterComponentStyle={{ paddingBottom: 300 }} diff --git a/src/screens/search/components/grid-list.tsx b/src/screens/search/components/grid-list.tsx index 1d0968a..29323ce 100644 --- a/src/screens/search/components/grid-list.tsx +++ b/src/screens/search/components/grid-list.tsx @@ -5,10 +5,13 @@ import { type FragmentType, useFragment } from '@/gql'; import useScreenSize from '@/hooks/use-screen-size'; import { View } from '@/ui'; import { Card, CARD_WIDTH, CardFragment } from '@/ui/card'; +import RefreshControl from '@/ui/core/refresh-control'; interface GridListProps { mediaList: readonly FragmentType[]; onLoadMore: () => void; + refetch: () => void; + isRefetching: boolean; } const SPACE_BETWEEN = 32; @@ -17,7 +20,12 @@ const LIST_PADDING = 16; const CARD_HEIGHT = CARD_WIDTH * (3 / 2); const CARD_TITLE_HEIGHT = 60; -const GridList: React.FC = ({ mediaList, onLoadMore }) => { +const GridList: React.FC = ({ + mediaList, + onLoadMore, + isRefetching, + refetch, +}) => { const { width } = useScreenSize(); return ( @@ -44,6 +52,9 @@ const GridList: React.FC = ({ mediaList, onLoadMore }) => { )} + refreshControl={ + + } estimatedItemSize={294} onEndReached={onLoadMore} onEndReachedThreshold={0.05} diff --git a/src/screens/search/components/layout-container.tsx b/src/screens/search/components/layout-container.tsx index 8c222c2..eea0578 100644 --- a/src/screens/search/components/layout-container.tsx +++ b/src/screens/search/components/layout-container.tsx @@ -18,11 +18,15 @@ export const SearchLayoutContainerFragment = graphql(` interface LayoutContainerProps { mediaList: FragmentType[]; onLoadMore: () => void; + refetch: () => void; + isRefetching: boolean; } const LayoutContainer: React.FC = ({ mediaList: mediaListProps, onLoadMore, + isRefetching, + refetch, }) => { const layout = useAtomValue(layoutAtom); @@ -32,10 +36,24 @@ const LayoutContainer: React.FC = ({ ); if (layout === 'grid') { - return ; + return ( + + ); } - return ; + return ( + + ); }; export default LayoutContainer; diff --git a/src/screens/search/screen.tsx b/src/screens/search/screen.tsx index 5c59902..ff0303b 100644 --- a/src/screens/search/screen.tsx +++ b/src/screens/search/screen.tsx @@ -114,8 +114,15 @@ const SearchScreen: React.FC = ({ route: { params } }) => { setYear, ]); - const { data, isLoading, fetchNextPage, isFetchingNextPage, hasNextPage } = - useSearchMedia(); + const { + data, + isLoading, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + refetch, + isRefetching, + } = useSearchMedia(); const handleLoadMore = () => { if (isFetchingNextPage) { @@ -181,10 +188,12 @@ const SearchScreen: React.FC = ({ route: { params } }) => { - + @@ -192,7 +201,7 @@ const SearchScreen: React.FC = ({ route: { params } }) => { - + diff --git a/src/ui/core/refresh-control.tsx b/src/ui/core/refresh-control.tsx new file mode 100644 index 0000000..930883b --- /dev/null +++ b/src/ui/core/refresh-control.tsx @@ -0,0 +1,20 @@ +import { styled } from 'nativewind'; +import React from 'react'; +import type { RefreshControlProps } from 'react-native'; +import { RefreshControl as RNRefreshControl } from 'react-native-gesture-handler'; + +import colors from '../theme/colors'; + +const SRefreshControl = styled(RNRefreshControl); + +const RefreshControl = (props: RefreshControlProps) => { + return ( + + ); +}; + +export default RefreshControl;