diff --git a/babel.config.js b/babel.config.js index 8b0276c4..af461387 100644 --- a/babel.config.js +++ b/babel.config.js @@ -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', + ], } diff --git a/bun.lock b/bun.lock index 6a4a7697..01ec36f7 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 83538529..db800bac 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Context/index.tsx b/src/components/Context/index.tsx index 3410d1c9..63de2abe 100644 --- a/src/components/Context/index.tsx +++ b/src/components/Context/index.tsx @@ -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 ? ( { + const goToAlbum = () => { if (stackNavigation && album) stackNavigation.navigate('Album', { album }) else goToAlbumFromContextSheet(album) - }, [album, stackNavigation, navigationRef]) + } return ( { - 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 ? ( 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 ( @@ -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 @@ -87,7 +87,7 @@ export function PlayPauseIcon(): React.JSX.Element { return } } - }, [state, togglePlayback]) + })() return button } diff --git a/src/components/Player/components/header.tsx b/src/components/Player/components/header.tsx index 91d69903..d5a87d34 100644 --- a/src/components/Player/components/header.tsx +++ b/src/components/Player/components/header.tsx @@ -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 ( @@ -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 ( { - 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 ( diff --git a/src/components/Player/mini-player.tsx b/src/components/Player/mini-player.tsx index 25b44471..44b3e931 100644 --- a/src/components/Player/mini-player.tsx +++ b/src/components/Player/mini-player.tsx @@ -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 ( diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index db05d45c..644cccc0 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -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 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 ( >() - const handleScrollBeginDrag = useCallback(() => { + const handleScrollBeginDrag = () => { closeAllSwipeableRows() - }, []) + } return ( (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' ? ( - - ) : typeof track === 'number' ? null : typeof track === 'object' ? ( - - ) : null, - [tracksToDisplay, queue, navigation, queue], - ) + const renderItem = ({ + item: track, + index, + }: { + index: number + item: string | number | BaseItemDto + }) => + typeof track === 'string' ? ( + + ) : typeof track === 'number' ? null : typeof track === 'object' ? ( + + ) : null - const ItemSeparatorComponent = useCallback( - ({ leadingItem, trailingItem }: { leadingItem: unknown; trailingItem: unknown }) => - typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : ( - - ), - [], - ) + const ItemSeparatorComponent = ({ + leadingItem, + trailingItem, + }: { + leadingItem: unknown + trailingItem: unknown + }) => + typeof leadingItem === 'string' || typeof trailingItem === 'string' ? null : // 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 ( diff --git a/src/hooks/use-item-context.ts b/src/hooks/use-item-context.ts index 71a1fe71..43870611 100644 --- a/src/hooks/use-item-context.ts +++ b/src/hooks/use-item-context.ts @@ -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>(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( diff --git a/src/providers/Artist/index.tsx b/src/providers/Artist/index.tsx index 5c610a84..0dea031c 100644 --- a/src/providers/Artist/index.tsx +++ b/src/providers/Artist/index.tsx @@ -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 {children} } diff --git a/src/providers/Network/index.tsx b/src/providers/Network/index.tsx index bb932383..64a2fcd7 100644 --- a/src/providers/Network/index.tsx +++ b/src/providers/Network/index.tsx @@ -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 {children} } diff --git a/src/providers/Player/index.tsx b/src/providers/Player/index.tsx index f5cac90c..6154b38c 100644 --- a/src/providers/Player/index.tsx +++ b/src/providers/Player/index.tsx @@ -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) diff --git a/src/providers/Storage/index.tsx b/src/providers/Storage/index.tsx index 54c7ac4d..d34a4918 100644 --- a/src/providers/Storage/index.tsx +++ b/src/providers/Storage/index.tsx @@ -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(() => { + 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(() => { + 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 => { - 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 => { + 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( - () => ({ - 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 {children} }