react-native-gesture-handler#FlatList TypeScript Examples

The following examples show how to use react-native-gesture-handler#FlatList. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: Carousel.tsx    From react-native-scroll-bottom-sheet with MIT License 6 votes vote down vote up
Carousel: React.FC<{ index: number }> = React.memo(
  ({ index }) => {
    const renderItem = React.useCallback(() => <CarouselItem />, []);

    return (
      <View style={[styles.row, { borderTopWidth: index === 0 ? 0 : 1 }]}>
        <Text style={styles.title}>{`Popular in ${Faker.address.city()}`}</Text>
        <FlatList
          contentContainerStyle={{ paddingHorizontal: 8 }}
          showsHorizontalScrollIndicator={false}
          initialNumToRender={5}
          data={Array.from({ length: 10 }).map((_, i) => String(i))}
          horizontal
          keyExtractor={j => `row-${index}-item-${j}`}
          renderItem={renderItem}
        />
      </View>
    );
  },
  () => true
)
Example #2
Source File: ChatPage.tsx    From GiveNGo with MIT License 5 votes vote down vote up
export default function ChatPage({ navigation }: any) {
  const renderItemAccessory = () => (
    <Button size="tiny" status="primary">
      View
    </Button>
  );

  const RightActions = () => {
    return (
      <View
        style={{ flex: 1, backgroundColor: 'red', justifyContent: 'center' }}
      >
        <Text
          style={{
            color: 'white',
            paddingHorizontal: 300,
            fontWeight: '600',
          }}
        >
          Delete?
        </Text>
      </View>
    );
  };

  const renderItem = ({ item, index }: any) => (
    <Swipeable renderRightActions={RightActions}>
      <ListItem
        style={styles.items}
        description={`${item.task}`}
        accessoryLeft={ProfileIcon}
        accessoryRight={renderItemAccessory}
        onPress={() => navigation.navigate('Chat Feed')}
      />
    </Swipeable>
  );

  return (
    <React.Fragment>
      <Layout
        style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
        level="3"
      >
        <View>
          <Text style={styles.title} category="h2">
            Messages
          </Text>
        </View>
        <Divider />
        <FlatList
          style={styles.container}
          data={flatListData}
          renderItem={renderItem}
          keyExtractor={(item, index) => index.toString()}
          showsHorizontalScrollIndicator={false}
        />
      </Layout>
    </React.Fragment>
  );
}
Example #3
Source File: Explore.tsx    From online-groceries-app with MIT License 5 votes vote down vote up
ExploreTab = ({navigation}: ExploreTabProps) => {
  const ui_array = [
    {id: 0},
    {id: 1},
    {id: 2},
    {id: 3},
    {id: 4},
    {id: 5},
    {id: 6},
    {id: 7},
  ];

  return (
    <ScrollView style={styles.container}>
      <Header title="Find Products" />
      <View style={styles.searchBarBox}>
        <SearchBar navigation={navigation} navigateTo="" />
      </View>
      <View style={styles.body}>
        <FlatList
          data={ui_array}
          keyExtractor={(item) => item.id}
          scrollEnabled={true}
          numColumns={2}
          renderItem={({item}) => {
            return (
              <CategoryCard
                key={item.id}
                bgColour="#F00"
                borderColour="#0F0"
                title="Teste"
                image={ImageTest}
                onPress={() => null}
              />
            );
          }}
        />
      </View>
      <View style={styles.scrollFooter} />
    </ScrollView>
  );
}
Example #4
Source File: StickyItemFlatList.tsx    From react-native-sticky-item with MIT License 5 votes vote down vote up
AnimatedFlatList = Animated.createAnimatedComponent(FlatList)
Example #5
Source File: CoachingSettings.tsx    From nyxo-app with GNU General Public License v3.0 5 votes vote down vote up
CoachingSettings: FC = () => {
  const {
    data: months,
    isLoading,
    refetch: refetchCoaching
  } = useListCoaching()
  const {
    data: activeMonth,
    refetch: refetchActiveMonth
  } = useGetActiveCoaching()

  const renderItem: ListRenderItem<CoachingPeriod> = ({ item }) => (
    <CoachingMonthCard key={`${item?.id}`} month={item} />
  )

  const data = months?.filter((m) => m?.id !== activeMonth?.id)

  const refresh = () => {
    refetchCoaching()
    refetchActiveMonth()
  }

  return (
    <SafeAreaView>
      <FlatList
        refreshControl={
          <ThemedRefreshControl refreshing={isLoading} onRefresh={refresh} />
        }
        ListHeaderComponent={() => (
          <>
            <GoBackContainer>
              <GoBack route={'Settings'} />
            </GoBackContainer>
            <Container>
              <H2>COACHING.SETTINGS.TITLE</H2>
              <H4>COACHING.SETTINGS.ACTIVE</H4>
            </Container>

            {activeMonth ? (
              <CoachingMonthCard actionsEnabled={false} month={activeMonth} />
            ) : null}
            <Container>
              <H4>COACHING.SETTINGS.ALL</H4>
            </Container>
          </>
        )}
        data={data}
        renderItem={renderItem}
      />
    </SafeAreaView>
  )
}
Example #6
Source File: ListView.tsx    From rn-clean-architecture-template with MIT License 5 votes vote down vote up
ListView: ListViewFC = (props) => {
  const {
    refreshing,
    ListFooterComponent,
    data,
    isLoadingMore,
    LoadingComponent,
  } = props;

  const refreshIndicatorVisible =
    refreshing === true && (data?.length ?? 0) > 0;

  const skeletonDisplayable =
    (refreshing && data?.length === 0) || isLoadingMore;

  const emptyItem = () => {
    if (refreshing) {
      return null;
    }
    return <EmptyListView {...props.emptyListViewProps} />;
  };

  const footer = () => {
    if (skeletonDisplayable) {
      if (LoadingComponent) {
        return LoadingComponent;
      }
      return (
        <>
          <SkeletonLoadingItem />
          <SkeletonLoadingItem />
          <SkeletonLoadingItem />
        </>
      );
    }
    return ListFooterComponent;
  };

  return (
    <View style={[styles.container]}>
      <FlatList
        {...props}
        ListEmptyComponent={emptyItem()}
        ListFooterComponent={footer()}
        refreshControl={
          <RefreshControl
            refreshing={refreshIndicatorVisible}
            onRefresh={props.onRefresh}
          />
        }
        style={styles.list}
      />
    </View>
  );
}
Example #7
Source File: news.tsx    From THUInfo with MIT License 4 votes vote down vote up
NewsScreen = ({navigation}: {navigation: RootNav}) => {
	const [newsList, setNewsList] = useState<NewsSlice[]>([]);
	const [refreshing, setRefreshing] = useState(true);
	const [loading, setLoading] = useState(false);
	const [page, setPage] = useState(1);
	const [inSearchMode, setInSearchMode] = useState(false);
	const [searchKey, setSearchKey] = useState("");
	const [channel, setChannel] = useState<SourceTag | undefined>();
	const [fetchedAll, setFetchedAll] = useState(false);

	const themeName = useColorScheme();
	const theme = themes(themeName);
	const style = styles(themeName);

	const fetchNewsList = (
		request: boolean = true,
		searchMode: boolean | undefined = undefined,
	) => {
		setRefreshing(true);
		setLoading(true);

		if (request) {
			setNewsList([]);
			setPage(1);
			setFetchedAll(false);
			if (searchMode === undefined) {
				setInSearchMode(false);
				setSearchKey("");
			} else {
				setInSearchMode(searchMode);
			}
		} else {
			if (fetchedAll) {
				setRefreshing(false);
				setLoading(false);
				return;
			}
			setPage((p) => p + 1);
		}

		(searchMode === true ||
		(searchMode === undefined && !request && inSearchMode)
			? helper.searchNewsList(request ? 1 : page + 1, searchKey, channel)
			: helper.getNewsList(request ? 1 : page + 1, 30, channel)
		)
			.then((res) => {
				if (res.length === 0) {
					setFetchedAll(true);
				} else {
					setNewsList((o) => o.concat(res));
				}
			})
			.catch(() => {
				Snackbar.show({
					text: getStr("networkRetry"),
					duration: Snackbar.LENGTH_LONG,
				});
			})
			.then(() => {
				setRefreshing(false);
				setLoading(false);
			});
	};

	// eslint-disable-next-line react-hooks/exhaustive-deps
	useEffect(fetchNewsList, [channel]);

	let screenHeight = Dimensions.get("window");
	const flatListRef = React.useRef(null);

	return (
		<View style={{marginHorizontal: 12}}>
			<ScrollView
				style={{margin: 6}}
				showsHorizontalScrollIndicator={false}
				horizontal={true}>
				<ChannelTag
					channel={undefined}
					selected={channel === undefined}
					onPress={() => setChannel(undefined)}
				/>
				{sourceTags.map((tag) => (
					<ChannelTag
						key={tag}
						channel={tag}
						selected={channel === tag}
						onPress={() => setChannel(tag)}
					/>
				))}
			</ScrollView>
			<FlatList
				ref={flatListRef}
				refreshControl={
					<RefreshControl
						refreshing={refreshing}
						onRefresh={fetchNewsList}
						colors={[theme.colors.accent]}
					/>
				}
				ListHeaderComponent={
					<View style={{flexDirection: "row"}}>
						<TextInput
							value={searchKey}
							onChangeText={setSearchKey}
							style={{
								flex: 3,
								marginLeft: 12,
								textAlignVertical: "center",
								fontSize: 15,
								paddingHorizontal: 12,
								backgroundColor: theme.colors.themeBackground,
								color: theme.colors.text,
								borderColor: "#CCC",
								borderWidth: 1,
								borderRadius: 5,
							}}
							placeholder={getStr("searchNewsPrompt")}
						/>
						<SettingsLargeButton
							text={getStr("search")}
							onPress={() => {
								fetchNewsList(true, searchKey !== "");
							}}
							disabled={refreshing || loading}
							redText={false}
						/>
					</View>
				}
				ListEmptyComponent={
					<View
						style={{
							margin: 15,
							height: screenHeight.height * 0.6,
							justifyContent: "center",
							alignItems: "center",
						}}>
						<Text
							style={{
								fontSize: 18,
								fontWeight: "bold",
								alignSelf: "center",
								margin: 5,
								color: theme.colors.text,
							}}>
							{getStr("waitForLoading")}
						</Text>
					</View>
				}
				data={newsList}
				keyExtractor={(item) => item.url}
				renderItem={({item}) => (
					<View style={style.newsSliceContainer}>
						<TouchableOpacity
							onPress={() => navigation.navigate("NewsDetail", {detail: item})}>
							<Text
								numberOfLines={2}
								style={{
									fontSize: 16,
									fontWeight: "bold",
									margin: 5,
									lineHeight: 20,
									color: theme.colors.text,
								}}>
								{item.name.trim()}
							</Text>
							<View
								style={{margin: 5, flexDirection: "row", alignItems: "center"}}>
								{item.source.length > 0 && (
									<>
										<Text
											style={{fontWeight: "bold", color: theme.colors.text}}>
											{item.source}
										</Text>
										<View
											style={{
												marginHorizontal: 6,
												height: 12,
												width: 4,
												borderRadius: 2,
												backgroundColor: theme.colors.accent,
											}}
										/>
									</>
								)}
								<Text style={{fontWeight: "bold", color: theme.colors.text}}>
									{getStr(item.channel)}
								</Text>
							</View>
							<Text style={{color: "gray", margin: 5}}>
								{item.date}
								{item.topped && (
									<Text style={{color: "red"}}>
										{"   "}
										{getStr("topped")}
									</Text>
								)}
							</Text>
						</TouchableOpacity>
					</View>
				)}
				onEndReached={() => fetchNewsList(false)}
				onEndReachedThreshold={0.6}
				ListFooterComponent={
					loading && newsList.length !== 0 ? (
						<View style={style.footerContainer}>
							<ActivityIndicator size="small" />
							<Text style={{margin: 10, color: theme.colors.text}}>
								{getStr("loading")}
							</Text>
						</View>
					) : null
				}
			/>
		</View>
	);
}
Example #8
Source File: Settings.tsx    From BitcoinWalletMobile with MIT License 4 votes vote down vote up
Settings: React.FC<Props> = (props) => {


    const [recoveryWordsModalVisible, setrecoveryWordsModalVisible] = useState(false)
    const [exitModalVisible, setExitModalVisible] = useState(false)

    const languageSelector = (state: WalletState) => state.language
    const language = useSelector(languageSelector)

    const currencySelector = (state: WalletState) => state.currency
    const currency = useSelector(currencySelector)

    const data = [getTranslated(language).currency, getTranslated(language).language, getTranslated(language).seed_phrase, "Support multiple devices", getTranslated(language).exit_wallet]

    const clearAndDelete = async () => {
        setExitModalVisible(false)
        store.dispatch(clearWallet())
    }


    const showRecoveryWordsModal = () => {
        setrecoveryWordsModalVisible(true)
    }

    const hideRecoveryWordsModal = () => {
        setrecoveryWordsModalVisible(false)
    }

    const showExitModal = () => {
        setExitModalVisible(true)
    }

    const hideExitModal = () => {
        setExitModalVisible(false)
    }

    const pushCurrency = () => {
        // @ts-ignore
        props.navigation.navigate('PickerView', { type: "Choose Currency" })
    }

    const pushLanguage = () => {
        // @ts-ignore
        props.navigation.navigate('PickerView', { type: "Choose Language" })
    }

    interface ChildProps {
        item: string
    }

    const conditonalView: React.FC<ChildProps> = ({ item }) => {
        if (item == getTranslated(language).currency) {
            return <View style={{ marginTop: 20 }}><SettingsItem label={getTranslated(language).currency} subLabel={currency} onClick={pushCurrency} /></View>
        }

        if (item == getTranslated(language).seed_phrase) {
            return <View style={{ marginTop: 50 }}><SettingsItem label={getTranslated(language).seed_phrase} subLabel="" onClick={showRecoveryWordsModal} /></View>
        }

        if (item == getTranslated(language).exit_wallet) {
            return <View style={{ marginTop: 50 }}><SettingsItem label={getTranslated(language).exit_wallet} subLabel="" onClick={showExitModal} /></View>
        }

        if (item == "Support multiple devices") {
            return <SettingsItem label="Support multiple devices" subLabel="" onClick={() => { }} />
        }

        else {
            return <SettingsItem label={getTranslated(language).language} subLabel={getLanguageBigName(language)} onClick={pushLanguage} />
        }
    }

    const renderItem: ListRenderItem<string> = ({ item }) => (
        conditonalView({ item: item })
    )

    return (
        <View style={styles.container}>
            <Header screen={getTranslated(language).settings} />
            <Screen>
                <View style={styles.content}>
                    <FlatList data={data}
                        renderItem={renderItem}
                        keyExtractor={item => item} />
                    <RecoveryWordsModal isVisible={recoveryWordsModalVisible} hideModal={hideRecoveryWordsModal} />
                    <ExitWalletModal isVisible={exitModalVisible} hideModal={hideExitModal} deleteCallback={clearAndDelete} />
                </View>
            </Screen>
        </View>
    );
}
Example #9
Source File: StickyItemFlatList.tsx    From react-native-sticky-item with MIT License 4 votes vote down vote up
StickyItemFlatList = forwardRef(
  <T extends {}>(props: StickyItemFlatListProps<T>, ref: Ref<FlatList<T>>) => {
    const {
      initialScrollIndex = 0,
      decelerationRate = DEFAULT_DECELERATION_RATE,
      itemWidth,
      itemHeight,
      separatorSize = DEFAULT_SEPARATOR_SIZE,
      borderRadius = DEFAULT_BORDER_RADIUS,
      stickyItemActiveOpacity = DEFAULT_STICKY_ITEM_ACTIVE_OPACITY,
      stickyItemWidth,
      stickyItemHeight,
      stickyItemBackgroundColors,
      stickyItemContent,
      onStickyItemPress,
      isRTL = DEFAULT_IS_RTL,
      ItemSeparatorComponent = Separator,
      ...rest
    } = props;

    // refs
    const flatListRef = useRef<FlatList<T>>(null);
    const tapRef = useRef<TapGestureHandler>(null);

    //#region variables
    const itemWidthWithSeparator = useMemo(
      () => itemWidth + separatorSize,
      [itemWidth, separatorSize]
    );
    const separatorProps = useMemo(
      () => ({
        size: separatorSize,
      }),
      [separatorSize]
    );
    //#endregion

    //#region styles
    const contentContainerStyle = useMemo(
      () => [
        rest.contentContainerStyle,
        {
          paddingLeft: itemWidth + separatorSize * 2,
          paddingRight: separatorSize,
        },
      ],
      [rest.contentContainerStyle, itemWidth, separatorSize]
    );
    //#endregion

    //#region methods
    const getHitSlop = useCallback(
      isMinimized => {
        const verticalPosition = isMinimized
          ? -((itemHeight - stickyItemHeight) / 2)
          : 0;
        const startPosition = isMinimized ? 0 : -separatorSize;
        const endPosition = isMinimized
          ? -(SCREEN_WIDTH - stickyItemWidth)
          : -(SCREEN_WIDTH - separatorSize - itemWidth);

        return {
          top: verticalPosition,
          right: isRTL ? startPosition : endPosition,
          left: isRTL ? endPosition : startPosition,
          bottom: verticalPosition,
        };
      },
      [
        itemWidth,
        itemHeight,
        stickyItemWidth,
        stickyItemHeight,
        separatorSize,
        isRTL,
      ]
    );
    const getItemLayout = useCallback(
      (_, index) => {
        return {
          length: itemWidthWithSeparator,
          // sticky item + previous items width
          offset: itemWidthWithSeparator + itemWidthWithSeparator * index,
          index,
        };
      },
      [itemWidthWithSeparator]
    );
    //#endregion

    //#region gesture
    const x = useValue(0);
    const tapState = useValue(State.UNDETERMINED);
    const tapGestures = useGestureHandler({ state: tapState });
    const onScroll = event([
      {
        nativeEvent: {
          contentOffset: {
            x,
          },
        },
      },
    ]);
    const onScrollEnd = event([
      {
        nativeEvent: {
          contentOffset: {
            x,
          },
        },
      },
    ]);
    //#endregion

    //#region effects
    //@ts-ignore
    useImperativeHandle(ref, () => flatListRef.current!.getNode());
    useCode(
      () =>
        cond(eq(tapState, State.END), [
          call([tapState], () => {
            if (onStickyItemPress) {
              onStickyItemPress();
            }
          }),
          set(tapState, State.UNDETERMINED),
        ]),
      [tapState]
    );
    useCode(
      () =>
        onChange(
          x,
          call([x], args => {
            if (tapRef.current) {
              const isMinimized = args[0] > 0;
              // @ts-ignore
              tapRef.current.setNativeProps({
                hitSlop: getHitSlop(isMinimized),
              });
            }
          })
        ),
      [
        x,
        itemWidth,
        itemHeight,
        stickyItemWidth,
        stickyItemWidth,
        separatorSize,
      ]
    );
    useEffect(() => {
      /**
       * @DEV
       * to fix stick item position with fast refresh
       */
      x.setValue(0);

      if (tapRef.current) {
        // @ts-ignore
        tapRef.current.setNativeProps({
          hitSlop: getHitSlop(initialScrollIndex !== 0),
        });
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [getHitSlop]);
    //#endregion

    // render
    const renderSeparator = useCallback(() => {
      if (typeof ItemSeparatorComponent === 'function') {
        // @ts-ignore
        return ItemSeparatorComponent(separatorProps);
      } else {
        // @ts-ignore
        return <ItemSeparatorComponent size={separatorProps.size} />;
      }
    }, [ItemSeparatorComponent, separatorProps]);

    return (
      <TapGestureHandler
        ref={tapRef}
        waitFor={flatListRef}
        shouldCancelWhenOutside={true}
        {...tapGestures}
      >
        <Animated.View>
          <AnimatedFlatList
            {...rest}
            ref={flatListRef}
            initialScrollIndex={initialScrollIndex}
            inverted={isRTL}
            ItemSeparatorComponent={renderSeparator}
            contentContainerStyle={contentContainerStyle}
            horizontal={true}
            showsHorizontalScrollIndicator={false}
            scrollEventThrottle={1}
            pagingEnabled={true}
            decelerationRate={decelerationRate}
            snapToAlignment={'start'}
            snapToInterval={itemWidth + separatorSize}
            onScroll={onScroll}
            onScrollAnimationEnd={onScrollEnd}
            getItemLayout={getItemLayout}
          />
          <StickyItem
            x={x}
            tapState={tapState}
            itemWidth={itemWidth}
            itemHeight={itemHeight}
            separatorSize={separatorSize}
            borderRadius={borderRadius}
            stickyItemActiveOpacity={stickyItemActiveOpacity}
            stickyItemWidth={stickyItemWidth}
            stickyItemHeight={stickyItemHeight}
            stickyItemBackgroundColors={stickyItemBackgroundColors}
            stickyItemContent={stickyItemContent}
            isRTL={isRTL}
          />
        </Animated.View>
      </TapGestureHandler>
    );
  }
)
Example #10
Source File: index.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
function Downloads() {
    const defaultStyles = useDefaultStyles();
    const dispatch = useAppDispatch();
    const getImage = useGetImage();

    const { entities, ids } = useTypedSelector((state) => state.downloads);
    const tracks = useTypedSelector((state) => state.music.tracks.entities);

    // Calculate the total download size
    const totalDownloadSize = useMemo(() => (
        ids?.reduce<number>((sum, id) => sum + (entities[id]?.size || 0), 0)
    ), [ids, entities]);

    /**
     * Handlers for actions in this components
     */

    // Delete a single downloaded track
    const handleDelete = useCallback((id: EntityId) => {
        dispatch(removeDownloadedTrack(id));
    }, [dispatch]);

    // Delete all downloaded tracks
    const handleDeleteAllTracks = useCallback(() => ids.forEach(handleDelete), [handleDelete, ids]);

    // Retry a single failed track
    const retryTrack = useCallback((id: EntityId) => {
        dispatch(queueTrackForDownload(id));
    }, [dispatch]);

    // Retry all failed tracks
    const failedIds = useMemo(() => ids.filter((id) => !entities[id]?.isComplete), [ids, entities]);
    const handleRetryFailed = useCallback(() => (
        failedIds.forEach(retryTrack)
    ), [failedIds, retryTrack]);

    /**
     * Render section
     */

    const ListHeaderComponent = useMemo(() => (
        <View style={[{ paddingHorizontal: 20, paddingBottom: 12, borderBottomWidth: 0.5 }, defaultStyles.border]}>
            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
                <Text 
                    style={[
                        defaultStyles.textHalfOpacity,
                        { marginRight: 8, flex: 1, fontSize: 12 },
                    ]}
                    numberOfLines={1}
                >
                    {t('total-download-size')}: {formatBytes(totalDownloadSize)}
                </Text>
                <Button
                    icon={TrashIcon}
                    title={t('delete-all-tracks')}
                    onPress={handleDeleteAllTracks}
                    disabled={!ids.length}
                    size="small"
                />
            </View>
            {failedIds.length > 0 && (
                <Button
                    icon={ArrowClockwise}
                    title={t('retry-failed-downloads')}
                    onPress={handleRetryFailed}
                    disabled={failedIds.length === 0}
                    style={{ marginTop: 4 }}
                />
            )}
        </View>
    ), [totalDownloadSize, defaultStyles, failedIds.length, handleRetryFailed, handleDeleteAllTracks, ids.length]);
    
    const renderItem = useCallback<NonNullable<FlatListProps<EntityId>['renderItem']>>(({ item }) => (
        <DownloadedTrack>
            <View style={{ marginRight: 12 }}>
                <ShadowWrapper size="small">
                    <AlbumImage source={{ uri: getImage(item as string) }} style={defaultStyles.imageBackground} />
                </ShadowWrapper>
            </View>
            <View style={{ flexShrink: 1, marginRight: 8 }}>
                <Text style={[{ fontSize: 16, marginBottom: 4 }, defaultStyles.text]} numberOfLines={1}>
                    {tracks[item]?.Name}
                </Text>
                <Text style={[{ flexShrink: 1, fontSize: 11 }, defaultStyles.textHalfOpacity]} numberOfLines={1}>
                    {tracks[item]?.AlbumArtist} {tracks[item]?.Album ? `— ${tracks[item]?.Album}` : ''}
                </Text>
            </View>
            <View style={{ marginLeft: 'auto', flexDirection: 'row', alignItems: 'center' }}>
                {entities[item]?.isComplete && entities[item]?.size ? (
                    <Text style={[defaultStyles.textQuarterOpacity, { marginRight: 12, fontSize: 12 }]}>
                        {formatBytes(entities[item]?.size || 0)}
                    </Text>
                ) : null}
                <View style={{ marginRight: 12 }}>
                    <DownloadIcon trackId={item} />
                </View>
                <Button onPress={() => handleDelete(item)} size="small" icon={TrashIcon} />
                {!entities[item]?.isComplete && (
                    <Button onPress={() => retryTrack(item)} size="small" icon={ArrowClockwise} style={{ marginLeft: 4 }} />
                )}
            </View>
        </DownloadedTrack>
    ), [entities, retryTrack, handleDelete, defaultStyles, tracks, getImage]);

    // If no tracks have been downloaded, show a short message describing this
    if (!ids.length) {
        return (
            <View style={{ margin: 24, flex: 1, alignItems: 'center', justifyContent: 'center' }}>
                <Text style={[{ textAlign: 'center'}, defaultStyles.textHalfOpacity]}>
                    {t('no-downloads')}
                </Text>
            </View>
        );
    }

    return (
        <SafeAreaView style={{ flex: 1 }}>
            {ListHeaderComponent}
            <FlatList
                data={ids}
                style={{ flex: 1, paddingTop: 12 }}
                contentContainerStyle={{ flexGrow: 1 }}
                renderItem={renderItem}
            />
        </SafeAreaView>
    );
}
Example #11
Source File: index.tsx    From jellyfin-audio-player with MIT License 4 votes vote down vote up
export default function Search() {
    const defaultStyles = useDefaultStyles();

    // Prepare state for fuse and albums
    const [fuseIsReady, setFuseReady] = useState(false);
    const [searchTerm, setSearchTerm] = useState('');
    const [isLoading, setLoading] = useState(false);
    const [fuseResults, setFuseResults] = useState<CombinedResults>([]);
    const [jellyfinResults, setJellyfinResults] = useState<CombinedResults>([]);
    const albums = useTypedSelector(state => state.music.albums.entities);
    const fuse = useRef<Fuse<Album>>();

    // Prepare helpers
    const navigation = useNavigation<MusicNavigationProp>();
    const keyboardHeight = useKeyboardHeight();
    const getImage = useGetImage();
    const dispatch = useAppDispatch();

    /**
     * Since it is impractical to have a global fuse variable, we need to
     * instantiate it for thsi function. With this effect, we generate a new
     * Fuse instance every time the albums change. This can of course be done
     * more intelligently by removing and adding the changed albums, but this is
     * an open todo.
     */
    useEffect(() => {
        fuse.current = new Fuse(Object.values(albums) as Album[], fuseOptions);
        setFuseReady(true);
    }, [albums, setFuseReady]);

    /**
     * This function retrieves search results from Jellyfin. It is a seperate
     * callback, so that we can make sure it is properly debounced and doesn't
     * cause execessive jank in the interface.
     */
    // eslint-disable-next-line react-hooks/exhaustive-deps
    const fetchJellyfinResults = useCallback(debounce(async (searchTerm: string, currentResults: CombinedResults) => {
        // First, query the Jellyfin API
        const { payload } = await dispatch(searchAndFetchAlbums({ term: searchTerm }));

        // Convert the current results to album ids
        const albumIds = currentResults.map(item => item.id);

        // Parse the result in correct typescript form
        const results = (payload as { results: (Album | AlbumTrack)[] }).results;

        // Filter any results that are already displayed
        const items = results.filter(item => (
            !(item.Type === 'MusicAlbum' && albumIds.includes(item.Id))
        // Then convert the results to proper result form
        )).map((item) => ({
            type: item.Type,
            id: item.Id,
            album: item.Type === 'Audio'
                ? item.AlbumId
                : undefined,
            name: item.Type === 'Audio'
                ? item.Name
                : undefined,
        }));

        // Lastly, we'll merge the two and assign them to the state
        setJellyfinResults([...items] as CombinedResults);

        // Loading is now complete
        setLoading(false);
    }, 50), [dispatch, setJellyfinResults]);

    /**
     * Whenever the search term changes, we gather results from Fuse and assign
     * them to state
     */
    useEffect(() => {
        if (!searchTerm) {
            return;
        }

        const retrieveResults = async () => {
            // GUARD: In some extraordinary cases, Fuse might not be presented since
            // it is assigned via refs. In this case, we can't handle any searching.
            if (!fuse.current) {
                return;
            }

            // First set the immediate results from fuse
            const fuseResults = fuse.current.search(searchTerm);
            const albums: AlbumResult[] = fuseResults
                .map(({ item }) => ({ 
                    id: item.Id,
                    type: 'AlbumArtist',
                    album: undefined,
                    name: undefined,
                }));
            
            // Assign the preliminary results
            setFuseResults(albums);
            setLoading(true);
            try {
                // Wrap the call in a try/catch block so that we catch any
                // network issues in search and just use local search if the
                // network is unavailable
                fetchJellyfinResults(searchTerm, albums);
            } catch {
                // Reset the loading indicator if the network fails
                setLoading(false);
            }
        };

        retrieveResults();
    }, [searchTerm, setFuseResults, setLoading, fuse, fetchJellyfinResults]);

    // Handlers
    const selectAlbum = useCallback((id: string) => 
        navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]
    );

    const HeaderComponent = React.useMemo(() => (
        <View>
            <Container style={[
                defaultStyles.border, 
                defaultStyles.view,
                { transform: [{ translateY: keyboardHeight }]},
            ]}>
                <View>
                    <Input
                        value={searchTerm}
                        onChangeText={setSearchTerm}
                        style={[defaultStyles.input, { marginBottom: 12 }]}
                        placeholder={t('search') + '...'}
                        icon
                    />
                    <SearchIndicator width={14} height={14} fill={defaultStyles.textHalfOpacity.color} />
                    {isLoading && <Loading><ActivityIndicator /></Loading>}
                </View>
            </Container>
            {/* <ScrollView horizontal showsHorizontalScrollIndicator={false}>
                <View style={{ paddingHorizontal: 32, paddingBottom: 12, flex: 1, flexDirection: 'row' }}>
                    <SelectableFilter
                        text="Artists"
                        icon={MicrophoneIcon}
                        active
                    />
                    <SelectableFilter
                        text="Albums"
                        icon={AlbumIcon}
                        active={false}
                    />
                    <SelectableFilter
                        text="Tracks"
                        icon={TrackIcon}
                        active={false}
                    />
                    <SelectableFilter
                        text="Playlist"
                        icon={PlaylistIcon}
                        active={false}
                    />
                    <SelectableFilter
                        text="Streaming"
                        icon={StreamIcon}
                        active={false}
                    />
                    <SelectableFilter
                        text="Local Playback"
                        icon={LocalIcon}
                        active={false}
                    />
                </View>
            </ScrollView> */}
        </View>
    ), [searchTerm, setSearchTerm, defaultStyles, isLoading, keyboardHeight]);

    // GUARD: We cannot search for stuff unless Fuse is loaded with results.
    // Therefore we delay rendering to when we are certain it's there.
    if (!fuseIsReady) {
        return null;
    }

    return (
        <SafeAreaView style={{ flex: 1 }}>
            <FlatList
                style={{ flex: 2 }}
                data={[...jellyfinResults, ...fuseResults]}
                renderItem={({ item: { id, type, album: trackAlbum, name: trackName } }: { item: AlbumResult | AudioResult }) => {
                    const album = albums[trackAlbum || id];

                    // GUARD: If the album cannot be found in the store, we
                    // cannot display it.
                    if (!album) {
                        return null;
                    }

                    return (
                        <TouchableHandler<string> id={album.Id} onPress={selectAlbum}>
                            <SearchResult>
                                <ShadowWrapper>
                                    <AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} />
                                </ShadowWrapper>
                                <View style={{ flex: 1 }}>
                                    <Text numberOfLines={1}>
                                        {trackName || album.Name}
                                    </Text>
                                    <HalfOpacity style={defaultStyles.text} numberOfLines={1}>
                                        {type === 'AlbumArtist' 
                                            ? `${t('album')} • ${album.AlbumArtist}`
                                            : `${t('track')} • ${album.AlbumArtist} — ${album.Name}`
                                        }
                                    </HalfOpacity>
                                </View>
                                <View style={{ marginLeft: 16 }}>
                                    <DownloadIcon trackId={id} />
                                </View>
                                <View style={{ marginLeft: 16 }}>
                                    <ChevronRight width={14} height={14} fill={defaultStyles.textQuarterOpacity.color}  />
                                </View>
                            </SearchResult>
                        </TouchableHandler>
                    );
                }}
                keyExtractor={(item) => item.id}
                extraData={[searchTerm, albums]}
            />
            {(searchTerm.length && !jellyfinResults.length && !fuseResults.length && !isLoading) ? (
                <FullSizeContainer>
                    <Text style={{ textAlign: 'center', opacity: 0.5, fontSize: 18 }}>{t('no-results')}</Text> 
                </FullSizeContainer>
            ) : null}
            {HeaderComponent}
        </SafeAreaView>
    );
}