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 { 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 { Text } from '../Global/helpers/text'
|
||||||
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
import { FlashList, FlashListRef } from '@shopify/flash-list'
|
||||||
import { UseInfiniteQueryResult } from '@tanstack/react-query'
|
import { UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||||
@@ -39,55 +39,52 @@ export default function Albums({
|
|||||||
const pendingLetterRef = useRef<string | null>(null)
|
const pendingLetterRef = useRef<string | null>(null)
|
||||||
|
|
||||||
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
// Memoize expensive stickyHeaderIndices calculation to prevent unnecessary re-computations
|
||||||
const stickyHeaderIndices = React.useMemo(() => {
|
const stickyHeaderIndices =
|
||||||
if (!showAlphabeticalSelector || !albumsInfiniteQuery.data) return []
|
!showAlphabeticalSelector || !albumsInfiniteQuery.data
|
||||||
|
? []
|
||||||
return albumsInfiniteQuery.data
|
: albumsInfiniteQuery.data
|
||||||
.map((album, index) => (typeof album === 'string' ? index : 0))
|
.map((album, index) => (typeof album === 'string' ? index : 0))
|
||||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||||
}, [showAlphabeticalSelector, albumsInfiniteQuery.data])
|
|
||||||
|
|
||||||
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
||||||
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
||||||
|
|
||||||
const refreshControl = useMemo(
|
const refreshControl = (
|
||||||
() => (
|
<RefreshControl
|
||||||
<RefreshControl
|
refreshing={albumsInfiniteQuery.isFetching && !isAlphabetSelectorPending}
|
||||||
refreshing={albumsInfiniteQuery.isFetching && !isAlphabetSelectorPending}
|
onRefresh={albumsInfiniteQuery.refetch}
|
||||||
onRefresh={albumsInfiniteQuery.refetch}
|
tintColor={theme.primary.val}
|
||||||
tintColor={theme.primary.val}
|
/>
|
||||||
/>
|
|
||||||
),
|
|
||||||
[albumsInfiniteQuery.isFetching, isAlphabetSelectorPending, albumsInfiniteQuery.refetch],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ItemSeparatorComponent = useCallback(
|
const ItemSeparatorComponent = ({
|
||||||
({ leadingItem, trailingItem }: { leadingItem: unknown; trailingItem: unknown }) =>
|
leadingItem,
|
||||||
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : (
|
trailingItem,
|
||||||
<Separator />
|
}: {
|
||||||
),
|
leadingItem: unknown
|
||||||
[],
|
trailingItem: unknown
|
||||||
)
|
}) =>
|
||||||
|
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : <Separator />
|
||||||
|
|
||||||
const keyExtractor = useCallback(
|
const keyExtractor = (item: BaseItemDto | string | number) =>
|
||||||
(item: BaseItemDto | string | number) =>
|
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!
|
||||||
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!,
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = ({
|
||||||
({ index, item: album }: { index: number; item: BaseItemDto | string | number }) =>
|
index,
|
||||||
typeof album === 'string' ? (
|
item: album,
|
||||||
<FlashListStickyHeader text={album.toUpperCase()} />
|
}: {
|
||||||
) : typeof album === 'number' ? null : typeof album === 'object' ? (
|
index: number
|
||||||
<ItemRow item={album} navigation={navigation} />
|
item: BaseItemDto | string | number
|
||||||
) : null,
|
}) =>
|
||||||
[navigation],
|
typeof album === 'string' ? (
|
||||||
)
|
<FlashListStickyHeader text={album.toUpperCase()} />
|
||||||
|
) : typeof album === 'number' ? null : typeof album === 'object' ? (
|
||||||
|
<ItemRow item={album} navigation={navigation} />
|
||||||
|
) : null
|
||||||
|
|
||||||
const onEndReached = useCallback(() => {
|
const onEndReached = () => {
|
||||||
if (albumsInfiniteQuery.hasNextPage) albumsInfiniteQuery.fetchNextPage()
|
if (albumsInfiniteQuery.hasNextPage) albumsInfiniteQuery.fetchNextPage()
|
||||||
}, [albumsInfiniteQuery.hasNextPage, albumsInfiniteQuery.fetchNextPage])
|
}
|
||||||
|
|
||||||
// Effect for handling the pending alphabet selector letter
|
// Effect for handling the pending alphabet selector letter
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { RefObject, useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { RefObject, useEffect, useRef } from 'react'
|
||||||
import { getTokenValue, Separator, useTheme, XStack, YStack } from 'tamagui'
|
import { Separator, useTheme, XStack, YStack } from 'tamagui'
|
||||||
import { Text } from '../Global/helpers/text'
|
import { Text } from '../Global/helpers/text'
|
||||||
import { RefreshControl } from 'react-native'
|
import { RefreshControl } from 'react-native'
|
||||||
import ItemRow from '../Global/components/item-row'
|
import ItemRow from '../Global/components/item-row'
|
||||||
@@ -50,41 +50,41 @@ export default function Artists({
|
|||||||
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
|
||||||
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
|
||||||
|
|
||||||
const stickyHeaderIndices = useMemo(() => {
|
const stickyHeaderIndices =
|
||||||
if (!showAlphabeticalSelector || !artists) return []
|
!showAlphabeticalSelector || !artists
|
||||||
|
? []
|
||||||
|
: artists
|
||||||
|
.map((artist, index, artists) => (typeof artist === 'string' ? index : 0))
|
||||||
|
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||||
|
|
||||||
return artists
|
const ItemSeparatorComponent = ({
|
||||||
.map((artist, index, artists) => (typeof artist === 'string' ? index : 0))
|
leadingItem,
|
||||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
trailingItem,
|
||||||
}, [showAlphabeticalSelector, artists])
|
}: {
|
||||||
|
leadingItem: unknown
|
||||||
|
trailingItem: unknown
|
||||||
|
}) =>
|
||||||
|
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : <Separator />
|
||||||
|
|
||||||
const ItemSeparatorComponent = useCallback(
|
const KeyExtractor = (item: BaseItemDto | string | number, index: number) =>
|
||||||
({ leadingItem, trailingItem }: { leadingItem: unknown; trailingItem: unknown }) =>
|
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!
|
||||||
typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : (
|
|
||||||
<Separator />
|
|
||||||
),
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const KeyExtractor = useCallback(
|
const renderItem = ({
|
||||||
(item: BaseItemDto | string | number, index: number) =>
|
index,
|
||||||
typeof item === 'string' ? item : typeof item === 'number' ? item.toString() : item.Id!,
|
item: artist,
|
||||||
[],
|
}: {
|
||||||
)
|
index: number
|
||||||
|
item: BaseItemDto | number | string
|
||||||
const renderItem = useCallback(
|
}) =>
|
||||||
({ index, item: artist }: { index: number; item: BaseItemDto | number | string }) =>
|
typeof artist === 'string' ? (
|
||||||
typeof artist === 'string' ? (
|
// Don't render the letter if we don't have any artists that start with it
|
||||||
// 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
|
||||||
// If the index is the last index, or the next index is not an object, then don't render the letter
|
index - 1 === artists.length || typeof artists[index + 1] !== 'object' ? null : (
|
||||||
index - 1 === artists.length || typeof artists[index + 1] !== 'object' ? null : (
|
<FlashListStickyHeader text={artist.toUpperCase()} />
|
||||||
<FlashListStickyHeader text={artist.toUpperCase()} />
|
)
|
||||||
)
|
) : typeof artist === 'number' ? null : typeof artist === 'object' ? (
|
||||||
) : typeof artist === 'number' ? null : typeof artist === 'object' ? (
|
<ItemRow circular item={artist} navigation={navigation} />
|
||||||
<ItemRow circular item={artist} navigation={navigation} />
|
) : null
|
||||||
) : null,
|
|
||||||
[navigation],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Effect for handling the pending alphabet selector letter
|
// Effect for handling the pending alphabet selector letter
|
||||||
useEffect(() => {
|
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 { LayoutChangeEvent, Platform, View as RNView } from 'react-native'
|
||||||
import { getToken, Spinner, useTheme, View, YStack } from 'tamagui'
|
import { getToken, Spinner, useTheme, View, YStack } from 'tamagui'
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||||
@@ -61,78 +61,70 @@ export default function AZScroller({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const panGesture = useMemo(
|
const panGesture = Gesture.Pan()
|
||||||
() =>
|
.runOnJS(true)
|
||||||
Gesture.Pan()
|
.onBegin((e) => {
|
||||||
.runOnJS(true)
|
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||||
.onBegin((e) => {
|
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
|
||||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
const index = Math.floor(relativeY / letterHeight.current)
|
||||||
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
|
if (alphabet[index]) {
|
||||||
const index = Math.floor(relativeY / letterHeight.current)
|
const letter = alphabet[index]
|
||||||
if (alphabet[index]) {
|
selectedLetter.value = letter
|
||||||
const letter = alphabet[index]
|
setOverlayLetter(letter)
|
||||||
selectedLetter.value = letter
|
scheduleOnRN(showOverlay)
|
||||||
setOverlayLetter(letter)
|
}
|
||||||
scheduleOnRN(showOverlay)
|
})
|
||||||
}
|
.onUpdate((e) => {
|
||||||
})
|
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||||
.onUpdate((e) => {
|
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
|
||||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
const index = Math.floor(relativeY / letterHeight.current)
|
||||||
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
|
if (alphabet[index]) {
|
||||||
const index = Math.floor(relativeY / letterHeight.current)
|
const letter = alphabet[index]
|
||||||
if (alphabet[index]) {
|
selectedLetter.value = letter
|
||||||
const letter = alphabet[index]
|
setOverlayLetter(letter)
|
||||||
selectedLetter.value = letter
|
scheduleOnRN(showOverlay)
|
||||||
setOverlayLetter(letter)
|
}
|
||||||
scheduleOnRN(showOverlay)
|
})
|
||||||
}
|
.onEnd(() => {
|
||||||
})
|
if (selectedLetter.value) {
|
||||||
.onEnd(() => {
|
scheduleOnRN(async () => {
|
||||||
if (selectedLetter.value) {
|
setOperationPending(true)
|
||||||
scheduleOnRN(async () => {
|
onLetterSelect(selectedLetter.value.toLowerCase()).then(() => {
|
||||||
setOperationPending(true)
|
|
||||||
onLetterSelect(selectedLetter.value.toLowerCase()).then(() => {
|
|
||||||
scheduleOnRN(hideOverlay)
|
|
||||||
setOperationPending(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
scheduleOnRN(hideOverlay)
|
scheduleOnRN(hideOverlay)
|
||||||
}
|
setOperationPending(false)
|
||||||
}),
|
})
|
||||||
[onLetterSelect],
|
})
|
||||||
)
|
} else {
|
||||||
|
scheduleOnRN(hideOverlay)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const tapGesture = useMemo(
|
const tapGesture = Gesture.Tap()
|
||||||
() =>
|
.runOnJS(true)
|
||||||
Gesture.Tap()
|
.onBegin((e) => {
|
||||||
.runOnJS(true)
|
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
||||||
.onBegin((e) => {
|
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
|
||||||
const relativeY = e.absoluteY - alphabetSelectorTopY.current
|
const index = Math.floor(relativeY / letterHeight.current)
|
||||||
setOverlayPositionY(relativeY - letterHeight.current * 1.5)
|
if (alphabet[index]) {
|
||||||
const index = Math.floor(relativeY / letterHeight.current)
|
const letter = alphabet[index]
|
||||||
if (alphabet[index]) {
|
selectedLetter.value = letter
|
||||||
const letter = alphabet[index]
|
setOverlayLetter(letter)
|
||||||
selectedLetter.value = letter
|
scheduleOnRN(showOverlay)
|
||||||
setOverlayLetter(letter)
|
}
|
||||||
scheduleOnRN(showOverlay)
|
})
|
||||||
}
|
.onEnd(() => {
|
||||||
})
|
if (selectedLetter.value) {
|
||||||
.onEnd(() => {
|
scheduleOnRN(async () => {
|
||||||
if (selectedLetter.value) {
|
setOperationPending(true)
|
||||||
scheduleOnRN(async () => {
|
onLetterSelect(selectedLetter.value.toLowerCase()).then(() => {
|
||||||
setOperationPending(true)
|
|
||||||
onLetterSelect(selectedLetter.value.toLowerCase()).then(() => {
|
|
||||||
scheduleOnRN(hideOverlay)
|
|
||||||
setOperationPending(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
scheduleOnRN(hideOverlay)
|
scheduleOnRN(hideOverlay)
|
||||||
}
|
setOperationPending(false)
|
||||||
}),
|
})
|
||||||
[onLetterSelect],
|
})
|
||||||
)
|
} else {
|
||||||
|
scheduleOnRN(hideOverlay)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const gesture = Gesture.Simultaneous(panGesture, tapGesture)
|
const gesture = Gesture.Simultaneous(panGesture, tapGesture)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useNetworkStatus } from '../../../stores/network'
|
|||||||
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
||||||
import useItemContext from '../../../hooks/use-item-context'
|
import useItemContext from '../../../hooks/use-item-context'
|
||||||
import { RouteProp, useRoute } from '@react-navigation/native'
|
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 { LayoutChangeEvent } from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
SharedValue,
|
SharedValue,
|
||||||
@@ -51,322 +51,264 @@ interface ItemRowProps {
|
|||||||
* @param navigation - The navigation object.
|
* @param navigation - The navigation object.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const ItemRow = memo(
|
function ItemRow({
|
||||||
function ItemRow({
|
item,
|
||||||
item,
|
circular,
|
||||||
circular,
|
navigation,
|
||||||
navigation,
|
onPress,
|
||||||
onPress,
|
queueName,
|
||||||
queueName,
|
}: ItemRowProps): React.JSX.Element {
|
||||||
}: ItemRowProps): React.JSX.Element {
|
const artworkAreaWidth = useSharedValue(0)
|
||||||
const artworkAreaWidth = useSharedValue(0)
|
|
||||||
|
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
const [networkStatus] = useNetworkStatus()
|
const [networkStatus] = useNetworkStatus()
|
||||||
|
|
||||||
const deviceProfile = useStreamingDeviceProfile()
|
const deviceProfile = useStreamingDeviceProfile()
|
||||||
|
|
||||||
const loadNewQueue = useLoadNewQueue()
|
const loadNewQueue = useLoadNewQueue()
|
||||||
const addToQueue = useAddToQueue()
|
const addToQueue = useAddToQueue()
|
||||||
const { mutate: addFavorite } = useAddFavorite()
|
const { mutate: addFavorite } = useAddFavorite()
|
||||||
const { mutate: removeFavorite } = useRemoveFavorite()
|
const { mutate: removeFavorite } = useRemoveFavorite()
|
||||||
const [hideRunTimes] = useHideRunTimesSetting()
|
const [hideRunTimes] = useHideRunTimesSetting()
|
||||||
|
|
||||||
const warmContext = useItemContext()
|
const warmContext = useItemContext()
|
||||||
const { data: isFavorite } = useIsFavorite(item)
|
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', {
|
||||||
navigationRef.navigate('Context', {
|
item,
|
||||||
item,
|
navigation,
|
||||||
navigation,
|
})
|
||||||
}),
|
|
||||||
[navigationRef, navigation, item.Id],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onPressCallback = useCallback(async () => {
|
const onPressCallback = async () => {
|
||||||
if (onPress) await onPress()
|
if (onPress) await onPress()
|
||||||
else
|
else
|
||||||
switch (item.Type) {
|
switch (item.Type) {
|
||||||
case 'Audio': {
|
case 'Audio': {
|
||||||
loadNewQueue({
|
loadNewQueue({
|
||||||
api,
|
|
||||||
networkStatus,
|
|
||||||
deviceProfile,
|
|
||||||
track: item,
|
|
||||||
tracklist: [item],
|
|
||||||
index: 0,
|
|
||||||
queue: queueName ?? 'Search',
|
|
||||||
queuingType: QueuingType.FromSelection,
|
|
||||||
startPlayback: true,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'MusicArtist': {
|
|
||||||
navigation?.navigate('Artist', { artist: item })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'MusicAlbum': {
|
|
||||||
navigation?.navigate('Album', { album: item })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'Playlist': {
|
|
||||||
navigation?.navigate('Playlist', { playlist: item, canEdit: true })
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [onPress, loadNewQueue, item.Id, navigation, queueName])
|
|
||||||
|
|
||||||
const renderRunTime = useMemo(
|
|
||||||
() => item.Type === BaseItemKind.Audio && !hideRunTimes,
|
|
||||||
[item.Type, hideRunTimes],
|
|
||||||
)
|
|
||||||
|
|
||||||
const isAudio = useMemo(() => item.Type === 'Audio', [item.Type])
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
const swipeHandlers = useCallback(
|
|
||||||
() => ({
|
|
||||||
addToQueue: async () =>
|
|
||||||
await addToQueue({
|
|
||||||
api,
|
api,
|
||||||
deviceProfile,
|
|
||||||
networkStatus,
|
networkStatus,
|
||||||
tracks: [item],
|
deviceProfile,
|
||||||
queuingType: QueuingType.DirectlyQueued,
|
track: item,
|
||||||
}),
|
tracklist: [item],
|
||||||
toggleFavorite: () =>
|
index: 0,
|
||||||
isFavorite ? removeFavorite({ item }) : addFavorite({ item }),
|
queue: queueName ?? 'Search',
|
||||||
addToPlaylist: () => navigationRef.navigate('AddToPlaylist', { track: item }),
|
queuingType: QueuingType.FromSelection,
|
||||||
}),
|
startPlayback: true,
|
||||||
[
|
})
|
||||||
addToQueue,
|
break
|
||||||
|
}
|
||||||
|
case 'MusicArtist': {
|
||||||
|
navigation?.navigate('Artist', { artist: item })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MusicAlbum': {
|
||||||
|
navigation?.navigate('Album', { album: item })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'Playlist': {
|
||||||
|
navigation?.navigate('Playlist', { playlist: item, canEdit: true })
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderRunTime = item.Type === BaseItemKind.Audio && !hideRunTimes
|
||||||
|
|
||||||
|
const isAudio = item.Type === 'Audio'
|
||||||
|
|
||||||
|
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 = () => ({
|
||||||
|
addToQueue: async () =>
|
||||||
|
await addToQueue({
|
||||||
api,
|
api,
|
||||||
deviceProfile,
|
deviceProfile,
|
||||||
networkStatus,
|
networkStatus,
|
||||||
item,
|
tracks: [item],
|
||||||
addFavorite,
|
queuingType: QueuingType.DirectlyQueued,
|
||||||
removeFavorite,
|
}),
|
||||||
isFavorite,
|
toggleFavorite: () => (isFavorite ? removeFavorite({ item }) : addFavorite({ item })),
|
||||||
],
|
addToPlaylist: () => navigationRef.navigate('AddToPlaylist', { track: item }),
|
||||||
)
|
})
|
||||||
|
|
||||||
const swipeConfig = useMemo(
|
const swipeConfig = isAudio
|
||||||
() =>
|
? buildSwipeConfig({
|
||||||
isAudio
|
left: leftSettings,
|
||||||
? buildSwipeConfig({
|
right: rightSettings,
|
||||||
left: leftSettings,
|
handlers: swipeHandlers(),
|
||||||
right: rightSettings,
|
})
|
||||||
handlers: swipeHandlers(),
|
: {}
|
||||||
})
|
|
||||||
: {},
|
|
||||||
[isAudio, leftSettings, rightSettings, swipeHandlers],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleArtworkLayout = useCallback(
|
const handleArtworkLayout = (event: LayoutChangeEvent) => {
|
||||||
(event: LayoutChangeEvent) => {
|
const { width } = event.nativeEvent.layout
|
||||||
const { width } = event.nativeEvent.layout
|
artworkAreaWidth.value = width
|
||||||
artworkAreaWidth.value = width
|
}
|
||||||
},
|
|
||||||
[artworkAreaWidth],
|
|
||||||
)
|
|
||||||
|
|
||||||
const pressStyle = useMemo(() => ({ opacity: 0.5 }), [])
|
const pressStyle = {
|
||||||
|
opacity: 0.5,
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SwipeableRow
|
<SwipeableRow
|
||||||
disabled={!isAudio}
|
disabled={!isAudio}
|
||||||
{...swipeConfig}
|
{...swipeConfig}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
|
onPress={onPressCallback}
|
||||||
|
>
|
||||||
|
<XStack
|
||||||
|
alignContent='center'
|
||||||
|
width={'100%'}
|
||||||
|
testID={item.Id ? `item-row-${item.Id}` : undefined}
|
||||||
|
onPressIn={onPressIn}
|
||||||
onPress={onPressCallback}
|
onPress={onPressCallback}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
animation={'quick'}
|
||||||
|
pressStyle={pressStyle}
|
||||||
|
paddingVertical={'$2'}
|
||||||
|
paddingRight={'$2'}
|
||||||
|
paddingLeft={'$1'}
|
||||||
|
backgroundColor={'$background'}
|
||||||
|
borderRadius={'$2'}
|
||||||
>
|
>
|
||||||
<XStack
|
<HideableArtwork item={item} circular={circular} onLayout={handleArtworkLayout} />
|
||||||
alignContent='center'
|
<SlidingTextArea leftGapWidth={artworkAreaWidth}>
|
||||||
width={'100%'}
|
<ItemRowDetails item={item} />
|
||||||
testID={item.Id ? `item-row-${item.Id}` : undefined}
|
</SlidingTextArea>
|
||||||
onPressIn={onPressIn}
|
|
||||||
onPress={onPressCallback}
|
|
||||||
onLongPress={onLongPress}
|
|
||||||
animation={'quick'}
|
|
||||||
pressStyle={pressStyle}
|
|
||||||
paddingVertical={'$2'}
|
|
||||||
paddingRight={'$2'}
|
|
||||||
paddingLeft={'$1'}
|
|
||||||
backgroundColor={'$background'}
|
|
||||||
borderRadius={'$2'}
|
|
||||||
>
|
|
||||||
<HideableArtwork
|
|
||||||
item={item}
|
|
||||||
circular={circular}
|
|
||||||
onLayout={handleArtworkLayout}
|
|
||||||
/>
|
|
||||||
<SlidingTextArea leftGapWidth={artworkAreaWidth}>
|
|
||||||
<ItemRowDetails item={item} />
|
|
||||||
</SlidingTextArea>
|
|
||||||
|
|
||||||
<XStack justifyContent='flex-end' alignItems='center' flexShrink={1}>
|
|
||||||
{renderRunTime ? (
|
|
||||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
|
||||||
) : item.Type === 'Playlist' ? (
|
|
||||||
<Text color={'$borderColor'}>
|
|
||||||
{`${playlistTrackCount ?? 0} ${playlistTrackCount === 1 ? 'Track' : 'Tracks'}`}
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<FavoriteIcon item={item} />
|
|
||||||
|
|
||||||
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
|
|
||||||
<Icon name='dots-horizontal' onPress={onLongPress} />
|
|
||||||
) : null}
|
|
||||||
</XStack>
|
|
||||||
</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 {
|
|
||||||
const route = useRoute<RouteProp<BaseStackParamList>>()
|
|
||||||
|
|
||||||
const shouldRenderArtistName =
|
|
||||||
item.Type === 'Audio' || (item.Type === 'MusicAlbum' && route.name !== 'Artist')
|
|
||||||
|
|
||||||
const shouldRenderProductionYear = item.Type === 'MusicAlbum' && route.name === 'Artist'
|
|
||||||
|
|
||||||
const shouldRenderGenres =
|
|
||||||
item.Type === 'Playlist' || item.Type === BaseItemKind.MusicArtist
|
|
||||||
|
|
||||||
return (
|
|
||||||
<YStack alignContent='center' justifyContent='center' flexGrow={1}>
|
|
||||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
|
||||||
{item.Name ?? ''}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
{shouldRenderArtistName && (
|
|
||||||
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
|
||||||
{item.AlbumArtist ?? 'Untitled Artist'}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{shouldRenderProductionYear && (
|
|
||||||
<XStack gap='$2'>
|
|
||||||
<Text
|
|
||||||
color={'$borderColor'}
|
|
||||||
lineBreakStrategyIOS='standard'
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{item.ProductionYear?.toString() ?? 'Unknown Year'}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text color={'$borderColor'}>•</Text>
|
|
||||||
|
|
||||||
|
<XStack justifyContent='flex-end' alignItems='center' flexShrink={1}>
|
||||||
|
{renderRunTime ? (
|
||||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||||
</XStack>
|
) : item.Type === 'Playlist' ? (
|
||||||
)}
|
<Text color={'$borderColor'}>
|
||||||
|
{`${playlistTrackCount ?? 0} ${playlistTrackCount === 1 ? 'Track' : 'Tracks'}`}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<FavoriteIcon item={item} />
|
||||||
|
|
||||||
{shouldRenderGenres && item.Genres && (
|
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
|
||||||
|
<Icon name='dots-horizontal' onPress={onLongPress} />
|
||||||
|
) : null}
|
||||||
|
</XStack>
|
||||||
|
</XStack>
|
||||||
|
</SwipeableRow>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ItemRowDetails({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||||
|
const route = useRoute<RouteProp<BaseStackParamList>>()
|
||||||
|
|
||||||
|
const shouldRenderArtistName =
|
||||||
|
item.Type === 'Audio' || (item.Type === 'MusicAlbum' && route.name !== 'Artist')
|
||||||
|
|
||||||
|
const shouldRenderProductionYear = item.Type === 'MusicAlbum' && route.name === 'Artist'
|
||||||
|
|
||||||
|
const shouldRenderGenres = item.Type === 'Playlist' || item.Type === BaseItemKind.MusicArtist
|
||||||
|
|
||||||
|
return (
|
||||||
|
<YStack alignContent='center' justifyContent='center' flexGrow={1}>
|
||||||
|
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||||
|
{item.Name ?? ''}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{shouldRenderArtistName && (
|
||||||
|
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||||
|
{item.AlbumArtist ?? 'Untitled Artist'}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{shouldRenderProductionYear && (
|
||||||
|
<XStack gap='$2'>
|
||||||
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||||
{item.Genres?.join(', ') ?? ''}
|
{item.ProductionYear?.toString() ?? 'Unknown Year'}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
|
||||||
</YStack>
|
<Text color={'$borderColor'}>•</Text>
|
||||||
)
|
|
||||||
},
|
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||||
(prevProps, nextProps) => prevProps.item.Id === nextProps.item.Id,
|
</XStack>
|
||||||
)
|
)}
|
||||||
|
|
||||||
|
{shouldRenderGenres && item.Genres && (
|
||||||
|
<Text color={'$borderColor'} lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||||
|
{item.Genres?.join(', ') ?? ''}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</YStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Artwork wrapper that fades out when the quick-action menu is open
|
// Artwork wrapper that fades out when the quick-action menu is open
|
||||||
const HideableArtwork = memo(
|
function HideableArtwork({
|
||||||
function HideableArtwork({
|
item,
|
||||||
item,
|
circular,
|
||||||
circular,
|
onLayout,
|
||||||
onLayout,
|
}: {
|
||||||
}: {
|
item: BaseItemDto
|
||||||
item: BaseItemDto
|
circular?: boolean
|
||||||
circular?: boolean
|
onLayout?: (event: LayoutChangeEvent) => void
|
||||||
onLayout?: (event: LayoutChangeEvent) => void
|
}): React.JSX.Element {
|
||||||
}): React.JSX.Element {
|
const { tx } = useSwipeableRowContext()
|
||||||
const { tx } = useSwipeableRowContext()
|
// Hide artwork as soon as swiping starts (any non-zero tx)
|
||||||
// Hide artwork as soon as swiping starts (any non-zero tx)
|
const style = useAnimatedStyle(() => ({
|
||||||
const style = useAnimatedStyle(() => ({
|
opacity: tx.value === 0 ? withTiming(1) : 0,
|
||||||
opacity: tx.value === 0 ? withTiming(1) : 0,
|
}))
|
||||||
}))
|
return (
|
||||||
return (
|
<Animated.View style={style} onLayout={onLayout}>
|
||||||
<Animated.View style={style} onLayout={onLayout}>
|
<XStack marginHorizontal={'$3'} marginVertical={'auto'} alignItems='center'>
|
||||||
<XStack marginHorizontal={'$3'} marginVertical={'auto'} alignItems='center'>
|
<ItemImage
|
||||||
<ItemImage
|
item={item}
|
||||||
item={item}
|
height={'$12'}
|
||||||
height={'$12'}
|
width={'$12'}
|
||||||
width={'$12'}
|
circular={item.Type === 'MusicArtist' || circular}
|
||||||
circular={item.Type === 'MusicArtist' || circular}
|
/>
|
||||||
/>
|
</XStack>
|
||||||
</XStack>
|
</Animated.View>
|
||||||
</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,
|
||||||
leftGapWidth,
|
children,
|
||||||
children,
|
}: {
|
||||||
}: {
|
leftGapWidth: SharedValue<number>
|
||||||
leftGapWidth: SharedValue<number>
|
children: React.ReactNode
|
||||||
children: React.ReactNode
|
}): React.JSX.Element {
|
||||||
}): React.JSX.Element {
|
const { tx, rightWidth } = useSwipeableRowContext()
|
||||||
const { tx, rightWidth } = useSwipeableRowContext()
|
const tokenValue = getToken('$2', 'space')
|
||||||
const tokenValue = getToken('$2', 'space')
|
const spacingValue = typeof tokenValue === 'number' ? tokenValue : parseFloat(`${tokenValue}`)
|
||||||
const spacingValue =
|
const quickActionBuffer = Number.isFinite(spacingValue) ? spacingValue : 8
|
||||||
typeof tokenValue === 'number' ? tokenValue : parseFloat(`${tokenValue}`)
|
const style = useAnimatedStyle(() => {
|
||||||
const quickActionBuffer = Number.isFinite(spacingValue) ? spacingValue : 8
|
const t = tx.value
|
||||||
const style = useAnimatedStyle(() => {
|
let offset = 0
|
||||||
const t = tx.value
|
if (t > 0 && leftGapWidth.get() > 0) {
|
||||||
let offset = 0
|
offset = -Math.min(t, leftGapWidth.get())
|
||||||
if (t > 0 && leftGapWidth.get() > 0) {
|
} else if (t < 0) {
|
||||||
offset = -Math.min(t, leftGapWidth.get())
|
const rightSpace = Math.max(0, rightWidth)
|
||||||
} else if (t < 0) {
|
const compensate = Math.min(-t, rightSpace)
|
||||||
const rightSpace = Math.max(0, rightWidth)
|
const progress = rightSpace > 0 ? compensate / rightSpace : 1
|
||||||
const compensate = Math.min(-t, rightSpace)
|
offset = compensate * 0.7 + quickActionBuffer * progress
|
||||||
const progress = rightSpace > 0 ? compensate / rightSpace : 1
|
}
|
||||||
offset = compensate * 0.7 + quickActionBuffer * progress
|
return { transform: [{ translateX: offset }] }
|
||||||
}
|
})
|
||||||
return { transform: [{ translateX: offset }] }
|
const paddingRightValue = Number.isFinite(spacingValue) ? spacingValue : 8
|
||||||
})
|
return (
|
||||||
const paddingRightValue = Number.isFinite(spacingValue) ? spacingValue : 8
|
<Animated.View style={[{ flex: 5, paddingRight: paddingRightValue }, style]}>
|
||||||
return (
|
{children}
|
||||||
<Animated.View style={[{ flex: 5, paddingRight: paddingRightValue }, style]}>
|
</Animated.View>
|
||||||
{children}
|
)
|
||||||
</Animated.View>
|
}
|
||||||
)
|
|
||||||
},
|
|
||||||
(prevProps, nextProps) =>
|
|
||||||
prevProps.leftGapWidth === nextProps.leftGapWidth &&
|
|
||||||
prevProps.children?.valueOf() === nextProps.children?.valueOf(),
|
|
||||||
)
|
|
||||||
|
|
||||||
export default ItemRow
|
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 { getToken, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||||
import { Text } from '../helpers/text'
|
import { Text } from '../helpers/text'
|
||||||
import { RunTimeTicks } from '../helpers/time-codes'
|
import { RunTimeTicks } from '../helpers/time-codes'
|
||||||
@@ -28,10 +28,7 @@ import { useAddFavorite, useRemoveFavorite } from '../../../api/mutations/favori
|
|||||||
import { StackActions } from '@react-navigation/native'
|
import { StackActions } from '@react-navigation/native'
|
||||||
import { useSwipeableRowContext } from './swipeable-row-context'
|
import { useSwipeableRowContext } from './swipeable-row-context'
|
||||||
import { useHideRunTimesSetting } from '../../../stores/settings/app'
|
import { useHideRunTimesSetting } from '../../../stores/settings/app'
|
||||||
import { queryClient, ONE_HOUR } from '../../../constants/query-client'
|
import useStreamedMediaInfo from '../../../api/queries/media'
|
||||||
import { fetchMediaInfo } from '../../../api/queries/media/utils'
|
|
||||||
import MediaInfoQueryKey from '../../../api/queries/media/keys'
|
|
||||||
import JellifyTrack from '../../../types/JellifyTrack'
|
|
||||||
|
|
||||||
export interface TrackProps {
|
export interface TrackProps {
|
||||||
track: BaseItemDto
|
track: BaseItemDto
|
||||||
@@ -48,329 +45,243 @@ export interface TrackProps {
|
|||||||
editing?: boolean | undefined
|
editing?: boolean | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const queueItemsCache = new WeakMap<JellifyTrack[], BaseItemDto[]>()
|
export default 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 getQueueItems = (queue: JellifyTrack[] | undefined): BaseItemDto[] => {
|
const api = useApi()
|
||||||
if (!queue?.length) return []
|
|
||||||
|
|
||||||
const cached = queueItemsCache.get(queue)
|
const deviceProfile = useStreamingDeviceProfile()
|
||||||
if (cached) return cached
|
|
||||||
|
|
||||||
const mapped = queue.map((entry) => entry.item)
|
const [hideRunTimes] = useHideRunTimesSetting()
|
||||||
queueItemsCache.set(queue, mapped)
|
|
||||||
return mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
const Track = memo(
|
const nowPlaying = useCurrentTrack()
|
||||||
function Track({
|
const playQueue = usePlayQueue()
|
||||||
track,
|
const loadNewQueue = useLoadNewQueue()
|
||||||
navigation,
|
const addToQueue = useAddToQueue()
|
||||||
tracklist,
|
const [networkStatus] = useNetworkStatus()
|
||||||
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 { data: mediaInfo } = useStreamedMediaInfo(track.Id)
|
||||||
|
|
||||||
const deviceProfile = useStreamingDeviceProfile()
|
const offlineAudio = useDownloadedTrack(track.Id)
|
||||||
|
|
||||||
const [hideRunTimes] = useHideRunTimesSetting()
|
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 nowPlaying = useCurrentTrack()
|
// Memoize expensive computations
|
||||||
const playQueue = usePlayQueue()
|
const isPlaying = nowPlaying?.item.Id === track.Id
|
||||||
const loadNewQueue = useLoadNewQueue()
|
|
||||||
const addToQueue = useAddToQueue()
|
|
||||||
const [networkStatus] = useNetworkStatus()
|
|
||||||
|
|
||||||
const offlineAudio = useDownloadedTrack(track.Id)
|
const isOffline = networkStatus === networkStatusTypes.DISCONNECTED
|
||||||
|
|
||||||
const { mutate: addFavorite } = useAddFavorite()
|
// Memoize tracklist for queue loading
|
||||||
const { mutate: removeFavorite } = useRemoveFavorite()
|
const memoizedTracklist = tracklist ?? playQueue?.map((track) => track.item) ?? []
|
||||||
const { data: isFavoriteTrack } = useIsFavorite(track)
|
|
||||||
const leftSettings = useSwipeSettingsStore((s) => s.left)
|
|
||||||
const rightSettings = useSwipeSettingsStore((s) => s.right)
|
|
||||||
|
|
||||||
// Memoize expensive computations
|
// Memoize handlers to prevent recreation
|
||||||
const isPlaying = useMemo(
|
const handlePress = async () => {
|
||||||
() => nowPlaying?.item.Id === track.Id,
|
if (onPress) {
|
||||||
[nowPlaying?.item.Id, track.Id],
|
await onPress()
|
||||||
)
|
} else {
|
||||||
|
loadNewQueue({
|
||||||
const isOffline = useMemo(
|
|
||||||
() => networkStatus === networkStatusTypes.DISCONNECTED,
|
|
||||||
[networkStatus],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Memoize tracklist for queue loading
|
|
||||||
const memoizedTracklist = useMemo(
|
|
||||||
() => tracklist ?? getQueueItems(playQueue),
|
|
||||||
[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,
|
|
||||||
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()
|
|
||||||
|
|
||||||
navigationRef.navigate('Context', {
|
|
||||||
item: track,
|
|
||||||
navigation,
|
|
||||||
streamingMediaSourceInfo,
|
|
||||||
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
|
|
||||||
})
|
|
||||||
}, [fetchStreamingMediaSourceInfo, track, navigation, offlineAudio?.mediaSourceInfo])
|
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
|
||||||
if (onLongPress) {
|
|
||||||
onLongPress()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
void openContextSheet()
|
|
||||||
}, [onLongPress, openContextSheet])
|
|
||||||
|
|
||||||
const handleIconPress = useCallback(() => {
|
|
||||||
void openContextSheet()
|
|
||||||
}, [openContextSheet])
|
|
||||||
|
|
||||||
// Memoize artists text
|
|
||||||
const artistsText = useMemo(() => track.Artists?.join(', ') ?? '', [track.Artists])
|
|
||||||
|
|
||||||
// Memoize track name
|
|
||||||
const trackName = useMemo(() => track.Name ?? 'Untitled Track', [track.Name])
|
|
||||||
|
|
||||||
// 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],
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
api,
|
||||||
deviceProfile,
|
deviceProfile,
|
||||||
networkStatus,
|
networkStatus,
|
||||||
track,
|
track,
|
||||||
addFavorite,
|
index,
|
||||||
removeFavorite,
|
tracklist: memoizedTracklist,
|
||||||
isFavoriteTrack,
|
queue,
|
||||||
navigationRef,
|
queuingType: QueuingType.FromSelection,
|
||||||
],
|
startPlayback: true,
|
||||||
)
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const swipeConfig = useMemo(
|
const handleLongPress = () => {
|
||||||
() =>
|
if (onLongPress) {
|
||||||
buildSwipeConfig({
|
onLongPress()
|
||||||
left: leftSettings,
|
} else {
|
||||||
right: rightSettings,
|
navigationRef.navigate('Context', {
|
||||||
handlers: swipeHandlers,
|
item: track,
|
||||||
}),
|
navigation,
|
||||||
[leftSettings, rightSettings, swipeHandlers],
|
streamingMediaSourceInfo: mediaInfo?.MediaSources
|
||||||
)
|
? mediaInfo!.MediaSources![0]
|
||||||
|
: undefined,
|
||||||
|
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const textColor = useMemo(
|
const handleIconPress = () => {
|
||||||
() => (isPlaying ? theme.primary.val : theme.color.val),
|
navigationRef.navigate('Context', {
|
||||||
[isPlaying],
|
item: track,
|
||||||
)
|
navigation,
|
||||||
|
streamingMediaSourceInfo: mediaInfo?.MediaSources
|
||||||
|
? mediaInfo!.MediaSources![0]
|
||||||
|
: undefined,
|
||||||
|
downloadedMediaSourceInfo: offlineAudio?.mediaSourceInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const runtimeComponent = useMemo(
|
// Memoize text color to prevent recalculation
|
||||||
() =>
|
const textColor = isPlaying
|
||||||
hideRunTimes ? (
|
? theme.primary.val
|
||||||
<></>
|
: isOffline
|
||||||
) : (
|
? offlineAudio
|
||||||
<RunTimeTicks
|
? theme.color
|
||||||
key={`${track.Id}-runtime`}
|
: theme.neutral.val
|
||||||
props={{
|
: theme.color
|
||||||
style: {
|
|
||||||
textAlign: 'right',
|
|
||||||
minWidth: getToken('$10'),
|
|
||||||
alignSelf: 'center',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{track.RunTimeTicks}
|
|
||||||
</RunTimeTicks>
|
|
||||||
),
|
|
||||||
[hideRunTimes, track.RunTimeTicks],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
// Memoize artists text
|
||||||
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
|
const artistsText = track.Artists?.join(', ') ?? ''
|
||||||
<SwipeableRow
|
|
||||||
disabled={isNested === true}
|
// Memoize track name
|
||||||
{...swipeConfig}
|
const trackName = track.Name ?? 'Untitled Track'
|
||||||
onLongPress={handleLongPress}
|
|
||||||
onPress={handlePress}
|
// Memoize index number
|
||||||
|
const indexNumber = track.IndexNumber?.toString() ?? ''
|
||||||
|
|
||||||
|
// Memoize show artists condition
|
||||||
|
const shouldShowArtists = showArtwork || (track.Artists && track.Artists.length > 1)
|
||||||
|
|
||||||
|
const swipeHandlers = {
|
||||||
|
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 }))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeConfig = buildSwipeConfig({
|
||||||
|
left: leftSettings,
|
||||||
|
right: rightSettings,
|
||||||
|
handlers: swipeHandlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
const runtimeComponent = hideRunTimes ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<RunTimeTicks
|
||||||
|
key={`${track.Id}-runtime`}
|
||||||
|
props={{
|
||||||
|
style: {
|
||||||
|
textAlign: 'right',
|
||||||
|
minWidth: getToken('$10'),
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{track.RunTimeTicks}
|
||||||
|
</RunTimeTicks>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Theme name={invertedColors ? 'inverted_purple' : undefined}>
|
||||||
|
<SwipeableRow
|
||||||
|
disabled={isNested === true}
|
||||||
|
{...swipeConfig}
|
||||||
|
onLongPress={handleLongPress}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
<XStack
|
||||||
|
alignContent='center'
|
||||||
|
alignItems='center'
|
||||||
|
flex={1}
|
||||||
|
testID={testID ?? undefined}
|
||||||
|
paddingVertical={'$2'}
|
||||||
|
justifyContent='flex-start'
|
||||||
|
paddingRight={'$2'}
|
||||||
|
animation={'quick'}
|
||||||
|
pressStyle={{ opacity: 0.5 }}
|
||||||
|
backgroundColor={'$background'}
|
||||||
>
|
>
|
||||||
<XStack
|
<XStack
|
||||||
alignContent='center'
|
alignContent='center'
|
||||||
alignItems='center'
|
justifyContent='center'
|
||||||
flex={1}
|
marginHorizontal={showArtwork ? '$2' : '$1'}
|
||||||
testID={testID ?? undefined}
|
onLayout={(e) => setArtworkAreaWidth(e.nativeEvent.layout.width)}
|
||||||
paddingVertical={'$2'}
|
|
||||||
justifyContent='flex-start'
|
|
||||||
paddingRight={'$2'}
|
|
||||||
animation={'quick'}
|
|
||||||
pressStyle={{ opacity: 0.5 }}
|
|
||||||
backgroundColor={'$background'}
|
|
||||||
>
|
>
|
||||||
<XStack
|
{showArtwork ? (
|
||||||
alignContent='center'
|
<HideableArtwork>
|
||||||
justifyContent='center'
|
<ItemImage item={track} width={'$12'} height={'$12'} />
|
||||||
marginHorizontal={showArtwork ? '$2' : '$1'}
|
</HideableArtwork>
|
||||||
onLayout={(e) => setArtworkAreaWidth(e.nativeEvent.layout.width)}
|
) : (
|
||||||
>
|
<Text
|
||||||
{showArtwork ? (
|
key={`${track.Id}-number`}
|
||||||
<HideableArtwork>
|
color={textColor}
|
||||||
<ItemImage item={track} width={'$12'} height={'$12'} />
|
width={getToken('$12')}
|
||||||
</HideableArtwork>
|
textAlign='center'
|
||||||
) : (
|
fontVariant={['tabular-nums']}
|
||||||
<Text
|
>
|
||||||
key={`${track.Id}-number`}
|
{indexNumber}
|
||||||
color={textColor}
|
</Text>
|
||||||
width={getToken('$12')}
|
)}
|
||||||
textAlign='center'
|
</XStack>
|
||||||
fontVariant={['tabular-nums']}
|
|
||||||
>
|
|
||||||
{indexNumber}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</XStack>
|
|
||||||
|
|
||||||
<SlidingTextArea leftGapWidth={artworkAreaWidth} hasArtwork={!!showArtwork}>
|
<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
|
||||||
|
color={textColor}
|
||||||
|
lineBreakStrategyIOS='standard'
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{trackName}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{shouldShowArtists && (
|
||||||
<Text
|
<Text
|
||||||
key={`${track.Id}-name`}
|
key={`${track.Id}-artists`}
|
||||||
bold
|
|
||||||
color={textColor}
|
|
||||||
lineBreakStrategyIOS='standard'
|
lineBreakStrategyIOS='standard'
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
|
color={'$borderColor'}
|
||||||
>
|
>
|
||||||
{trackName}
|
{artistsText}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{shouldShowArtists && (
|
|
||||||
<Text
|
|
||||||
key={`${track.Id}-artists`}
|
|
||||||
lineBreakStrategyIOS='standard'
|
|
||||||
numberOfLines={1}
|
|
||||||
color={'$borderColor'}
|
|
||||||
>
|
|
||||||
{artistsText}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</YStack>
|
|
||||||
</SlidingTextArea>
|
|
||||||
|
|
||||||
<XStack justifyContent='flex-end' alignItems='center' flex={2} gap='$1'>
|
|
||||||
<DownloadedIcon item={track} />
|
|
||||||
<FavoriteIcon item={track} />
|
|
||||||
{runtimeComponent}
|
|
||||||
{!editing && (
|
|
||||||
<Icon name={'dots-horizontal'} onPress={handleIconPress} />
|
|
||||||
)}
|
)}
|
||||||
</XStack>
|
</YStack>
|
||||||
|
</SlidingTextArea>
|
||||||
|
|
||||||
|
<XStack justifyContent='flex-end' alignItems='center' flexShrink={1} gap='$1'>
|
||||||
|
<DownloadedIcon item={track} />
|
||||||
|
<FavoriteIcon item={track} />
|
||||||
|
{runtimeComponent}
|
||||||
|
{!editing && <Icon name={'dots-horizontal'} onPress={handleIconPress} />}
|
||||||
</XStack>
|
</XStack>
|
||||||
</SwipeableRow>
|
</XStack>
|
||||||
</Theme>
|
</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 }) {
|
function HideableArtwork({ children }: { children: React.ReactNode }) {
|
||||||
const { tx } = useSwipeableRowContext()
|
const { tx } = useSwipeableRowContext()
|
||||||
@@ -402,7 +313,5 @@ function SlidingTextArea({
|
|||||||
}
|
}
|
||||||
return { transform: [{ translateX: offset }] }
|
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