mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-19 20:40:20 -06:00
remove memoization since we're using the compiler (#746)
* remove memoization since we're using the compiler * remove memoization on the track and item rows * remove memoization from artists list * remove additional memoization
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import { Separator, useTheme, XStack, YStack } from 'tamagui'
|
||||
import React, { RefObject, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import React, { RefObject, useEffect, useRef } from 'react'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||
import { UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||
@@ -39,55 +39,52 @@ export default function Albums({
|
||||
const pendingLetterRef = useRef<string | null>(null)
|
||||
|
||||
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
||||
const stickyHeaderIndices = React.useMemo(() => {
|
||||
if (!showAlphabeticalSelector || !albumsInfiniteQuery.data) return []
|
||||
|
||||
return albumsInfiniteQuery.data
|
||||
const stickyHeaderIndices =
|
||||
!showAlphabeticalSelector || !albumsInfiniteQuery.data
|
||||
? []
|
||||
: albumsInfiniteQuery.data
|
||||
.map((album, index) => (typeof album === 'string' ? index : 0))
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
}, [showAlphabeticalSelector, albumsInfiniteQuery.data])
|
||||
|
||||
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
||||
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
||||
|
||||
const refreshControl = useMemo(
|
||||
() => (
|
||||
const refreshControl = (
|
||||
<RefreshControl
|
||||
refreshing={albumsInfiniteQuery.isFetching && !isAlphabetSelectorPending}
|
||||
onRefresh={albumsInfiniteQuery.refetch}
|
||||
tintColor={theme.primary.val}
|
||||
/>
|
||||
),
|
||||
[albumsInfiniteQuery.isFetching, isAlphabetSelectorPending, albumsInfiniteQuery.refetch],
|
||||
)
|
||||
|
||||
const ItemSeparatorComponent = useCallback(
|
||||
({ leadingItem, trailingItem }: { leadingItem: unknown; trailingItem: unknown }) =>
|
||||
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : (
|
||||
<Separator />
|
||||
),
|
||||
[],
|
||||
)
|
||||
const ItemSeparatorComponent = ({
|
||||
leadingItem,
|
||||
trailingItem,
|
||||
}: {
|
||||
leadingItem: unknown
|
||||
trailingItem: unknown
|
||||
}) =>
|
||||
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : <Separator />
|
||||
|
||||
const keyExtractor = useCallback(
|
||||
(item: BaseItemDto | string | number) =>
|
||||
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!,
|
||||
[],
|
||||
)
|
||||
const keyExtractor = (item: BaseItemDto | string | number) =>
|
||||
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ index, item: album }: { index: number; item: BaseItemDto | string | number }) =>
|
||||
const renderItem = ({
|
||||
index,
|
||||
item: album,
|
||||
}: {
|
||||
index: number
|
||||
item: BaseItemDto | string | number
|
||||
}) =>
|
||||
typeof album === 'string' ? (
|
||||
<FlashListStickyHeader text={album.toUpperCase()} />
|
||||
) : typeof album === 'number' ? null : typeof album === 'object' ? (
|
||||
<ItemRow item={album} navigation={navigation} />
|
||||
) : null,
|
||||
[navigation],
|
||||
)
|
||||
) : null
|
||||
|
||||
const onEndReached = useCallback(() => {
|
||||
const onEndReached = () => {
|
||||
if (albumsInfiniteQuery.hasNextPage) albumsInfiniteQuery.fetchNextPage()
|
||||
}, [albumsInfiniteQuery.hasNextPage, albumsInfiniteQuery.fetchNextPage])
|
||||
}
|
||||
|
||||
// Effect for handling the pending alphabet selector letter
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { RefObject, useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { getTokenValue, Separator, useTheme, XStack, YStack } from 'tamagui'
|
||||
import React, { RefObject, useEffect, useRef } from 'react'
|
||||
import { Separator, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
@@ -50,30 +50,32 @@ export default function Artists({
|
||||
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
||||
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
||||
|
||||
const stickyHeaderIndices = useMemo(() => {
|
||||
if (!showAlphabeticalSelector || !artists) return []
|
||||
|
||||
return artists
|
||||
const stickyHeaderIndices =
|
||||
!showAlphabeticalSelector || !artists
|
||||
? []
|
||||
: artists
|
||||
.map((artist, index, artists) => (typeof artist === 'string' ? index : 0))
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
}, [showAlphabeticalSelector, artists])
|
||||
|
||||
const ItemSeparatorComponent = useCallback(
|
||||
({ leadingItem, trailingItem }: { leadingItem: unknown; trailingItem: unknown }) =>
|
||||
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : (
|
||||
<Separator />
|
||||
),
|
||||
[],
|
||||
)
|
||||
const ItemSeparatorComponent = ({
|
||||
leadingItem,
|
||||
trailingItem,
|
||||
}: {
|
||||
leadingItem: unknown
|
||||
trailingItem: unknown
|
||||
}) =>
|
||||
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : <Separator />
|
||||
|
||||
const KeyExtractor = useCallback(
|
||||
(item: BaseItemDto | string | number, index: number) =>
|
||||
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!,
|
||||
[],
|
||||
)
|
||||
const KeyExtractor = (item: BaseItemDto | string | number, index: number) =>
|
||||
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ index, item: artist }: { index: number; item: BaseItemDto | number | string }) =>
|
||||
const renderItem = ({
|
||||
index,
|
||||
item: artist,
|
||||
}: {
|
||||
index: number
|
||||
item: BaseItemDto | number | string
|
||||
}) =>
|
||||
typeof artist === 'string' ? (
|
||||
// Don't render the letter if we don't have any artists that start with it
|
||||
// If the index is the last index, or the next index is not an object, then don't render the letter
|
||||
@@ -82,9 +84,7 @@ export default function Artists({
|
||||
)
|
||||
) : typeof artist === 'number' ? null : typeof artist === 'object' ? (
|
||||
<ItemRow circular item={artist} navigation={navigation} />
|
||||
) : null,
|
||||
[navigation],
|
||||
)
|
||||
) : null
|
||||
|
||||
// Effect for handling the pending alphabet selector letter
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import React, { RefObject, useEffect, useRef, useState } from 'react'
|
||||
import { LayoutChangeEvent, Platform, View as RNView } from 'react-native'
|
||||
import { getToken, Spinner, useTheme, View, YStack } from 'tamagui'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
@@ -61,9 +61,7 @@ export default function AZScroller({
|
||||
})
|
||||
}
|
||||
|
||||
const panGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
const panGesture = Gesture.Pan()
|
||||
.runOnJS(true)
|
||||
.onBegin((e) => {
|
||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||
@@ -99,13 +97,9 @@ export default function AZScroller({
|
||||
} else {
|
||||
scheduleOnRN(hideOverlay)
|
||||
}
|
||||
}),
|
||||
[onLetterSelect],
|
||||
)
|
||||
})
|
||||
|
||||
const tapGesture = useMemo(
|
||||
() =>
|
||||
Gesture.Tap()
|
||||
const tapGesture = Gesture.Tap()
|
||||
.runOnJS(true)
|
||||
.onBegin((e) => {
|
||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||
@@ -130,9 +124,7 @@ export default function AZScroller({
|
||||
} else {
|
||||
scheduleOnRN(hideOverlay)
|
||||
}
|
||||
}),
|
||||
[onLetterSelect],
|
||||
)
|
||||
})
|
||||
|
||||
const gesture = Gesture.Simultaneous(panGesture, tapGesture)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ 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, useMemo, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { LayoutChangeEvent } from 'react-native'
|
||||
import Animated, {
|
||||
SharedValue,
|
||||
@@ -51,14 +51,13 @@ interface ItemRowProps {
|
||||
* @param navigation - The navigation object.
|
||||
* @returns
|
||||
*/
|
||||
const ItemRow = memo(
|
||||
function ItemRow({
|
||||
function ItemRow({
|
||||
item,
|
||||
circular,
|
||||
navigation,
|
||||
onPress,
|
||||
queueName,
|
||||
}: ItemRowProps): React.JSX.Element {
|
||||
}: ItemRowProps): React.JSX.Element {
|
||||
const artworkAreaWidth = useSharedValue(0)
|
||||
|
||||
const api = useApi()
|
||||
@@ -76,18 +75,15 @@ const ItemRow = memo(
|
||||
const warmContext = useItemContext()
|
||||
const { data: isFavorite } = useIsFavorite(item)
|
||||
|
||||
const onPressIn = useCallback(() => warmContext(item), [warmContext, item.Id])
|
||||
const onPressIn = () => warmContext(item)
|
||||
|
||||
const onLongPress = useCallback(
|
||||
() =>
|
||||
const onLongPress = () =>
|
||||
navigationRef.navigate('Context', {
|
||||
item,
|
||||
navigation,
|
||||
}),
|
||||
[navigationRef, navigation, item.Id],
|
||||
)
|
||||
})
|
||||
|
||||
const onPressCallback = useCallback(async () => {
|
||||
const onPressCallback = async () => {
|
||||
if (onPress) await onPress()
|
||||
else
|
||||
switch (item.Type) {
|
||||
@@ -123,25 +119,19 @@ const ItemRow = memo(
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [onPress, loadNewQueue, item.Id, navigation, queueName])
|
||||
}
|
||||
|
||||
const renderRunTime = useMemo(
|
||||
() => item.Type === BaseItemKind.Audio && !hideRunTimes,
|
||||
[item.Type, hideRunTimes],
|
||||
)
|
||||
const renderRunTime = item.Type === BaseItemKind.Audio && !hideRunTimes
|
||||
|
||||
const isAudio = useMemo(() => item.Type === 'Audio', [item.Type])
|
||||
const isAudio = item.Type === 'Audio'
|
||||
|
||||
const playlistTrackCount = useMemo(
|
||||
() => (item.Type === 'Playlist' ? (item.SongCount ?? item.ChildCount ?? 0) : undefined),
|
||||
[item.Type, item.SongCount, item.ChildCount],
|
||||
)
|
||||
const playlistTrackCount =
|
||||
item.Type === 'Playlist' ? (item.SongCount ?? item.ChildCount ?? 0) : undefined
|
||||
|
||||
const leftSettings = useSwipeSettingsStore((s) => s.left)
|
||||
const rightSettings = useSwipeSettingsStore((s) => s.right)
|
||||
|
||||
const swipeHandlers = useCallback(
|
||||
() => ({
|
||||
const swipeHandlers = () => ({
|
||||
addToQueue: async () =>
|
||||
await addToQueue({
|
||||
api,
|
||||
@@ -150,43 +140,26 @@ const ItemRow = memo(
|
||||
tracks: [item],
|
||||
queuingType: QueuingType.DirectlyQueued,
|
||||
}),
|
||||
toggleFavorite: () =>
|
||||
isFavorite ? removeFavorite({ item }) : addFavorite({ item }),
|
||||
toggleFavorite: () => (isFavorite ? removeFavorite({ item }) : addFavorite({ item })),
|
||||
addToPlaylist: () => navigationRef.navigate('AddToPlaylist', { track: item }),
|
||||
}),
|
||||
[
|
||||
addToQueue,
|
||||
api,
|
||||
deviceProfile,
|
||||
networkStatus,
|
||||
item,
|
||||
addFavorite,
|
||||
removeFavorite,
|
||||
isFavorite,
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
const swipeConfig = useMemo(
|
||||
() =>
|
||||
isAudio
|
||||
const swipeConfig = isAudio
|
||||
? buildSwipeConfig({
|
||||
left: leftSettings,
|
||||
right: rightSettings,
|
||||
handlers: swipeHandlers(),
|
||||
})
|
||||
: {},
|
||||
[isAudio, leftSettings, rightSettings, swipeHandlers],
|
||||
)
|
||||
: {}
|
||||
|
||||
const handleArtworkLayout = useCallback(
|
||||
(event: LayoutChangeEvent) => {
|
||||
const handleArtworkLayout = (event: LayoutChangeEvent) => {
|
||||
const { width } = event.nativeEvent.layout
|
||||
artworkAreaWidth.value = width
|
||||
},
|
||||
[artworkAreaWidth],
|
||||
)
|
||||
}
|
||||
|
||||
const pressStyle = useMemo(() => ({ opacity: 0.5 }), [])
|
||||
const pressStyle = {
|
||||
opacity: 0.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<SwipeableRow
|
||||
@@ -210,11 +183,7 @@ const ItemRow = memo(
|
||||
backgroundColor={'$background'}
|
||||
borderRadius={'$2'}
|
||||
>
|
||||
<HideableArtwork
|
||||
item={item}
|
||||
circular={circular}
|
||||
onLayout={handleArtworkLayout}
|
||||
/>
|
||||
<HideableArtwork item={item} circular={circular} onLayout={handleArtworkLayout} />
|
||||
<SlidingTextArea leftGapWidth={artworkAreaWidth}>
|
||||
<ItemRowDetails item={item} />
|
||||
</SlidingTextArea>
|
||||
@@ -236,17 +205,9 @@ const ItemRow = memo(
|
||||
</XStack>
|
||||
</SwipeableRow>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.item.Id === nextProps.item.Id &&
|
||||
prevProps.circular === nextProps.circular &&
|
||||
prevProps.navigation === nextProps.navigation &&
|
||||
prevProps.queueName === nextProps.queueName &&
|
||||
!!prevProps.onPress === !!nextProps.onPress,
|
||||
)
|
||||
}
|
||||
|
||||
const ItemRowDetails = memo(
|
||||
function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const route = useRoute<RouteProp<BaseStackParamList>>()
|
||||
|
||||
const shouldRenderArtistName =
|
||||
@@ -254,8 +215,7 @@ const ItemRowDetails = memo(
|
||||
|
||||
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 (
|
||||
<YStack alignContent='center' justifyContent='center' flexGrow={1}>
|
||||
@@ -271,11 +231,7 @@ const ItemRowDetails = memo(
|
||||
|
||||
{shouldRenderProductionYear && (
|
||||
<XStack gap='$2'>
|
||||
<Text
|
||||
color={'$borderColor'}
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
>
|
||||
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.ProductionYear?.toString() ?? 'Unknown Year'}
|
||||
</Text>
|
||||
|
||||
@@ -292,21 +248,18 @@ const ItemRowDetails = memo(
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.item.Id === nextProps.item.Id,
|
||||
)
|
||||
}
|
||||
|
||||
// Artwork wrapper that fades out when the quick-action menu is open
|
||||
const HideableArtwork = memo(
|
||||
function HideableArtwork({
|
||||
function HideableArtwork({
|
||||
item,
|
||||
circular,
|
||||
onLayout,
|
||||
}: {
|
||||
}: {
|
||||
item: BaseItemDto
|
||||
circular?: boolean
|
||||
onLayout?: (event: LayoutChangeEvent) => void
|
||||
}): React.JSX.Element {
|
||||
}): React.JSX.Element {
|
||||
const { tx } = useSwipeableRowContext()
|
||||
// Hide artwork as soon as swiping starts (any non-zero tx)
|
||||
const style = useAnimatedStyle(() => ({
|
||||
@@ -324,25 +277,18 @@ const HideableArtwork = memo(
|
||||
</XStack>
|
||||
</Animated.View>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.item.Id === nextProps.item.Id &&
|
||||
prevProps.circular === nextProps.circular &&
|
||||
!!prevProps.onLayout === !!nextProps.onLayout,
|
||||
)
|
||||
}
|
||||
|
||||
const SlidingTextArea = memo(
|
||||
function SlidingTextArea({
|
||||
function SlidingTextArea({
|
||||
leftGapWidth,
|
||||
children,
|
||||
}: {
|
||||
}: {
|
||||
leftGapWidth: SharedValue<number>
|
||||
children: React.ReactNode
|
||||
}): React.JSX.Element {
|
||||
}): React.JSX.Element {
|
||||
const { tx, rightWidth } = useSwipeableRowContext()
|
||||
const tokenValue = getToken('$2', 'space')
|
||||
const spacingValue =
|
||||
typeof tokenValue === 'number' ? tokenValue : parseFloat(`${tokenValue}`)
|
||||
const spacingValue = typeof tokenValue === 'number' ? tokenValue : parseFloat(`${tokenValue}`)
|
||||
const quickActionBuffer = Number.isFinite(spacingValue) ? spacingValue : 8
|
||||
const style = useAnimatedStyle(() => {
|
||||
const t = tx.value
|
||||
@@ -363,10 +309,6 @@ const SlidingTextArea = memo(
|
||||
{children}
|
||||
</Animated.View>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) =>
|
||||
prevProps.leftGapWidth === nextProps.leftGapWidth &&
|
||||
prevProps.children?.valueOf() === nextProps.children?.valueOf(),
|
||||
)
|
||||
}
|
||||
|
||||
export default ItemRow
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useCallback, useState, memo } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
@@ -28,10 +28,7 @@ import { useAddFavorite, useRemoveFavorite } from '../../../api/mutations/favori
|
||||
import { StackActions } from '@react-navigation/native'
|
||||
import { useSwipeableRowContext } from './swipeable-row-context'
|
||||
import { useHideRunTimesSetting } from '../../../stores/settings/app'
|
||||
import { queryClient, ONE_HOUR } from '../../../constants/query-client'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media/utils'
|
||||
import MediaInfoQueryKey from '../../../api/queries/media/keys'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import useStreamedMediaInfo from '../../../api/queries/media'
|
||||
|
||||
export interface TrackProps {
|
||||
track: BaseItemDto
|
||||
@@ -48,21 +45,7 @@ export interface TrackProps {
|
||||
editing?: boolean | undefined
|
||||
}
|
||||
|
||||
const queueItemsCache = new WeakMap<JellifyTrack[], BaseItemDto[]>()
|
||||
|
||||
const getQueueItems = (queue: JellifyTrack[] | undefined): BaseItemDto[] => {
|
||||
if (!queue?.length) return []
|
||||
|
||||
const cached = queueItemsCache.get(queue)
|
||||
if (cached) return cached
|
||||
|
||||
const mapped = queue.map((entry) => entry.item)
|
||||
queueItemsCache.set(queue, mapped)
|
||||
return mapped
|
||||
}
|
||||
|
||||
const Track = memo(
|
||||
function Track({
|
||||
export default function Track({
|
||||
track,
|
||||
navigation,
|
||||
tracklist,
|
||||
@@ -75,7 +58,7 @@ const Track = memo(
|
||||
isNested,
|
||||
invertedColors,
|
||||
editing,
|
||||
}: TrackProps): React.JSX.Element {
|
||||
}: TrackProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const [artworkAreaWidth, setArtworkAreaWidth] = useState(0)
|
||||
|
||||
@@ -91,6 +74,8 @@ const Track = memo(
|
||||
const addToQueue = useAddToQueue()
|
||||
const [networkStatus] = useNetworkStatus()
|
||||
|
||||
const { data: mediaInfo } = useStreamedMediaInfo(track.Id)
|
||||
|
||||
const offlineAudio = useDownloadedTrack(track.Id)
|
||||
|
||||
const { mutate: addFavorite } = useAddFavorite()
|
||||
@@ -100,24 +85,15 @@ const Track = memo(
|
||||
const rightSettings = useSwipeSettingsStore((s) => s.right)
|
||||
|
||||
// Memoize expensive computations
|
||||
const isPlaying = useMemo(
|
||||
() => nowPlaying?.item.Id === track.Id,
|
||||
[nowPlaying?.item.Id, track.Id],
|
||||
)
|
||||
const isPlaying = nowPlaying?.item.Id === track.Id
|
||||
|
||||
const isOffline = useMemo(
|
||||
() => networkStatus === networkStatusTypes.DISCONNECTED,
|
||||
[networkStatus],
|
||||
)
|
||||
const isOffline = networkStatus === networkStatusTypes.DISCONNECTED
|
||||
|
||||
// Memoize tracklist for queue loading
|
||||
const memoizedTracklist = useMemo(
|
||||
() => tracklist ?? getQueueItems(playQueue),
|
||||
[tracklist, playQueue],
|
||||
)
|
||||
const memoizedTracklist = tracklist ?? playQueue?.map((track) => track.item) ?? []
|
||||
|
||||
// Memoize handlers to prevent recreation
|
||||
const handlePress = useCallback(async () => {
|
||||
const handlePress = async () => {
|
||||
if (onPress) {
|
||||
await onPress()
|
||||
} else {
|
||||
@@ -133,79 +109,56 @@ const Track = memo(
|
||||
startPlayback: true,
|
||||
})
|
||||
}
|
||||
}, [
|
||||
onPress,
|
||||
api,
|
||||
deviceProfile,
|
||||
networkStatus,
|
||||
track,
|
||||
index,
|
||||
memoizedTracklist,
|
||||
queue,
|
||||
loadNewQueue,
|
||||
])
|
||||
|
||||
const fetchStreamingMediaSourceInfo = useCallback(async () => {
|
||||
if (!api || !deviceProfile || !track.Id) return undefined
|
||||
|
||||
const queryKey = MediaInfoQueryKey({ api, deviceProfile, itemId: track.Id })
|
||||
|
||||
try {
|
||||
const info = await queryClient.ensureQueryData({
|
||||
queryKey,
|
||||
queryFn: () => fetchMediaInfo(api, deviceProfile, track.Id),
|
||||
staleTime: ONE_HOUR,
|
||||
gcTime: ONE_HOUR,
|
||||
})
|
||||
|
||||
return info.MediaSources?.[0]
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch media info for context sheet', error)
|
||||
return undefined
|
||||
}
|
||||
}, [api, deviceProfile, track.Id])
|
||||
|
||||
const openContextSheet = useCallback(async () => {
|
||||
const streamingMediaSourceInfo = await fetchStreamingMediaSourceInfo()
|
||||
|
||||
const handleLongPress = () => {
|
||||
if (onLongPress) {
|
||||
onLongPress()
|
||||
} else {
|
||||
navigationRef.navigate('Context', {
|
||||
item: track,
|
||||
navigation,
|
||||
streamingMediaSourceInfo,
|
||||
streamingMediaSourceInfo: mediaInfo?.MediaSources
|
||||
? mediaInfo!.MediaSources![0]
|
||||
: undefined,
|
||||
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
|
||||
})
|
||||
}, [fetchStreamingMediaSourceInfo, track, navigation, offlineAudio?.mediaSourceInfo])
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
if (onLongPress) {
|
||||
onLongPress()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
void openContextSheet()
|
||||
}, [onLongPress, openContextSheet])
|
||||
const handleIconPress = () => {
|
||||
navigationRef.navigate('Context', {
|
||||
item: track,
|
||||
navigation,
|
||||
streamingMediaSourceInfo: mediaInfo?.MediaSources
|
||||
? mediaInfo!.MediaSources![0]
|
||||
: undefined,
|
||||
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
|
||||
})
|
||||
}
|
||||
|
||||
const handleIconPress = useCallback(() => {
|
||||
void openContextSheet()
|
||||
}, [openContextSheet])
|
||||
// Memoize text color to prevent recalculation
|
||||
const textColor = isPlaying
|
||||
? theme.primary.val
|
||||
: isOffline
|
||||
? offlineAudio
|
||||
? theme.color
|
||||
: theme.neutral.val
|
||||
: theme.color
|
||||
|
||||
// Memoize artists text
|
||||
const artistsText = useMemo(() => track.Artists?.join(', ') ?? '', [track.Artists])
|
||||
const artistsText = track.Artists?.join(', ') ?? ''
|
||||
|
||||
// Memoize track name
|
||||
const trackName = useMemo(() => track.Name ?? 'Untitled Track', [track.Name])
|
||||
const trackName = track.Name ?? 'Untitled Track'
|
||||
|
||||
// Memoize index number
|
||||
const indexNumber = useMemo(() => track.IndexNumber?.toString() ?? '', [track.IndexNumber])
|
||||
const indexNumber = track.IndexNumber?.toString() ?? ''
|
||||
|
||||
// Memoize show artists condition
|
||||
const shouldShowArtists = useMemo(
|
||||
() => showArtwork || (track.Artists && track.Artists.length > 1),
|
||||
[showArtwork, track.Artists],
|
||||
)
|
||||
const shouldShowArtists = showArtwork || (track.Artists && track.Artists.length > 1)
|
||||
|
||||
const swipeHandlers = useMemo(
|
||||
() => ({
|
||||
const swipeHandlers = {
|
||||
addToQueue: async () => {
|
||||
console.info('Running add to queue swipe action')
|
||||
await addToQueue({
|
||||
@@ -217,9 +170,7 @@ const Track = memo(
|
||||
})
|
||||
},
|
||||
toggleFavorite: () => {
|
||||
console.info(
|
||||
`Running ${isFavoriteTrack ? 'Remove' : 'Add'} favorite swipe action`,
|
||||
)
|
||||
console.info(`Running ${isFavoriteTrack ? 'Remove' : 'Add'} favorite swipe action`)
|
||||
if (isFavoriteTrack) removeFavorite({ item: track })
|
||||
else addFavorite({ item: track })
|
||||
},
|
||||
@@ -227,38 +178,15 @@ const Track = memo(
|
||||
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({
|
||||
const swipeConfig = buildSwipeConfig({
|
||||
left: leftSettings,
|
||||
right: rightSettings,
|
||||
handlers: swipeHandlers,
|
||||
}),
|
||||
[leftSettings, rightSettings, swipeHandlers],
|
||||
)
|
||||
})
|
||||
|
||||
const textColor = useMemo(
|
||||
() => (isPlaying ? theme.primary.val : theme.color.val),
|
||||
[isPlaying],
|
||||
)
|
||||
|
||||
const runtimeComponent = useMemo(
|
||||
() =>
|
||||
hideRunTimes ? (
|
||||
const runtimeComponent = hideRunTimes ? (
|
||||
<></>
|
||||
) : (
|
||||
<RunTimeTicks
|
||||
@@ -273,8 +201,6 @@ const Track = memo(
|
||||
>
|
||||
{track.RunTimeTicks}
|
||||
</RunTimeTicks>
|
||||
),
|
||||
[hideRunTimes, track.RunTimeTicks],
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -321,7 +247,7 @@ const Track = memo(
|
||||
</XStack>
|
||||
|
||||
<SlidingTextArea leftGapWidth={artworkAreaWidth} hasArtwork={!!showArtwork}>
|
||||
<YStack alignItems='flex-start' justifyContent='center' flex={6}>
|
||||
<YStack alignItems='flex-start' justifyContent='center' flex={1}>
|
||||
<Text
|
||||
key={`${track.Id}-name`}
|
||||
bold
|
||||
@@ -345,32 +271,17 @@ const Track = memo(
|
||||
</YStack>
|
||||
</SlidingTextArea>
|
||||
|
||||
<XStack justifyContent='flex-end' alignItems='center' flex={2} gap='$1'>
|
||||
<XStack justifyContent='flex-end' alignItems='center' flexShrink={1} gap='$1'>
|
||||
<DownloadedIcon item={track} />
|
||||
<FavoriteIcon item={track} />
|
||||
{runtimeComponent}
|
||||
{!editing && (
|
||||
<Icon name={'dots-horizontal'} onPress={handleIconPress} />
|
||||
)}
|
||||
{!editing && <Icon name={'dots-horizontal'} onPress={handleIconPress} />}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</SwipeableRow>
|
||||
</Theme>
|
||||
)
|
||||
},
|
||||
(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()
|
||||
@@ -402,7 +313,5 @@ function SlidingTextArea({
|
||||
}
|
||||
return { transform: [{ translateX: offset }] }
|
||||
})
|
||||
return <Animated.View style={[{ flex: 5 }, style]}>{children}</Animated.View>
|
||||
return <Animated.View style={[{ flex: 1 }, style]}>{children}</Animated.View>
|
||||
}
|
||||
|
||||
export default Track
|
||||
|
||||
Reference in New Issue
Block a user