React Compiler (#737)

* chore: r2

* compiler

* compiler
This commit is contained in:
Ritesh Shukla
2025-12-02 00:54:05 +05:30
committed by GitHub
parent ac7df341e0
commit 4b5faacd28
16 changed files with 292 additions and 395 deletions

View File

@@ -1,4 +1,8 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-worklets/plugin', 'react-native-worklets-core/plugin'],
plugins: [
'babel-plugin-react-compiler',
'react-native-worklets/plugin',
'react-native-worklets-core/plugin',
],
}

View File

@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "jellify",
@@ -83,6 +82,7 @@
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "19.1.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
@@ -1033,6 +1033,8 @@
"babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.5", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg=="],
"babel-plugin-react-compiler": ["babel-plugin-react-compiler@1.0.0", "", { "dependencies": { "@babel/types": "^7.26.0" } }, "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw=="],
"babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="],
"babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="],

View File

@@ -115,6 +115,7 @@
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "19.1.0",
"babel-plugin-module-resolver": "^5.0.2",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",

View File

@@ -15,7 +15,7 @@ import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { AddToQueueMutation } from '../../providers/Player/interfaces'
import { QueuingType } from '../../enums/queuing-type'
import { useCallback, useEffect, useMemo } from 'react'
import { useEffect } from 'react'
import navigationRef from '../../../navigation'
import { goToAlbumFromContextSheet, goToArtistFromContextSheet } from './utils/navigation'
import { getItemName } from '../../utils/text'
@@ -98,12 +98,12 @@ export default function ItemContext({
: []
: []
const itemTracks = useMemo(() => {
const itemTracks = (() => {
if (isTrack) return [item]
else if (isAlbum && discs) return discs.flatMap((data) => data.data)
else if (isPlaylist && tracks) return tracks
else return []
}, [isTrack, isAlbum, discs, isPlaylist, tracks])
})()
useEffect(() => trigger('impactLight'), [item?.Id])
@@ -251,26 +251,20 @@ function DownloadMenuRow({ items }: { items: BaseItemDto[] }): React.JSX.Element
const isDownloaded = useIsDownloaded(items.map(({ Id }) => Id))
const downloadItems = useCallback(() => {
const downloadItems = () => {
if (!api) return
const tracks = items.map((item) => mapDtoToTrack(api, item, deviceProfile))
addToDownloadQueue(tracks)
}, [addToDownloadQueue, items])
}
const removeDownloads = useCallback(
() => useRemoveDownload(items.map(({ Id }) => Id)),
[useRemoveDownload, items],
)
const removeDownloads = () => useRemoveDownload(items.map(({ Id }) => Id))
const isPending = useMemo(
() =>
items.filter(
(item) =>
pendingDownloads.filter((download) => download.item.Id === item.Id).length > 0,
).length > 0,
[items, pendingDownloads],
)
const isPending =
items.filter(
(item) =>
pendingDownloads.filter((download) => download.item.Id === item.Id).length > 0,
).length > 0
return isPending ? (
<ListItem
@@ -326,10 +320,10 @@ interface MenuRowProps {
}
function ViewAlbumMenuRow({ album: album, stackNavigation }: MenuRowProps): React.JSX.Element {
const goToAlbum = useCallback(() => {
const goToAlbum = () => {
if (stackNavigation && album) stackNavigation.navigate('Album', { album })
else goToAlbumFromContextSheet(album)
}, [album, stackNavigation, navigationRef])
}
return (
<ListItem
@@ -380,13 +374,10 @@ function ViewArtistMenuRow({
enabled: !!artistId,
})
const goToArtist = useCallback(
(artist: BaseItemDto) => {
if (stackNavigation) stackNavigation.navigate('Artist', { artist })
else goToArtistFromContextSheet(artist)
},
[stackNavigation, navigationRef],
)
const goToArtist = (artist: BaseItemDto) => {
if (stackNavigation) stackNavigation.navigate('Artist', { artist })
else goToArtistFromContextSheet(artist)
}
return artist ? (
<ListItem

View File

@@ -4,7 +4,7 @@ import IconButton from '../../../components/Global/helpers/icon-button'
import { isUndefined } from 'lodash'
import { useTogglePlayback } from '../../../providers/Player/hooks/mutations'
import { usePlaybackState } from '../../../providers/Player/hooks/queries'
import React, { useMemo } from 'react'
import React from 'react'
import Icon from '../../Global/components/icon'
function PlayPauseButtonComponent({
@@ -18,9 +18,9 @@ function PlayPauseButtonComponent({
const state = usePlaybackState()
const largeIcon = useMemo(() => isUndefined(size) || size >= 24, [size])
const largeIcon = isUndefined(size) || size >= 24
const button = useMemo(() => {
const button = (() => {
switch (state) {
case State.Playing: {
return (
@@ -57,7 +57,7 @@ function PlayPauseButtonComponent({
)
}
}
}, [state, size, largeIcon, togglePlayback])
})()
return (
<View justifyContent='center' alignItems='center' flex={flex}>
@@ -72,7 +72,7 @@ export function PlayPauseIcon(): React.JSX.Element {
const togglePlayback = useTogglePlayback()
const state = usePlaybackState()
const button = useMemo(() => {
const button = (() => {
switch (state) {
case State.Playing: {
return <Icon name='pause' color='$primary' onPress={togglePlayback} />
@@ -87,7 +87,7 @@ export function PlayPauseIcon(): React.JSX.Element {
return <Icon name='play' color='$primary' onPress={togglePlayback} />
}
}
}, [state, togglePlayback])
})()
return button
}

View File

@@ -1,6 +1,6 @@
import { XStack, YStack, Spacer, useTheme } from 'tamagui'
import { Text } from '../../Global/helpers/text'
import React, { useCallback, useMemo } from 'react'
import React from 'react'
import ItemImage from '../../Global/components/image'
import Animated, {
useAnimatedStyle,
@@ -20,16 +20,11 @@ export default function PlayerHeader(): React.JSX.Element {
const theme = useTheme()
// If the Queue is a BaseItemDto, display the name of it
const playingFrom = useMemo(
() =>
!queueRef
? 'Untitled'
: typeof queueRef === 'object'
? (queueRef.Name ?? 'Untitled')
: queueRef,
[queueRef],
)
const playingFrom = !queueRef
? 'Untitled'
: typeof queueRef === 'object'
? (queueRef.Name ?? 'Untitled')
: queueRef
return (
<YStack flexGrow={1} justifyContent='flex-start'>
@@ -75,10 +70,10 @@ function PlayerArtwork(): React.JSX.Element {
opacity: withTiming(nowPlaying ? 1 : 0),
}))
const handleLayout = useCallback((event: LayoutChangeEvent) => {
const handleLayout = (event: LayoutChangeEvent) => {
artworkMaxHeight.set(event.nativeEvent.layout.height)
artworkMaxWidth.set(event.nativeEvent.layout.height)
}, [])
}
return (
<YStack

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
import React, { useEffect, useState, useRef } from 'react'
import { HorizontalSlider } from '../../../components/Global/helpers/slider'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import { Spacer, XStack, YStack } from 'tamagui'
@@ -42,14 +42,9 @@ export default function Scrubber(): React.JSX.Element {
const [displayAudioQualityBadge] = useDisplayAudioQualityBadge()
// Memoize expensive calculations
const maxDuration = useMemo(() => {
return Math.round(duration * ProgressMultiplier)
}, [duration])
const maxDuration = Math.round(duration * ProgressMultiplier)
const calculatedPosition = useMemo(() => {
return Math.round(position! * ProgressMultiplier)
}, [position])
const calculatedPosition = Math.round(position! * ProgressMultiplier)
// Optimized position update logic with throttling
useEffect(() => {
@@ -77,70 +72,57 @@ export default function Scrubber(): React.JSX.Element {
}
}, [nowPlaying?.id])
// Optimized seek handler with debouncing
const handleSeek = useCallback(
async (position: number) => {
const seekTime = Math.max(0, position / ProgressMultiplier)
lastSeekTimeRef.current = Date.now()
const handleSeek = async (position: number) => {
const seekTime = Math.max(0, position / ProgressMultiplier)
lastSeekTimeRef.current = Date.now()
try {
await seekTo(seekTime)
} catch (error) {
console.warn('handleSeek callback failed', error)
try {
await seekTo(seekTime)
} catch (error) {
console.warn('handleSeek callback failed', error)
isUserInteractingRef.current = false
setDisplayPosition(calculatedPosition)
} finally {
// Small delay to let the seek settle before allowing updates
setTimeout(() => {
isUserInteractingRef.current = false
setDisplayPosition(calculatedPosition)
} finally {
// Small delay to let the seek settle before allowing updates
setTimeout(() => {
isUserInteractingRef.current = false
}, 100)
}
}, 100)
}
}
const currentSeconds = Math.max(0, Math.round(displayPosition / ProgressMultiplier))
const totalSeconds = Math.round(duration)
const sliderProps = {
maxWidth: width / 1.1,
onSlideStart: (event: unknown, value: number) => {
isUserInteractingRef.current = true
trigger('impactLight')
// Immediately update position for responsive UI
const clampedValue = Math.max(0, Math.min(value, maxDuration))
setDisplayPosition(clampedValue)
},
[seekTo, setDisplayPosition],
)
onSlideMove: (event: unknown, value: number) => {
// Throttled haptic feedback for better performance
trigger('clockTick')
// Memoize time calculations to prevent unnecessary re-renders
const currentSeconds = useMemo(() => {
return Math.max(0, Math.round(displayPosition / ProgressMultiplier))
}, [displayPosition])
// Update position with proper clamping
const clampedValue = Math.max(0, Math.min(value, maxDuration))
setDisplayPosition(clampedValue)
},
onSlideEnd: async (event: unknown, value: number) => {
trigger('notificationSuccess')
const totalSeconds = useMemo(() => {
return Math.round(duration)
}, [duration])
// Clamp final value and update display
const clampedValue = Math.max(0, Math.min(value, maxDuration))
setDisplayPosition(clampedValue)
// Memoize slider props to prevent recreation
const sliderProps = useMemo(
() => ({
maxWidth: width / 1.1,
onSlideStart: (event: unknown, value: number) => {
isUserInteractingRef.current = true
trigger('impactLight')
// Immediately update position for responsive UI
const clampedValue = Math.max(0, Math.min(value, maxDuration))
setDisplayPosition(clampedValue)
},
onSlideMove: (event: unknown, value: number) => {
// Throttled haptic feedback for better performance
trigger('clockTick')
// Update position with proper clamping
const clampedValue = Math.max(0, Math.min(value, maxDuration))
setDisplayPosition(clampedValue)
},
onSlideEnd: async (event: unknown, value: number) => {
trigger('notificationSuccess')
// Clamp final value and update display
const clampedValue = Math.max(0, Math.min(value, maxDuration))
setDisplayPosition(clampedValue)
// Perform the seek operation
await handleSeek(clampedValue)
},
}),
[maxDuration, handleSeek, calculatedPosition, width],
)
// Perform the seek operation
await handleSeek(clampedValue)
},
}
return (
<GestureDetector gesture={scrubGesture}>

View File

@@ -1,4 +1,4 @@
import React, { useMemo, useCallback } from 'react'
import React from 'react'
import { Progress, XStack, YStack } from 'tamagui'
import { useNavigation } from '@react-navigation/native'
import { Text } from '../Global/helpers/text'
@@ -35,60 +35,47 @@ export const Miniplayer = React.memo(function Miniplayer(): React.JSX.Element {
const translateX = useSharedValue(0)
const translateY = useSharedValue(0)
const handleSwipe = useCallback(
(direction: string) => {
if (direction === 'Swiped Left') {
// Inverted: Swipe left -> next
skip(undefined)
} else if (direction === 'Swiped Right') {
// Inverted: Swipe right -> previous
previous()
} else if (direction === 'Swiped Up') {
// Navigate to the big player
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
const handleSwipe = (direction: string) => {
if (direction === 'Swiped Left') {
// Inverted: Swipe left -> next
skip(undefined)
} else if (direction === 'Swiped Right') {
// Inverted: Swipe right -> previous
previous()
} else if (direction === 'Swiped Up') {
// Navigate to the big player
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
}
}
const gesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX
translateY.value = event.translationY
})
.onEnd((event) => {
const threshold = 100
if (event.translationX > threshold) {
runOnJS(handleSwipe)('Swiped Right')
translateX.value = withSpring(200)
} else if (event.translationX < -threshold) {
runOnJS(handleSwipe)('Swiped Left')
translateX.value = withSpring(-200)
} else if (event.translationY < -threshold) {
runOnJS(handleSwipe)('Swiped Up')
translateY.value = withSpring(-200)
} else {
translateX.value = withSpring(0)
translateY.value = withSpring(0)
}
},
[skip, previous, navigation],
)
})
const gesture = useMemo(
() =>
Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX
translateY.value = event.translationY
})
.onEnd((event) => {
const threshold = 100
const openPlayer = () => navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
if (event.translationX > threshold) {
runOnJS(handleSwipe)('Swiped Right')
translateX.value = withSpring(200)
} else if (event.translationX < -threshold) {
runOnJS(handleSwipe)('Swiped Left')
translateX.value = withSpring(-200)
} else if (event.translationY < -threshold) {
runOnJS(handleSwipe)('Swiped Up')
translateY.value = withSpring(-200)
} else {
translateX.value = withSpring(0)
translateY.value = withSpring(0)
}
}),
[translateX, translateY, handleSwipe],
)
const openPlayer = useCallback(
() => navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' }),
[navigation],
)
const pressStyle = useMemo(
() => ({
opacity: 0.6,
}),
[],
)
const pressStyle = {
opacity: 0.6,
}
return (
<GestureDetector gesture={gesture}>

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react'
import React, { useState } from 'react'
import Input from '../Global/helpers/input'
import ItemRow from '../Global/components/item-row'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
@@ -45,7 +45,7 @@ export default function Search({
queryFn: () => fetchSearchSuggestions(api, user, library?.musicLibraryId),
})
const search = useCallback(() => {
const search = () => {
let timeout: ReturnType<typeof setTimeout>
return () => {
@@ -55,16 +55,16 @@ export default function Search({
refetchSuggestions()
}, 1000)
}
}, [])
}
const handleSearchStringUpdate = (value: string | undefined) => {
setSearchString(value)
search()
}
const handleScrollBeginDrag = useCallback(() => {
const handleScrollBeginDrag = () => {
closeAllSwipeableRows()
}, [])
}
return (
<FlatList

View File

@@ -1,4 +1,3 @@
import { useCallback } from 'react'
import ItemRow from '../Global/components/item-row'
import { Text } from '../Global/helpers/text'
import { H3, Separator, YStack } from 'tamagui'
@@ -17,9 +16,9 @@ export default function Suggestions({
suggestions: BaseItemDto[] | undefined
}): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<SearchParamList>>()
const handleScrollBeginDrag = useCallback(() => {
const handleScrollBeginDrag = () => {
closeAllSwipeableRows()
}, [])
}
return (
<FlashList

View File

@@ -1,4 +1,4 @@
import React, { RefObject, useMemo, useRef, useCallback, useEffect } from 'react'
import React, { RefObject, useRef, useEffect } from 'react'
import Track from '../Global/components/track'
import { Separator, useTheme, XStack, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -35,29 +35,22 @@ export default function Tracks({
const pendingLetterRef = useRef<string | null>(null)
const stickyHeaderIndicies = useMemo(() => {
const stickyHeaderIndicies = (() => {
if (!showAlphabeticalSelector || !tracksInfiniteQuery.data) return []
return tracksInfiniteQuery.data
.map((track, index) => (typeof track === 'string' ? index : 0))
.filter((value, index, indices) => indices.indexOf(value) === index)
}, [showAlphabeticalSelector, tracksInfiniteQuery.data])
})()
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
// Memoize the expensive tracks processing to prevent memory leaks
const tracksToDisplay = React.useMemo(
() => tracksInfiniteQuery.data?.filter((track) => typeof track === 'object') ?? [],
[tracksInfiniteQuery.data],
)
const tracksToDisplay =
tracksInfiniteQuery.data?.filter((track) => typeof track === 'object') ?? []
// Memoize key extraction for FlashList performance
const keyExtractor = React.useCallback(
(item: string | number | BaseItemDto) =>
typeof item === 'object' ? item.Id! : item.toString(),
[],
)
const keyExtractor = (item: string | number | BaseItemDto) =>
typeof item === 'object' ? item.Id! : item.toString()
/**
* Memoize render item to prevent recreation
@@ -66,31 +59,35 @@ export default function Tracks({
* it factors in the list headings, meaning pressing a track may not
* play that exact track, since the index was offset by the headings
*/
const renderItem = useCallback(
({ item: track, index }: { index: number; item: string | number | BaseItemDto }) =>
typeof track === 'string' ? (
<FlashListStickyHeader text={track.toUpperCase()} />
) : typeof track === 'number' ? null : typeof track === 'object' ? (
<Track
navigation={navigation}
showArtwork
index={0}
track={track}
testID={`track-item-${index}`}
tracklist={tracksToDisplay.slice(index, index + 50)}
queue={queue}
/>
) : null,
[tracksToDisplay, queue, navigation, queue],
)
const renderItem = ({
item: track,
index,
}: {
index: number
item: string | number | BaseItemDto
}) =>
typeof track === 'string' ? (
<FlashListStickyHeader text={track.toUpperCase()} />
) : typeof track === 'number' ? null : typeof track === 'object' ? (
<Track
navigation={navigation}
showArtwork
index={0}
track={track}
testID={`track-item-${index}`}
tracklist={tracksToDisplay.slice(index, index + 50)}
queue={queue}
/>
) : null
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 />
// Effect for handling the pending alphabet selector letter
useEffect(() => {
@@ -129,9 +126,9 @@ export default function Tracks({
}
}, [pendingLetterRef.current, tracksInfiniteQuery.data])
const handleScrollBeginDrag = useCallback(() => {
const handleScrollBeginDrag = () => {
closeAllSwipeableRows()
}, [])
}
return (
<XStack flex={1}>

View File

@@ -7,7 +7,7 @@ import { fetchMediaInfo } from '../api/queries/media/utils'
import { fetchAlbumDiscs, fetchItem } from '../api/queries/item'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import fetchUserData from '../api/queries/user-data/utils'
import { useCallback, useRef } from 'react'
import { useRef } from 'react'
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../stores/device-profile'
import UserDataQueryKey from '../api/queries/user-data/keys'
import MediaInfoQueryKey from '../api/queries/media/keys'
@@ -23,20 +23,17 @@ export default function useItemContext(): (item: BaseItemDto) => void {
const prefetchedContext = useRef<Set<string>>(new Set())
return useCallback(
(item: BaseItemDto) => {
const effectSig = `${item.Id}-${item.Type}`
return (item: BaseItemDto) => {
const effectSig = `${item.Id}-${item.Type}`
// If we've already warmed the cache for this item, return
if (prefetchedContext.current.has(effectSig)) return
// If we've already warmed the cache for this item, return
if (prefetchedContext.current.has(effectSig)) return
// Mark this item's context as warmed, preventing reruns
prefetchedContext.current.add(effectSig)
// Mark this item's context as warmed, preventing reruns
prefetchedContext.current.add(effectSig)
warmItemContext(api, user, item, streamingDeviceProfile, downloadingDeviceProfile)
},
[api, user, streamingDeviceProfile, downloadingDeviceProfile],
)
warmItemContext(api, user, item, streamingDeviceProfile, downloadingDeviceProfile)
}
}
function warmItemContext(

View File

@@ -2,7 +2,7 @@ import fetchSimilar from '../../api/queries/similar'
import { QueryKeys } from '../../enums/query-keys'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { useQuery } from '@tanstack/react-query'
import { createContext, ReactNode, useCallback, useContext, useMemo } from 'react'
import { createContext, ReactNode, useContext } from 'react'
import { SharedValue, useSharedValue } from 'react-native-reanimated'
import { isUndefined } from 'lodash'
import { useArtistAlbums, useArtistFeaturedOn } from '../../api/queries/artist'
@@ -65,38 +65,25 @@ export const ArtistProvider = ({
enabled: !isUndefined(artist.Id),
})
const refresh = useCallback(() => {
const refresh = () => {
refetchAlbums()
refetchFeaturedOn()
refetchSimilar()
}, [refetchAlbums, refetchFeaturedOn, refetchSimilar])
}
const scroll = useSharedValue(0)
const value = useMemo(
() => ({
artist,
albums,
featuredOn,
similarArtists,
fetchingAlbums,
fetchingFeaturedOn,
fetchingSimilarArtists,
refresh,
scroll,
}),
[
artist,
albums,
featuredOn,
similarArtists,
fetchingAlbums,
fetchingFeaturedOn,
fetchingSimilarArtists,
refresh,
scroll,
],
)
const value = {
artist,
albums,
featuredOn,
similarArtists,
fetchingAlbums,
fetchingFeaturedOn,
fetchingSimilarArtists,
refresh,
scroll,
}
return <ArtistContext.Provider value={value}>{children}</ArtistContext.Provider>
}

View File

@@ -1,4 +1,4 @@
import React, { createContext, ReactNode, useContext, useEffect, useState, useMemo } from 'react'
import React, { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { JellifyDownloadProgress } from '../../types/JellifyDownload'
import { saveAudio } from '../../api/mutations/download/offlineModeUtils'
import JellifyTrack from '../../types/JellifyTrack'
@@ -97,17 +97,7 @@ export const NetworkContextProvider: ({
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
const context = NetworkContextInitializer()
// Memoize the context value to prevent unnecessary re-renders
const value = useMemo(
() => context,
[
context.downloadedTracks?.length,
context.pendingDownloads.length,
context.downloadingDownloads.length,
context.completedDownloads.length,
context.failedDownloads.length,
],
)
const value = context
return <NetworkContext.Provider value={value}>{children}</NetworkContext.Provider>
}

View File

@@ -1,6 +1,6 @@
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
import { createContext, useCallback, useEffect, useState } from 'react'
import { createContext, useEffect, useState } from 'react'
import { handleActiveTrackChanged } from './functions'
import JellifyTrack from '../../types/JellifyTrack'
import { useAutoDownload } from '../../stores/settings/usage'
@@ -43,69 +43,61 @@ export const PlayerProvider: () => React.JSX.Element = () => {
usePerformanceMonitor('PlayerProvider', 3)
const eventHandler = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async (event: any) => {
switch (event.type) {
case Event.PlaybackActiveTrackChanged: {
// When we load a new queue, our index is updated before RNTP
// Because of this, we only need to respond to this event
// if the index from the event differs from what we have stored
if (event.track && enableAudioNormalization) {
const volume = calculateTrackVolume(event.track)
await TrackPlayer.setVolume(volume)
} else if (event.track) {
try {
await reportPlaybackStarted(api, event.track)
} catch (error) {
console.error('Unable to report playback started for track', error)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const eventHandler = async (event: any) => {
switch (event.type) {
case Event.PlaybackActiveTrackChanged: {
// When we load a new queue, our index is updated before RNTP
// Because of this, we only need to respond to this event
// if the index from the event differs from what we have stored
if (event.track && enableAudioNormalization) {
const volume = calculateTrackVolume(event.track)
await TrackPlayer.setVolume(volume)
} else if (event.track) {
try {
await reportPlaybackStarted(api, event.track)
} catch (error) {
console.error('Unable to report playback started for track', error)
}
await handleActiveTrackChanged()
if (event.lastTrack) {
try {
if (
isPlaybackFinished(
event.lastPosition,
event.lastTrack.duration ?? 1,
)
)
await reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
else await reportPlaybackStopped(api, event.lastTrack as JellifyTrack)
} catch (error) {
console.error('Unable to report playback stopped for lastTrack', error)
}
}
break
}
case Event.PlaybackProgressUpdated: {
const currentTrack = usePlayerQueueStore.getState().currentTrack
if (event.position / event.duration > 0.3 && autoDownload && currentTrack) {
await saveAudioItem(api, currentTrack.item, downloadingDeviceProfile, true)
}
break
}
case Event.PlaybackState: {
const currentTrack = usePlayerQueueStore.getState().currentTrack
switch (event.state) {
case State.Playing:
if (currentTrack) await reportPlaybackStarted(api, currentTrack)
break
default:
if (currentTrack) await reportPlaybackStopped(api, currentTrack)
break
await handleActiveTrackChanged()
if (event.lastTrack) {
try {
if (isPlaybackFinished(event.lastPosition, event.lastTrack.duration ?? 1))
await reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
else await reportPlaybackStopped(api, event.lastTrack as JellifyTrack)
} catch (error) {
console.error('Unable to report playback stopped for lastTrack', error)
}
break
}
break
}
},
[api, autoDownload, enableAudioNormalization],
)
case Event.PlaybackProgressUpdated: {
const currentTrack = usePlayerQueueStore.getState().currentTrack
if (event.position / event.duration > 0.3 && autoDownload && currentTrack) {
await saveAudioItem(api, currentTrack.item, downloadingDeviceProfile, true)
}
break
}
case Event.PlaybackState: {
const currentTrack = usePlayerQueueStore.getState().currentTrack
switch (event.state) {
case State.Playing:
if (currentTrack) await reportPlaybackStarted(api, currentTrack)
break
default:
if (currentTrack) await reportPlaybackStopped(api, currentTrack)
break
}
break
}
}
}
useTrackPlayerEvents(PLAYER_EVENTS, eventHandler)

View File

@@ -1,11 +1,4 @@
import React, {
PropsWithChildren,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
import React, { PropsWithChildren, createContext, useContext, useState } from 'react'
import { useAllDownloadedTracks, useStorageInUse } from '../../api/queries/download'
import { JellifyDownload, JellifyDownloadProgress } from '../../types/JellifyDownload'
import {
@@ -80,12 +73,9 @@ export function StorageProvider({ children }: PropsWithChildren): React.JSX.Elem
const [isDeleting, setIsDeleting] = useState(false)
const [isManuallyRefreshing, setIsManuallyRefreshing] = useState(false)
const activeDownloadsCount = useMemo(
() => Object.keys(activeDownloads ?? {}).length,
[activeDownloads],
)
const activeDownloadsCount = Object.keys(activeDownloads ?? {}).length
const summary = useMemo<StorageSummary | undefined>(() => {
const summary: StorageSummary | undefined = (() => {
if (!downloads || !storageInfo) return undefined
const audioBytes = downloads.reduce(
@@ -110,9 +100,9 @@ export function StorageProvider({ children }: PropsWithChildren): React.JSX.Elem
artworkBytes,
audioBytes,
}
}, [downloads, storageInfo])
})()
const suggestions = useMemo<CleanupSuggestion[]>(() => {
const suggestions: CleanupSuggestion[] = (() => {
if (!downloads || downloads.length === 0) return []
const now = Date.now()
@@ -168,86 +158,69 @@ export function StorageProvider({ children }: PropsWithChildren): React.JSX.Elem
})
return list
}, [downloads])
})()
const toggleSelection = useCallback((itemId: string) => {
const toggleSelection = (itemId: string) => {
setSelection((prev) => ({
...prev,
[itemId]: !prev[itemId],
}))
}, [])
}
const clearSelection = useCallback(() => setSelection({}), [])
const clearSelection = () => setSelection({})
const deleteDownloads = useCallback(
async (itemIds: string[]): Promise<DeleteDownloadsResult | undefined> => {
if (!itemIds.length) return undefined
setIsDeleting(true)
try {
const result = await deleteDownloadsByIds(itemIds)
await Promise.all([refetchDownloads(), refetchStorageInfo()])
setSelection((prev) => {
const updated = { ...prev }
itemIds.forEach((id) => delete updated[id])
return updated
})
return result
} finally {
setIsDeleting(false)
}
},
[refetchDownloads, refetchStorageInfo],
)
const deleteDownloads = async (
itemIds: string[],
): Promise<DeleteDownloadsResult | undefined> => {
if (!itemIds.length) return undefined
setIsDeleting(true)
try {
const result = await deleteDownloadsByIds(itemIds)
await Promise.all([refetchDownloads(), refetchStorageInfo()])
setSelection((prev) => {
const updated = { ...prev }
itemIds.forEach((id) => delete updated[id])
return updated
})
return result
} finally {
setIsDeleting(false)
}
}
const deleteSelection = useCallback(async () => {
const deleteSelection = async () => {
const idsToDelete = Object.entries(selection)
.filter(([, isSelected]) => isSelected)
.map(([id]) => id)
return deleteDownloads(idsToDelete)
}, [selection, deleteDownloads])
}
const refresh = useCallback(async () => {
const refresh = async () => {
setIsManuallyRefreshing(true)
try {
await Promise.all([refetchDownloads(), refetchStorageInfo()])
} finally {
setIsManuallyRefreshing(false)
}
}, [refetchDownloads, refetchStorageInfo])
}
const refreshing = isFetchingDownloads || isFetchingStorage || isManuallyRefreshing
const value = useMemo<StorageContextValue>(
() => ({
downloads,
summary,
suggestions,
selection,
toggleSelection,
clearSelection,
deleteSelection,
deleteDownloads,
isDeleting,
refresh,
refreshing,
activeDownloadsCount,
activeDownloads,
}),
[
downloads,
summary,
suggestions,
selection,
toggleSelection,
clearSelection,
deleteSelection,
deleteDownloads,
isDeleting,
refresh,
refreshing,
activeDownloadsCount,
],
)
const value: StorageContextValue = {
downloads,
summary,
suggestions,
selection,
toggleSelection,
clearSelection,
deleteSelection,
deleteDownloads,
isDeleting,
refresh,
refreshing,
activeDownloadsCount,
activeDownloads,
}
return <StorageContext.Provider value={value}>{children}</StorageContext.Provider>
}