import React, { PropsWithChildren, useCallback, useMemo } from 'react'; import { Platform, RefreshControl, StyleSheet, View } from 'react-native'; import { useGetImage } from '@/utility/JellyfinApi/lib'; import styled, { css } from 'styled-components/native'; import { useNavigation } from '@react-navigation/native'; import { useAppDispatch, useTypedSelector } from '@/store'; import TouchableHandler from '@/components/TouchableHandler'; import useCurrentTrack from '@/utility/useCurrentTrack'; import Play from '@/assets/icons/play.svg'; import Shuffle from '@/assets/icons/shuffle.svg'; import useDefaultStyles from '@/components/Colors'; import usePlayTracks from '@/utility/usePlayTracks'; import { WrappableButtonRow, WrappableButton } from '@/components/WrappableButtonRow'; import { NavigationProp } from '@/screens/types'; import DownloadIcon from '@/components/DownloadIcon'; import CloudDownArrow from '@/assets/icons/cloud-down-arrow.svg'; import Trash from '@/assets/icons/trash.svg'; import { queueTrackForDownload, removeDownloadedTrack } from '@/store/downloads/actions'; import { selectDownloadedTracks } from '@/store/downloads/selectors'; import { Header, SubHeader } from '@/components/Typography'; import { Text } from '@/components/Typography'; import CoverImage from '@/components/CoverImage'; import ticksToDuration from '@/utility/ticksToDuration'; import { t } from '@/localisation'; import { SafeScrollView, useNavigationOffsets } from '@/components/SafeNavigatorView'; import { groupBy } from 'lodash'; import Divider from '@/components/Divider'; const styles = StyleSheet.create({ index: { marginRight: 12, textAlign: 'right', }, activeText: { fontWeight: '500', }, discContainer: { flexDirection: 'row', gap: 24, alignItems: 'center', marginBottom: 12, } }); const AlbumImageContainer = styled.View` margin: 0 12px 24px 12px; flex: 1; align-items: center; `; const TrackContainer = styled.View<{ isPlaying: boolean, small?: boolean }>` padding: 12px 4px; flex-direction: row; border-radius: 6px; align-items: flex-start; ${props => props.isPlaying && css` margin: 0 -12px; padding: 12px 16px; `} ${props => props.small && css` padding: ${Platform.select({ ios: '8px 4px', android: '4px' })}; `} `; export interface TrackListViewProps extends PropsWithChildren<{}> { title?: string; artist?: string; trackIds: string[]; entityId: string; refresh: () => void; playButtonText: string; shuffleButtonText: string; downloadButtonText: string; deleteButtonText: string; listNumberingStyle?: 'album' | 'index'; itemDisplayStyle?: 'album' | 'playlist'; } const TrackListView: React.FC = ({ trackIds, entityId, title, artist, refresh, playButtonText, shuffleButtonText, downloadButtonText, deleteButtonText, listNumberingStyle = 'album', itemDisplayStyle = 'album', children }) => { const defaultStyles = useDefaultStyles(); const offsets = useNavigationOffsets(); // Retrieve state const tracks = useTypedSelector((state) => state.music.tracks.entities); const isLoading = useTypedSelector((state) => state.music.tracks.isLoading); const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds)); const totalDuration = useMemo(() => ( trackIds.reduce((sum, trackId) => ( sum + (tracks[trackId]?.RunTimeTicks || 0) ), 0) ), [trackIds, tracks]); // Split all tracks into trackgroups depending on their parent id (i.e. disc // number). const trackGroups: [string, string[]][] = useMemo(() => { // GUARD: Only apply this rendering style for albums if (listNumberingStyle !== 'album') { return [['0', trackIds]]; } const groups = groupBy(trackIds, (id) => tracks[id]?.ParentIndexNumber); return Object.entries(groups); }, [trackIds, tracks, listNumberingStyle]); // Retrieve helpers const getImage = useGetImage(); const playTracks = usePlayTracks(); const { track: currentTrack } = useCurrentTrack(); const navigation = useNavigation(); const dispatch = useAppDispatch(); // Visual helpers const { indexWidth } = useMemo(() => { // Retrieve the largest index in the current set of tracks const largestIndex = trackIds.reduce((max, trackId, i) => { // Retrieve the index for this trackid, depending on settings const index = listNumberingStyle === 'index' ? i + 1 : tracks[trackId]?.IndexNumber; // Check that the current index is larger than the current max. return index > max ? index : max; }, 0); // Retrieve the number of digits in the largest index const noDigits = largestIndex.toFixed(0).toString().length; // Set a minWidth proportional to the largest amount of digits in an index return StyleSheet.create({ indexWidth: { minWidth: noDigits * 8 } }); }, [trackIds, tracks, listNumberingStyle]); // Setup callbacks const playEntity = useCallback(() => { playTracks(trackIds); }, [playTracks, trackIds]); const shuffleEntity = useCallback(() => { playTracks(trackIds, { shuffle: true }); }, [playTracks, trackIds]); const selectTrack = useCallback(async (index: number) => { await playTracks(trackIds, { playIndex: index }); }, [playTracks, trackIds]); const longPressTrack = useCallback((index: number) => { navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() }); }, [navigation, trackIds]); const downloadAllTracks = useCallback(() => { trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId))); }, [dispatch, trackIds]); const deleteAllTracks = useCallback(() => { downloadedTracks.forEach((trackId) => dispatch(removeDownloadedTrack(trackId))); }, [dispatch, downloadedTracks]); return ( } >
{title}
{artist} {trackGroups.map(([discNo, groupTrackIds]) => ( {trackGroups.length > 1 && ( {t('disc')} {discNo} )} {groupTrackIds.map((trackId, i) => {listNumberingStyle === 'index' ? i + 1 : tracks[trackId]?.IndexNumber} {tracks[trackId]?.Name} {itemDisplayStyle === 'playlist' && ( {tracks[trackId]?.Artists.join(', ')} )} {ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} )} ))} {t('total-duration')}{': '}{ticksToDuration(totalDuration)} {children}
); }; export default TrackListView;