diff --git a/src/api/mutations/favorite/index.ts b/src/api/mutations/favorite/index.ts index 1c572e6a..5e967286 100644 --- a/src/api/mutations/favorite/index.ts +++ b/src/api/mutations/favorite/index.ts @@ -29,11 +29,6 @@ export const useAddFavorite = () => { }) }, onSuccess: (data, { item, onToggle }) => { - Toast.show({ - text1: 'Added favorite', - type: 'success', - }) - trigger('notificationSuccess') if (onToggle) onToggle() @@ -75,11 +70,6 @@ export const useRemoveFavorite = () => { }) }, onSuccess: (data, { item, onToggle }) => { - Toast.show({ - text1: 'Removed favorite', - type: 'success', - }) - trigger('notificationSuccess') if (onToggle) onToggle() diff --git a/src/components/Albums/component.tsx b/src/components/Albums/component.tsx index a1f49d8e..42624e4e 100644 --- a/src/components/Albums/component.tsx +++ b/src/components/Albums/component.tsx @@ -143,10 +143,6 @@ export default function Albums({ onEndReached={onEndReached} ItemSeparatorComponent={ItemSeparatorComponent} refreshControl={refreshControl} - stickyHeaderConfig={{ - // When this is true the flashlist likes to flicker - useNativeDriver: false, - }} stickyHeaderIndices={stickyHeaderIndices} removeClippedSubviews /> diff --git a/src/components/Artist/header.tsx b/src/components/Artist/header.tsx index 183040b8..4add85de 100644 --- a/src/components/Artist/header.tsx +++ b/src/components/Artist/header.tsx @@ -70,26 +70,15 @@ export default function ArtistHeader(): React.JSX.Element { return ( - - + - {!isLightMode && ( - - )} - - - +
diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index 368008f3..89c0533f 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -147,10 +147,6 @@ export default function Artists({ } renderItem={renderItem} stickyHeaderIndices={stickyHeaderIndices} - stickyHeaderConfig={{ - // When this is true the flashlist likes to flicker - useNativeDriver: false, - }} onStartReached={() => { if (artistsInfiniteQuery.hasPreviousPage) artistsInfiniteQuery.fetchPreviousPage() diff --git a/src/components/Global/components/image.tsx b/src/components/Global/components/image.tsx index 749a2310..b0109c70 100644 --- a/src/components/Global/components/image.tsx +++ b/src/components/Global/components/image.tsx @@ -32,11 +32,12 @@ const ItemImage = memo( }: ItemImageProps): React.JSX.Element { const api = useApi() - const imageUrl = getItemImageUrl(api, item, type) + const imageUrl = useMemo(() => getItemImageUrl(api, item, type), [api, item.Id, type]) - return api ? ( + return imageUrl ? ( ) }, - (prevProps, nextProps) => { - return ( - prevProps.item.Id === nextProps.item.Id && - prevProps.type === nextProps.type && - prevProps.cornered === nextProps.cornered && - prevProps.circular === nextProps.circular && - prevProps.width === nextProps.width && - prevProps.height === nextProps.height && - prevProps.testID === nextProps.testID - ) - }, + (prevProps, nextProps) => + prevProps.item.Id === nextProps.item.Id && + prevProps.type === nextProps.type && + prevProps.cornered === nextProps.cornered && + prevProps.circular === nextProps.circular && + prevProps.width === nextProps.width && + prevProps.height === nextProps.height && + prevProps.testID === nextProps.testID, ) interface ItemBlurhashProps { item: BaseItemDto + type: ImageType cornered?: boolean | undefined circular?: boolean | undefined width?: Token | string | number | string | undefined @@ -79,22 +78,27 @@ const Styles = StyleSheet.create({ }, }) -function ItemBlurhash({ item }: ItemBlurhashProps): React.JSX.Element { - const blurhash = getBlurhashFromDto(item) +const ItemBlurhash = memo( + function ItemBlurhash({ item, type }: ItemBlurhashProps): React.JSX.Element { + const blurhash = getBlurhashFromDto(item, type) - return ( - - ) -} + return ( + + ) + }, + (prevProps: ItemBlurhashProps, nextProps: ItemBlurhashProps) => + prevProps.item.Id === nextProps.item.Id && prevProps.type === nextProps.type, +) interface ImageProps { imageUrl: string + type: ImageType item: BaseItemDto cornered?: boolean | undefined circular?: boolean | undefined @@ -103,66 +107,94 @@ interface ImageProps { testID?: string | undefined } -function Image({ - item, - imageUrl, - width, - height, - circular, - cornered, - testID, -}: ImageProps): React.JSX.Element { - const [isLoaded, setIsLoaded] = useState(false) +const Image = memo( + function Image({ + item, + type = ImageType.Primary, + imageUrl, + width, + height, + circular, + cornered, + testID, + }: ImageProps): React.JSX.Element { + const [isLoaded, setIsLoaded] = useState(false) - const handleImageLoad = useCallback(() => setIsLoaded(true), [setIsLoaded]) + const handleImageLoad = useCallback(() => setIsLoaded(true), [setIsLoaded]) - const imageViewStyle = useMemo( - () => - StyleSheet.create({ - view: { - borderRadius: cornered - ? 0 - : width - ? getBorderRadius(circular, width) - : circular - ? getTokenValue('$20') * 10 - : getTokenValue('$5'), - width: !isUndefined(width) - ? typeof width === 'number' - ? width - : typeof width === 'string' && width.includes('%') - ? width - : getTokenValue(width as Token) - : '100%', - height: !isUndefined(height) - ? typeof height === 'number' - ? height - : typeof height === 'string' && height.includes('%') - ? height - : getTokenValue(height as Token) - : '100%', - alignSelf: 'center', - overflow: 'hidden', - }, - }), - [cornered, circular, width, height], - ) + const imageViewStyle = useMemo( + () => getImageStyleSheet(width, height, cornered, circular), + [cornered, circular, width, height], + ) - return ( - - - {!isLoaded && } - - ) + const imageSource = useMemo(() => ({ uri: imageUrl }), [imageUrl]) + + const blurhash = useMemo( + () => (!isLoaded ? : null), + [isLoaded], + ) + + return ( + + + {blurhash} + + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.imageUrl === nextProps.imageUrl && + prevProps.type === nextProps.type && + prevProps.item.Id === nextProps.item.Id && + prevProps.cornered === nextProps.cornered && + prevProps.circular === nextProps.circular && + prevProps.width === nextProps.width && + prevProps.height === nextProps.height && + prevProps.testID === nextProps.testID + ) + }, +) + +function getImageStyleSheet( + width: Token | string | number | string | undefined, + height: Token | string | number | string | undefined, + cornered: boolean | undefined, + circular: boolean | undefined, +) { + return StyleSheet.create({ + view: { + borderRadius: cornered + ? 0 + : width + ? getBorderRadius(circular, width) + : circular + ? getTokenValue('$20') * 10 + : getTokenValue('$5'), + width: !isUndefined(width) + ? typeof width === 'number' + ? width + : typeof width === 'string' && width.includes('%') + ? width + : getTokenValue(width as Token) + : '100%', + height: !isUndefined(height) + ? typeof height === 'number' + ? height + : typeof height === 'string' && height.includes('%') + ? height + : getTokenValue(height as Token) + : '100%', + alignSelf: 'center', + overflow: 'hidden', + }, + }) } /** diff --git a/src/components/Global/components/item-card.tsx b/src/components/Global/components/item-card.tsx index eade8ce1..9c00ea05 100644 --- a/src/components/Global/components/item-card.tsx +++ b/src/components/Global/components/item-card.tsx @@ -33,20 +33,30 @@ function ItemCardComponent({ captionAlign = 'center', ...cardProps }: CardProps) { - if (__DEV__) usePerformanceMonitor('ItemCard', 2) + usePerformanceMonitor('ItemCard', 2) const warmContext = useItemContext() useEffect(() => { if (item.Type === 'Audio') warmContext(item) - }, [item.Type, warmContext]) + }, [item.Id, warmContext]) const hoverStyle = useMemo(() => (onPress ? { scale: 0.925 } : undefined), [onPress]) + const pressStyle = useMemo(() => (onPress ? { scale: 0.875 } : undefined), [onPress]) const handlePressIn = useCallback( () => (item.Type !== 'Audio' ? warmContext(item) : undefined), - [item.Type, warmContext], + [item.Id, warmContext], + ) + + const background = useMemo( + () => ( + + + + ), + [item.Id, squared], ) return ( @@ -66,9 +76,7 @@ function ItemCardComponent({ pressStyle={pressStyle} {...cardProps} > - - - + {background} - - {caption} - - - {subCaption && ( + return ( + - {subCaption} + {caption} - )} - - ) -} + + {subCaption && ( + + {subCaption} + + )} + + ) + }, + (prevProps, nextProps) => + prevProps.size === nextProps.size && + prevProps.captionAlign === nextProps.captionAlign && + prevProps.caption === nextProps.caption && + prevProps.subCaption === nextProps.subCaption, +) export const ItemCard = React.memo( ItemCardComponent, @@ -129,6 +144,6 @@ export const ItemCard = React.memo( a.squared === b.squared && a.size === b.size && a.testId === b.testId && - a.onPress === b.onPress && + !!a.onPress === !!b.onPress && a.captionAlign === b.captionAlign, ) diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx index bff88eca..c9de8d2b 100644 --- a/src/components/Global/components/item-row.tsx +++ b/src/components/Global/components/item-row.tsx @@ -14,9 +14,14 @@ import { useNetworkStatus } from '../../../stores/network' import useStreamingDeviceProfile from '../../../stores/device-profile' import useItemContext from '../../../hooks/use-item-context' import { RouteProp, useRoute } from '@react-navigation/native' -import React, { memo, useCallback, useState } from 'react' +import React, { memo, useCallback, useMemo, useState } from 'react' import { LayoutChangeEvent } from 'react-native' -import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' +import Animated, { + SharedValue, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' import { useSwipeableRowContext } from './swipeable-row-context' import SwipeableRow from './SwipeableRow' import { useSwipeSettingsStore } from '../../../stores/settings/swipe' @@ -46,7 +51,7 @@ interface ItemRowProps { */ const ItemRow = memo( function ItemRow({ item, circular, navigation, onPress }: ItemRowProps): React.JSX.Element { - const [artworkAreaWidth, setArtworkAreaWidth] = useState(0) + const artworkAreaWidth = useSharedValue(0) const api = useApi() @@ -63,7 +68,7 @@ const ItemRow = memo( const warmContext = useItemContext() const { data: isFavorite } = useIsFavorite(item) - const onPressIn = useCallback(() => warmContext(item), [warmContext, item]) + const onPressIn = useCallback(() => warmContext(item), [warmContext, item.Id]) const onLongPress = useCallback( () => @@ -71,7 +76,7 @@ const ItemRow = memo( item, navigation, }), - [navigationRef, navigation, item], + [navigationRef, navigation, item.Id], ) const onPressCallback = useCallback(async () => { @@ -110,14 +115,19 @@ const ItemRow = memo( break } } - }, [loadNewQueue, item, navigation]) + }, [loadNewQueue, item.Id, navigation]) - const renderRunTime = item.Type === BaseItemKind.Audio && !hideRunTimes + const renderRunTime = useMemo( + () => item.Type === BaseItemKind.Audio && !hideRunTimes, + [item.Type, hideRunTimes], + ) - const isAudio = item.Type === 'Audio' + const isAudio = useMemo(() => item.Type === 'Audio', [item.Type]) - const playlistTrackCount = - item.Type === 'Playlist' ? (item.SongCount ?? item.ChildCount ?? 0) : undefined + const playlistTrackCount = useMemo( + () => (item.Type === 'Playlist' ? (item.SongCount ?? item.ChildCount ?? 0) : undefined), + [item.Type, item.SongCount, item.ChildCount], + ) const leftSettings = useSwipeSettingsStore((s) => s.left) const rightSettings = useSwipeSettingsStore((s) => s.right) @@ -148,13 +158,27 @@ const ItemRow = memo( ], ) - const swipeConfig = isAudio - ? buildSwipeConfig({ - left: leftSettings, - right: rightSettings, - handlers: swipeHandlers(), - }) - : {} + const swipeConfig = useMemo( + () => + isAudio + ? buildSwipeConfig({ + left: leftSettings, + right: rightSettings, + handlers: swipeHandlers(), + }) + : {}, + [isAudio, leftSettings, rightSettings, swipeHandlers], + ) + + const handleArtworkLayout = useCallback( + (event: LayoutChangeEvent) => { + const { width } = event.nativeEvent.layout + artworkAreaWidth.value = width + }, + [artworkAreaWidth], + ) + + const pressStyle = useMemo(() => ({ opacity: 0.5 }), []) return ( setArtworkAreaWidth(e.nativeEvent.layout.width)} + onLayout={handleArtworkLayout} /> @@ -209,114 +233,134 @@ const ItemRow = memo( return ( prevProps.item.Id === nextProps.item.Id && prevProps.circular === nextProps.circular && - prevProps.onPress === nextProps.onPress && + !!prevProps.onPress === !!nextProps.onPress && prevProps.navigation === nextProps.navigation ) }, ) -function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element { - const route = useRoute>() +const ItemRowDetails = memo( + function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element { + const route = useRoute>() - const shouldRenderArtistName = - item.Type === 'Audio' || (item.Type === 'MusicAlbum' && route.name !== 'Artist') + const shouldRenderArtistName = + item.Type === 'Audio' || (item.Type === 'MusicAlbum' && route.name !== 'Artist') - const shouldRenderProductionYear = item.Type === 'MusicAlbum' && route.name === 'Artist' + const shouldRenderProductionYear = item.Type === 'MusicAlbum' && route.name === 'Artist' - const shouldRenderGenres = item.Type === 'Playlist' || item.Type === BaseItemKind.MusicArtist + const shouldRenderGenres = + item.Type === 'Playlist' || item.Type === BaseItemKind.MusicArtist - return ( - - - {item.Name ?? ''} - - - {shouldRenderArtistName && ( - - {item.AlbumArtist ?? 'Untitled Artist'} + return ( + + + {item.Name ?? ''} - )} - {shouldRenderProductionYear && ( - + {shouldRenderArtistName && ( - {item.ProductionYear?.toString() ?? 'Unknown Year'} + {item.AlbumArtist ?? 'Untitled Artist'} + )} - + {shouldRenderProductionYear && ( + + + {item.ProductionYear?.toString() ?? 'Unknown Year'} + - {item.RunTimeTicks} - - )} + - {shouldRenderGenres && item.Genres && ( - - {item.Genres?.join(', ') ?? ''} - - )} - - ) -} + {item.RunTimeTicks} + + )} + + {shouldRenderGenres && item.Genres && ( + + {item.Genres?.join(', ') ?? ''} + + )} + + ) + }, + (prevProps, nextProps) => prevProps.item.Id === nextProps.item.Id, +) // Artwork wrapper that fades out when the quick-action menu is open -function HideableArtwork({ - item, - circular, - onLayout, -}: { - item: BaseItemDto - circular?: boolean - onLayout?: (event: LayoutChangeEvent) => void -}): React.JSX.Element { - const { tx } = useSwipeableRowContext() - // Hide artwork as soon as swiping starts (any non-zero tx) - const style = useAnimatedStyle(() => ({ - opacity: tx.value === 0 ? withTiming(1) : 0, - })) - return ( - - - - - - ) -} +const HideableArtwork = memo( + function HideableArtwork({ + item, + circular, + onLayout, + }: { + item: BaseItemDto + circular?: boolean + onLayout?: (event: LayoutChangeEvent) => void + }): React.JSX.Element { + const { tx } = useSwipeableRowContext() + // Hide artwork as soon as swiping starts (any non-zero tx) + const style = useAnimatedStyle(() => ({ + opacity: tx.value === 0 ? withTiming(1) : 0, + })) + return ( + + + + + + ) + }, + (prevProps, nextProps) => + prevProps.item.Id === nextProps.item.Id && + prevProps.circular === nextProps.circular && + !!prevProps.onLayout === !!nextProps.onLayout, +) -function SlidingTextArea({ - leftGapWidth, - children, -}: { - leftGapWidth: number - children: React.ReactNode -}): React.JSX.Element { - const { tx, rightWidth } = useSwipeableRowContext() - const tokenValue = getToken('$2', 'space') - const spacingValue = typeof tokenValue === 'number' ? tokenValue : parseFloat(`${tokenValue}`) - const quickActionBuffer = Number.isFinite(spacingValue) ? spacingValue : 8 - const style = useAnimatedStyle(() => { - const t = tx.value - let offset = 0 - if (t > 0 && leftGapWidth > 0) { - offset = -Math.min(t, leftGapWidth) - } else if (t < 0) { - const rightSpace = Math.max(0, rightWidth) - const compensate = Math.min(-t, rightSpace) - const progress = rightSpace > 0 ? compensate / rightSpace : 1 - offset = compensate * 0.7 + quickActionBuffer * progress - } - return { transform: [{ translateX: offset }] } - }) - const paddingRightValue = Number.isFinite(spacingValue) ? spacingValue : 8 - return ( - - {children} - - ) -} +const SlidingTextArea = memo( + function SlidingTextArea({ + leftGapWidth, + children, + }: { + leftGapWidth: SharedValue + children: React.ReactNode + }): React.JSX.Element { + const { tx, rightWidth } = useSwipeableRowContext() + const tokenValue = getToken('$2', 'space') + const spacingValue = + typeof tokenValue === 'number' ? tokenValue : parseFloat(`${tokenValue}`) + const quickActionBuffer = Number.isFinite(spacingValue) ? spacingValue : 8 + const style = useAnimatedStyle(() => { + const t = tx.value + let offset = 0 + if (t > 0 && leftGapWidth.get() > 0) { + offset = -Math.min(t, leftGapWidth.get()) + } else if (t < 0) { + const rightSpace = Math.max(0, rightWidth) + const compensate = Math.min(-t, rightSpace) + const progress = rightSpace > 0 ? compensate / rightSpace : 1 + offset = compensate * 0.7 + quickActionBuffer * progress + } + return { transform: [{ translateX: offset }] } + }) + const paddingRightValue = Number.isFinite(spacingValue) ? spacingValue : 8 + return ( + + {children} + + ) + }, + (prevProps, nextProps) => + prevProps.leftGapWidth === nextProps.leftGapWidth && + prevProps.children?.valueOf() === nextProps.children?.valueOf(), +) export default ItemRow diff --git a/src/components/Global/components/track.tsx b/src/components/Global/components/track.tsx index 71eddae3..95ab108d 100644 --- a/src/components/Global/components/track.tsx +++ b/src/components/Global/components/track.tsx @@ -1,5 +1,5 @@ -import React, { useMemo, useCallback, useState } from 'react' -import { getToken, Spacer, Theme, useTheme, XStack, YStack } from 'tamagui' +import React, { useMemo, useCallback, useState, memo } from 'react' +import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui' import { Text } from '../helpers/text' import { RunTimeTicks } from '../helpers/time-codes' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' @@ -41,91 +41,102 @@ export interface TrackProps { onLongPress?: () => void | undefined isNested?: boolean | undefined invertedColors?: boolean | undefined - prependElement?: React.JSX.Element | undefined testID?: string | undefined editing?: boolean | undefined } -export default function Track({ - track, - navigation, - tracklist, - index, - queue, - showArtwork, - onPress, - onLongPress, - testID, - isNested, - invertedColors, - prependElement, - editing, -}: TrackProps): React.JSX.Element { - const theme = useTheme() - const [artworkAreaWidth, setArtworkAreaWidth] = useState(0) +const Track = memo( + function Track({ + track, + navigation, + tracklist, + index, + queue, + showArtwork, + onPress, + onLongPress, + testID, + isNested, + invertedColors, + editing, + }: TrackProps): React.JSX.Element { + const theme = useTheme() + const [artworkAreaWidth, setArtworkAreaWidth] = useState(0) - const api = useApi() + const api = useApi() - const deviceProfile = useStreamingDeviceProfile() + const deviceProfile = useStreamingDeviceProfile() - const [hideRunTimes] = useHideRunTimesSetting() + const [hideRunTimes] = useHideRunTimesSetting() - const nowPlaying = useCurrentTrack() - const playQueue = usePlayQueue() - const loadNewQueue = useLoadNewQueue() - const addToQueue = useAddToQueue() - const [networkStatus] = useNetworkStatus() + const nowPlaying = useCurrentTrack() + const playQueue = usePlayQueue() + const loadNewQueue = useLoadNewQueue() + const addToQueue = useAddToQueue() + const [networkStatus] = useNetworkStatus() - const { data: mediaInfo } = useStreamedMediaInfo(track.Id) + const { data: mediaInfo } = useStreamedMediaInfo(track.Id) - const offlineAudio = useDownloadedTrack(track.Id) + const offlineAudio = useDownloadedTrack(track.Id) - const { mutate: addFavorite } = useAddFavorite() - const { mutate: removeFavorite } = useRemoveFavorite() - const { data: isFavoriteTrack } = useIsFavorite(track) - const leftSettings = useSwipeSettingsStore((s) => s.left) - const rightSettings = useSwipeSettingsStore((s) => s.right) + const { mutate: addFavorite } = useAddFavorite() + const { mutate: removeFavorite } = useRemoveFavorite() + const { data: isFavoriteTrack } = useIsFavorite(track) + const leftSettings = useSwipeSettingsStore((s) => s.left) + const rightSettings = useSwipeSettingsStore((s) => s.right) - // Memoize expensive computations - const isPlaying = useMemo( - () => nowPlaying?.item.Id === track.Id, - [nowPlaying?.item.Id, track.Id], - ) + // Memoize expensive computations + const isPlaying = useMemo( + () => nowPlaying?.item.Id === track.Id, + [nowPlaying?.item.Id, track.Id], + ) - const isOffline = useMemo( - () => networkStatus === networkStatusTypes.DISCONNECTED, - [networkStatus], - ) + const isOffline = useMemo( + () => networkStatus === networkStatusTypes.DISCONNECTED, + [networkStatus], + ) - // Memoize tracklist for queue loading - const memoizedTracklist = useMemo( - () => tracklist ?? playQueue?.map((track) => track.item) ?? [], - [tracklist, playQueue], - ) + // Memoize tracklist for queue loading + const memoizedTracklist = useMemo( + () => tracklist ?? playQueue?.map((track) => track.item) ?? [], + [tracklist, playQueue], + ) - // Memoize handlers to prevent recreation - const handlePress = useCallback(async () => { - if (onPress) { - await onPress() - } else { - loadNewQueue({ - api, - deviceProfile, - networkStatus, - track, - index, - tracklist: memoizedTracklist, - queue, - queuingType: QueuingType.FromSelection, - startPlayback: true, - }) - } - }, [onPress, track, index, memoizedTracklist, queue, useLoadNewQueue]) + // Memoize handlers to prevent recreation + const handlePress = useCallback(async () => { + if (onPress) { + await onPress() + } else { + loadNewQueue({ + api, + deviceProfile, + networkStatus, + track, + index, + tracklist: memoizedTracklist, + queue, + queuingType: QueuingType.FromSelection, + startPlayback: true, + }) + } + }, [onPress, track, index, memoizedTracklist, queue, useLoadNewQueue]) - const handleLongPress = useCallback(() => { - if (onLongPress) { - onLongPress() - } else { + const handleLongPress = useCallback(() => { + if (onLongPress) { + onLongPress() + } else { + navigationRef.navigate('Context', { + item: track, + navigation, + streamingMediaSourceInfo: mediaInfo?.MediaSources + ? mediaInfo!.MediaSources![0] + : undefined, + downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo, + }) + } + }, [onLongPress, track, isNested, mediaInfo?.MediaSources, offlineAudio]) + + const handleIconPress = useCallback(() => { navigationRef.navigate('Context', { item: track, navigation, @@ -134,190 +145,192 @@ export default function Track({ : undefined, downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo, }) - } - }, [onLongPress, track, isNested, mediaInfo?.MediaSources, offlineAudio]) + }, [track, isNested, mediaInfo?.MediaSources, offlineAudio]) - const handleIconPress = useCallback(() => { - navigationRef.navigate('Context', { - item: track, - navigation, - streamingMediaSourceInfo: mediaInfo?.MediaSources - ? mediaInfo!.MediaSources![0] - : undefined, - downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo, - }) - }, [track, isNested, mediaInfo?.MediaSources, offlineAudio]) + // Memoize text color to prevent recalculation + const textColor = useMemo(() => { + if (isPlaying) return theme.primary.val + if (isOffline) return offlineAudio ? theme.color : theme.neutral.val + return theme.color + }, [isPlaying, isOffline, offlineAudio, theme.primary.val, theme.color, theme.neutral.val]) - // Memoize text color to prevent recalculation - const textColor = useMemo(() => { - if (isPlaying) return theme.primary.val - if (isOffline) return offlineAudio ? theme.color : theme.neutral.val - return theme.color - }, [isPlaying, isOffline, offlineAudio, theme.primary.val, theme.color, theme.neutral.val]) + // Memoize artists text + const artistsText = useMemo(() => track.Artists?.join(', ') ?? '', [track.Artists]) - // Memoize artists text - const artistsText = useMemo(() => track.Artists?.join(', ') ?? '', [track.Artists]) + // Memoize track name + const trackName = useMemo(() => track.Name ?? 'Untitled Track', [track.Name]) - // Memoize track name - const trackName = useMemo(() => track.Name ?? 'Untitled Track', [track.Name]) + // Memoize index number + const indexNumber = useMemo(() => track.IndexNumber?.toString() ?? '', [track.IndexNumber]) - // Memoize index number - const indexNumber = useMemo(() => track.IndexNumber?.toString() ?? '', [track.IndexNumber]) + // Memoize show artists condition + const shouldShowArtists = useMemo( + () => showArtwork || (track.Artists && track.Artists.length > 1), + [showArtwork, track.Artists], + ) - // Memoize show artists condition - const shouldShowArtists = useMemo( - () => showArtwork || (track.Artists && track.Artists.length > 1), - [showArtwork, track.Artists], - ) + const swipeHandlers = useMemo( + () => ({ + addToQueue: async () => { + console.info('Running add to queue swipe action') + await addToQueue({ + api, + deviceProfile, + networkStatus, + tracks: [track], + queuingType: QueuingType.DirectlyQueued, + }) + }, + toggleFavorite: () => { + console.info( + `Running ${isFavoriteTrack ? 'Remove' : 'Add'} favorite swipe action`, + ) + if (isFavoriteTrack) removeFavorite({ item: track }) + else addFavorite({ item: track }) + }, + addToPlaylist: () => { + console.info('Running add to playlist swipe handler') + navigationRef.dispatch(StackActions.push('AddToPlaylist', { track })) + }, + }), + [ + addToQueue, + api, + deviceProfile, + networkStatus, + track, + addFavorite, + removeFavorite, + isFavoriteTrack, + navigationRef, + ], + ) - const swipeHandlers = useMemo( - () => ({ - addToQueue: async () => { - console.info('Running add to queue swipe action') - await addToQueue({ - api, - deviceProfile, - networkStatus, - tracks: [track], - queuingType: QueuingType.DirectlyQueued, - }) - }, - toggleFavorite: () => { - console.info(`Running ${isFavoriteTrack ? 'Remove' : 'Add'} favorite swipe action`) - if (isFavoriteTrack) removeFavorite({ item: track }) - else addFavorite({ item: track }) - }, - addToPlaylist: () => { - console.info('Running add to playlist swipe handler') - navigationRef.dispatch(StackActions.push('AddToPlaylist', { track })) - }, - }), - [ - addToQueue, - api, - deviceProfile, - networkStatus, - track, - addFavorite, - removeFavorite, - isFavoriteTrack, - navigationRef, - ], - ) + const swipeConfig = useMemo( + () => + buildSwipeConfig({ + left: leftSettings, + right: rightSettings, + handlers: swipeHandlers, + }), + [leftSettings, rightSettings, swipeHandlers], + ) - const swipeConfig = useMemo( - () => - buildSwipeConfig({ left: leftSettings, right: rightSettings, handlers: swipeHandlers }), - [leftSettings, rightSettings, swipeHandlers], - ) + const runtimeComponent = useMemo( + () => + hideRunTimes ? ( + <> + ) : ( + + {track.RunTimeTicks} + + ), + [hideRunTimes, track.RunTimeTicks], + ) - const runtimeComponent = useMemo( - () => - hideRunTimes ? ( - <> - ) : ( - + - {track.RunTimeTicks} - - ), - [hideRunTimes, track.RunTimeTicks], - ) - - return ( - - - - {prependElement ? ( - - {prependElement} - - ) : null} - setArtworkAreaWidth(e.nativeEvent.layout.width)} + alignItems='center' + flex={1} + testID={testID ?? undefined} + paddingVertical={'$2'} + justifyContent='flex-start' + paddingRight={'$2'} + animation={'quick'} + pressStyle={{ opacity: 0.5 }} + backgroundColor={'$background'} > - {showArtwork ? ( - - - - ) : ( - - {indexNumber} - - )} - - - - - - {trackName} - - - {shouldShowArtists && ( + setArtworkAreaWidth(e.nativeEvent.layout.width)} + > + {showArtwork ? ( + + + + ) : ( - {artistsText} + {indexNumber} )} - - + - - - - {runtimeComponent} - {!editing && } + + + + {trackName} + + + {shouldShowArtists && ( + + {artistsText} + + )} + + + + + + + {runtimeComponent} + {!editing && ( + + )} + - - - - ) -} + + + ) + }, + (prevProps, nextProps) => + prevProps.track.Id === nextProps.track.Id && + prevProps.index === nextProps.index && + prevProps.showArtwork === nextProps.showArtwork && + prevProps.isNested === nextProps.isNested && + prevProps.invertedColors === nextProps.invertedColors && + prevProps.testID === nextProps.testID && + prevProps.editing === nextProps.editing && + prevProps.queue === nextProps.queue && + prevProps.tracklist === nextProps.tracklist && + !!prevProps.onPress === !!nextProps.onPress && + !!prevProps.onLongPress === !!nextProps.onLongPress, +) function HideableArtwork({ children }: { children: React.ReactNode }) { const { tx } = useSwipeableRowContext() @@ -351,3 +364,5 @@ function SlidingTextArea({ }) return {children} } + +export default Track diff --git a/src/components/Player/components/header.tsx b/src/components/Player/components/header.tsx index 42a344b3..91d69903 100644 --- a/src/components/Player/components/header.tsx +++ b/src/components/Player/components/header.tsx @@ -12,6 +12,8 @@ import { LayoutChangeEvent } from 'react-native' import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons' import navigationRef from '../../../../navigation' import { useCurrentTrack, useQueueRef } from '../../../stores/player/queue' +import TextTicker from 'react-native-text-ticker' +import { TextTickerConfig } from '../component.config' export default function PlayerHeader(): React.JSX.Element { const queueRef = useQueueRef() @@ -37,17 +39,23 @@ export default function PlayerHeader(): React.JSX.Element { name={'chevron-down'} size={22} onPress={() => navigationRef.goBack()} - style={{ marginVertical: 'auto', width: 22 }} + style={{ + marginVertical: 'auto', + width: 22, + marginRight: 8, + }} /> Playing from - - {playingFrom} - + + + {playingFrom} + + - + diff --git a/src/components/Tracks/component.tsx b/src/components/Tracks/component.tsx index 393bae89..8d9fa8d8 100644 --- a/src/components/Tracks/component.tsx +++ b/src/components/Tracks/component.tsx @@ -1,17 +1,16 @@ import React, { RefObject, useMemo, useRef, useCallback, useEffect } from 'react' import Track from '../Global/components/track' -import { getToken, Separator, useTheme, XStack, YStack } from 'tamagui' +import { Separator, useTheme, XStack, YStack } from 'tamagui' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { Queue } from '../../player/types/queue-item' -import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list' +import { FlashList, FlashListRef } from '@shopify/flash-list' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { BaseStackParamList } from '../../screens/types' import { Text } from '../Global/helpers/text' import AZScroller, { useAlphabetSelector } from '../Global/components/alphabetical-selector' import { UseInfiniteQueryResult } from '@tanstack/react-query' -import { debounce, isString } from 'lodash' +import { isString } from 'lodash' import { RefreshControl } from 'react-native-gesture-handler' -import useItemContext from '../../hooks/use-item-context' import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry' import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header' @@ -32,8 +31,6 @@ export default function Tracks({ }: TracksProps): React.JSX.Element { const theme = useTheme() - const warmContext = useItemContext() - const sectionListRef = useRef>(null) const pendingLetterRef = useRef(null) @@ -80,14 +77,11 @@ export default function Tracks({ index={0} track={track} testID={`track-item-${index}`} - tracklist={tracksToDisplay.slice( - tracksToDisplay.indexOf(track), - tracksToDisplay.indexOf(track) + 50, - )} + tracklist={tracksToDisplay.slice(index, index + 50)} queue={queue} /> ) : null, - [tracksToDisplay, queue], + [tracksToDisplay, queue, navigation, queue], ) const ItemSeparatorComponent = useCallback( @@ -168,10 +162,6 @@ export default function Tracks({ } - stickyHeaderConfig={{ - // When this is true the flashlist likes to flicker - useNativeDriver: false, - }} removeClippedSubviews /> diff --git a/src/providers/Player/index.tsx b/src/providers/Player/index.tsx index b44af31a..f5cac90c 100644 --- a/src/providers/Player/index.tsx +++ b/src/providers/Player/index.tsx @@ -83,13 +83,6 @@ export const PlayerProvider: () => React.JSX.Element = () => { case Event.PlaybackProgressUpdated: { const currentTrack = usePlayerQueueStore.getState().currentTrack - if (currentTrack) - try { - await reportPlaybackProgress(api, currentTrack, event.position) - } catch (error) { - console.error('Unable to report playback progress', error) - } - if (event.position / event.duration > 0.3 && autoDownload && currentTrack) { await saveAudioItem(api, currentTrack.item, downloadingDeviceProfile, true) }