Merge pull request #134 from anultravioletaurora/59-improve-onboarding-experience

Memory Spillage Fixes (Again)
This commit is contained in:
Violet Caulfield
2025-02-15 09:26:40 -06:00
committed by GitHub
24 changed files with 162 additions and 184 deletions
-1
View File
@@ -9,7 +9,6 @@ import jellifyConfig from './tamagui.config';
import { clientPersister } from './constants/storage';
import { queryClient } from './constants/query-client';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { createWorkletRuntime } from 'react-native-reanimated';
// export const backgroundRuntime = createWorkletRuntime('background');
-2
View File
@@ -13,6 +13,4 @@ export const useApi = (serverUrl?: string, username?: string, password?: string,
return createApi(serverUrl, username, password, accessToken)
},
gcTime: 1000,
refetchInterval: false
})
-30
View File
@@ -1,30 +0,0 @@
import { QueryKeys } from "../../enums/query-keys";
import { useQuery } from "@tanstack/react-query";
import { fetchMusicLibraries, fetchPlaylistLibrary, fetchUserViews } from "./functions/libraries";
/**
* @deprecated use {@link useUserViews} instead as that will respect user permissions
* @returns
*/
export const useMusicLibraries = () => useQuery({
queryKey: [QueryKeys.Libraries],
queryFn: () => fetchMusicLibraries()
});
/**
* @deprecated use {@link useUserViews} instead as that will respect user permissions
* @returns
*/
export const usePlaylistLibrary = () => useQuery({
queryKey: [QueryKeys.Playlist],
queryFn: () => fetchPlaylistLibrary()
});
/**
*
* @returns
*/
export const useUserViews = () => useQuery({
queryKey: [QueryKeys.UserViews],
queryFn: () => fetchUserViews()
});
@@ -1,7 +1,6 @@
import { StackParamList } from "../types";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { HomeAlbumProps } from "../types";
import { YStack, XStack, Separator } from "tamagui";
import { BaseItemDto, ItemSortBy } from "@jellyfin/sdk/lib/generated-client/models";
import { ItemSortBy } from "@jellyfin/sdk/lib/generated-client/models";
import { H3, H5, Text } from "../Global/helpers/text";
import { FlatList } from "react-native";
import { RunTimeTicks } from "../Global/helpers/time-codes";
@@ -14,16 +13,15 @@ import { useQuery } from "@tanstack/react-query";
import { QueryKeys } from "../../enums/query-keys";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import Client from "../../api/client";
import { useMemo } from "react";
interface AlbumProps {
album: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>;
}
export default function Album({
album,
navigation
}: AlbumProps): React.JSX.Element {
export function AlbumScreen({
route,
navigation
} : HomeAlbumProps): React.JSX.Element {
const { album } = route.params;
navigation.setOptions({
headerRight: () => {
@@ -60,24 +58,31 @@ export default function Album({
<FlatList
contentInsetAdjustmentBehavior="automatic"
data={tracks}
keyExtractor={(item) => item.Id!}
numColumns={1}
ItemSeparatorComponent={() => <Separator />}
ListHeaderComponent={() => (
<YStack
alignItems="center"
alignContent="center"
marginTop={"$4"}
minHeight={width / 1.1}
>
<BlurhashedImage
item={album}
width={width / 1.1}
height={width / 1.1}
/>
ListHeaderComponent={(
useMemo(() => {
return (
<YStack
alignItems="center"
alignContent="center"
marginTop={"$4"}
minHeight={width / 1.1}
>
<BlurhashedImage
item={album}
width={width / 1.1}
height={width / 1.1}
/>
<H5>{ album.Name ?? "Untitled Album" }</H5>
<Text>{ album.ProductionYear?.toString() ?? "" }</Text>
</YStack>
<H5>{ album.Name ?? "Untitled Album" }</H5>
<Text>{ album.ProductionYear?.toString() ?? "" }</Text>
</YStack>
)
}, [
album
])
)}
renderItem={({ item: track, index }) => {
@@ -88,11 +93,12 @@ export default function Album({
tracklist={tracks!}
index={index}
navigation={navigation}
queue={album}
/>
)
}}
ListFooterComponent={() => (
ListFooterComponent={(
<YStack justifyContent="flex-start">
<XStack flex={1} marginTop={"$3"} justifyContent="flex-end">
<Text
@@ -108,6 +114,7 @@ export default function Album({
<H3>Album Artists</H3>
<FlatList
horizontal
keyExtractor={(item) => item.Id!}
data={album.ArtistItems}
renderItem={({ index, item: artist }) => {
return (
@@ -132,6 +139,5 @@ export default function Album({
// overflow: 'hidden' // Prevent unnecessary memory usage
// }}
/>
)
}
-14
View File
@@ -1,14 +0,0 @@
import { RouteProp } from "@react-navigation/native";
import Album from "../component";
import { StackParamList } from "../../types";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
export function AlbumScreen({ route, navigation } : { route: RouteProp<StackParamList, "Album">, navigation: NativeStackNavigationProp<StackParamList> }): React.JSX.Element {
return (
<Album
album={route.params.album }
navigation={navigation}
/>
)
}
@@ -26,13 +26,14 @@ export default function BlurhashedImage({
const { data: image, isSuccess } = useQuery({
queryKey: [
QueryKeys.ItemImage,
item.Id!,
item.AlbumId ? item.AlbumId : item.Id!,
type ?? ImageType.Primary,
Math.ceil(width / 100) * 100, // Images are fetched at a higher, generic resolution
Math.ceil(height ?? width / 100) * 100 // So these keys need to match
],
queryFn: () => fetchItemImage(item.Id!, type ?? ImageType.Primary, width, height ?? width),
staleTime: (1000 * 60 * 60) // 1 hour,
queryFn: () => fetchItemImage(item.AlbumId ? item.AlbumId : item.Id!, type ?? ImageType.Primary, width, height ?? width),
staleTime: (1000 * 60 * 60) * 24, // 1 day, images probably don't refresh that often
gcTime: (1000 * 1 * 1) * 1 // 1 second, these are stored on disk anyways so refetching is cheap
});;
const blurhash = !isEmpty(item.ImageBlurHashes)
+1 -1
View File
@@ -52,7 +52,7 @@ export default function Item({
usePlayNewQueue.mutate({
track: item,
tracklist: [item],
queueName,
queue: "Search",
queuingType: QueuingType.FromSelection
})
break;
+2 -2
View File
@@ -17,7 +17,7 @@ interface TrackProps {
navigation: NativeStackNavigationProp<StackParamList>;
tracklist?: BaseItemDto[] | undefined;
index?: number | undefined;
queue?: Queue;
queue: Queue;
showArtwork?: boolean | undefined;
onPress?: () => void | undefined;
onLongPress?: () => void | undefined;
@@ -64,7 +64,7 @@ export default function Track({
track,
index,
tracklist: tracklist ?? playQueue.map((track) => track.item),
queue: queue ? queue : "Queue",
queue,
queuingType: QueuingType.FromSelection
});
}
+10 -7
View File
@@ -9,6 +9,7 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { trigger } from "react-native-haptic-feedback";
import { QueuingType } from "../../../enums/queuing-type";
import Icon from "../../../components/Global/helpers/icon";
import { FlatList } from "react-native";
export default function RecentlyPlayed({
navigation
@@ -42,11 +43,10 @@ export default function RecentlyPlayed({
</YStack>
)}
</XStack>
<ScrollView
<FlatList
horizontal
removeClippedSubviews // Save memory usage
>
{ recentTracks && recentTracks.map((recentlyPlayedTrack, index) => {
data={recentTracks}
renderItem={({ index, item: recentlyPlayedTrack }) => {
return (
<ItemCard
caption={recentlyPlayedTrack.Name}
@@ -58,7 +58,7 @@ export default function RecentlyPlayed({
usePlayNewQueue.mutate({
track: recentlyPlayedTrack,
index: index,
tracklist: recentTracks,
tracklist: recentTracks ?? [recentlyPlayedTrack],
queue: "Recently Played",
queuingType: QueuingType.FromSelection
});
@@ -72,8 +72,11 @@ export default function RecentlyPlayed({
}}
/>
)
})}
</ScrollView>
}}
style={{
overflow: 'hidden'
}}
/>
</View>
)
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { HomeProvider } from "./provider";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { StackParamList } from "../types";
import { ArtistScreen } from "../Artist/screens";
import { AlbumScreen } from "../Album/screens";
import { AlbumScreen } from "../Album";
import { PlaylistScreen } from "../Playlist/screens";
import { ProvidedHome } from "./component";
import DetailsScreen from "../ItemDetail/screen";
+16 -4
View File
@@ -73,7 +73,7 @@ export default function TrackOptions({
<YStack width={width}>
<XStack justifyContent="space-evenly">
{ albumFetchSuccess ? (
{ albumFetchSuccess && album ? (
<IconButton
name="music-box"
title="Go to Album"
@@ -83,9 +83,21 @@ export default function TrackOptions({
navigation.goBack();
navigation.goBack();
navigation.navigate("Album", {
album
});
if (isNested)
navigation.navigate('Tabs', {
screen: 'Home',
params: {
screen: 'Album',
params: {
album
}
}
});
else
navigation.navigate('Album', {
album
});
}}
size={width / 6}
/>
+1 -1
View File
@@ -3,7 +3,7 @@ import React from "react";
import { StackParamList } from "../types";
import Library from "./component";
import { ArtistScreen } from "../Artist/screens";
import { AlbumScreen } from "../Album/screens";
import { AlbumScreen } from "../Album";
import { PlaylistScreen } from "../Playlist/screens";
import ArtistsScreen from "../Artists/screen";
import AlbumsScreen from "../Albums/screen";
+7 -2
View File
@@ -4,11 +4,13 @@ import { useAuthenticationContext } from "../provider";
import { H1, H2, Label, Text } from "../../Global/helpers/text";
import Button from "../../Global/helpers/button";
import _ from "lodash";
import { useUserViews } from "../../../api/queries/libraries";
import { SafeAreaView } from "react-native-safe-area-context";
import Client from "../../../api/client";
import { useJellifyContext } from "../../provider";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { QueryKeys } from "../../../enums/query-keys";
import { fetchUserViews } from "../../../api/queries/functions/libraries";
import { useQuery } from "@tanstack/react-query";
export default function ServerLibrary(): React.JSX.Element {
@@ -19,7 +21,10 @@ export default function ServerLibrary(): React.JSX.Element {
const [libraryId, setLibraryId] = useState<string | undefined>(undefined);
const [playlistLibrary, setPlaylistLibrary] = useState<BaseItemDto | undefined>(undefined);
const { data : libraries, isError, isPending, isSuccess, refetch } = useUserViews();
const { data : libraries, isError, isPending, isSuccess, refetch } = useQuery({
queryKey: [QueryKeys.UserViews],
queryFn: () => fetchUserViews()
});;
useEffect(() => {
if (!isPending && isSuccess)
+4 -3
View File
@@ -12,7 +12,7 @@ import { FadeIn, FadeOut, ReduceMotion, SequencedTransition } from "react-native
export default function Queue({ navigation }: { navigation: NativeStackNavigationProp<StackParamList>}): React.JSX.Element {
const { width } = useSafeAreaFrame();
const { playQueue: queue, useClearQueue, useRemoveFromQueue, useReorderQueue, useSkip, nowPlaying } = usePlayerContext();
const { playQueue, queue, useClearQueue, useRemoveFromQueue, useReorderQueue, useSkip, nowPlaying } = usePlayerContext();
navigation.setOptions({
headerRight: () => {
@@ -24,12 +24,12 @@ export default function Queue({ navigation }: { navigation: NativeStackNavigatio
}
})
const scrollIndex = queue.findIndex(queueItem => queueItem.item.Id! === nowPlaying!.item.Id!)
const scrollIndex = playQueue.findIndex(queueItem => queueItem.item.Id! === nowPlaying!.item.Id!)
return (
<DraggableFlatList
contentInsetAdjustmentBehavior="automatic"
data={queue}
data={playQueue}
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
extraData={nowPlaying}
// enableLayoutAnimationExperimental
@@ -54,6 +54,7 @@ export default function Queue({ navigation }: { navigation: NativeStackNavigatio
return (
<Track
queue={queue}
navigation={navigation}
track={queueItem.item}
index={getIndex()}
+1 -1
View File
@@ -4,7 +4,7 @@ import { StackParamList } from "../types";
import PlayerScreen from "./screens";
import Queue from "./screens/queue";
import DetailsScreen from "../ItemDetail/screen";
import { AlbumScreen } from "../Album/screens";
import { AlbumScreen } from "../Album";
export const PlayerStack = createNativeStackNavigator<StackParamList>();
+1 -1
View File
@@ -2,7 +2,7 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack"
import SearchScreen from "./screen";
import { StackParamList } from "../types";
import { ArtistScreen } from "../Artist/screens";
import { AlbumScreen } from "../Album/screens";
import { AlbumScreen } from "../Album";
import { PlaylistScreen } from "../Playlist/screens";
import DetailsScreen from "../ItemDetail/screen";
+1 -1
View File
@@ -31,7 +31,7 @@ export default function Tracks({ navigation }: { navigation: NativeStackNavigati
showArtwork
track={track}
tracklist={tracks?.slice(index, index + 50) ?? []}
queue="Queue"
queue="Favorite Tracks"
/>
)
+40 -1
View File
@@ -12,14 +12,53 @@ import { PortalProvider } from "@tamagui/portal";
import { JellifyProvider, useJellifyContext } from "./provider";
import { ToastProvider, ToastViewport } from "@tamagui/toast";
import SafeToastViewport from "./Global/components/toast-area-view-port";
import { QueryKeys } from "../enums/query-keys";
import { useQuery } from "@tanstack/react-query";
import TrackPlayer, { IOSCategory, IOSCategoryOptions } from "react-native-track-player";
import { CAPABILITIES } from "../player/constants";
export default function Jellify(): React.JSX.Element {
const { isSuccess: isPlayerReady } = useQuery({
queryKey: [QueryKeys.Player],
queryFn: async () => {
await TrackPlayer.setupPlayer({
autoHandleInterruptions: true,
maxCacheSize: 1000 * 100, // 100MB, TODO make this adjustable
iosCategory: IOSCategory.Playback,
iosCategoryOptions: [
IOSCategoryOptions.AllowAirPlay,
IOSCategoryOptions.AllowBluetooth,
]
});
return await TrackPlayer.updateOptions({
progressUpdateEventInterval: 1,
capabilities: CAPABILITIES,
notificationCapabilities: CAPABILITIES,
compactCapabilities: CAPABILITIES,
// ratingType: RatingType.Heart,
// likeOptions: {
// isActive: false,
// title: "Favorite"
// },
// dislikeOptions: {
// isActive: true,
// title: "Unfavorite"
// }
});
},
retry: 0,
// staleTime: 1000 * 60 * 60 * 24 * 7 // 7 days
});
return (
<PortalProvider shouldAddRootHost>
<ToastProvider burntOptions={{ from: 'top'}}>
<JellifyProvider>
<App />
{ isPlayerReady && (
<App />
)}
</JellifyProvider>
</ToastProvider>
</PortalProvider>
-1
View File
@@ -32,7 +32,6 @@ export function Tabs() : React.JSX.Element {
<>
<Separator />
<Miniplayer navigation={props.navigation} />
<Separator />
</>
)}
<BottomTabBar {...props} />
+14 -19
View File
@@ -1,26 +1,21 @@
import { MMKV } from "react-native-mmkv";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
import { persistQueryClient } from "@tanstack/react-query-persist-client";
import { queryClient } from "./query-client";
export const storage = new MMKV();
const clientStorage = {
setItem: (key: string, value: string | number | boolean | Uint8Array) => {
storage.set(key, value);
},
getItem: (key: string) => {
const value = storage.getString(key);
return value === undefined ? null : value;
},
removeItem: (key: string) => {
storage.delete(key);
},
};
setItem: (key: string, value: string | number | boolean | Uint8Array) => {
storage.set(key, value);
},
getItem: (key: string) => {
const value = storage.getString(key);
return value === undefined ? null : value;
},
removeItem: (key: string) => {
storage.delete(key);
},
};
export const clientPersister = createSyncStoragePersister({ storage: clientStorage });
persistQueryClient({
queryClient,
persister: clientPersister
});
export const clientPersister = createSyncStoragePersister({
storage: clientStorage,
});
+14
View File
@@ -0,0 +1,14 @@
import { Capability } from "react-native-track-player";
export const CAPABILITIES: Capability[] = [
Capability.Pause,
Capability.Play,
Capability.PlayFromId,
Capability.SeekTo,
// Capability.JumpForward,
// Capability.JumpBackward,
Capability.SkipToNext,
Capability.SkipToPrevious,
// Capability.Like,
// Capability.Dislike
]
+2 -46
View File
@@ -1,49 +1,5 @@
import { QueryKeys } from "../../enums/query-keys"
import { useQuery } from "@tanstack/react-query"
import TrackPlayer, { Capability, IOSCategory, IOSCategoryOptions, RatingType } from "react-native-track-player"
const CAPABILITIES: Capability[] = [
Capability.Pause,
Capability.Play,
Capability.PlayFromId,
Capability.SeekTo,
// Capability.JumpForward,
// Capability.JumpBackward,
Capability.SkipToNext,
Capability.SkipToPrevious,
// Capability.Like,
// Capability.Dislike
]
export const useSetupPlayer = () => useQuery({
queryKey: [QueryKeys.Player],
queryFn: async () => {
await TrackPlayer.setupPlayer({
autoHandleInterruptions: true,
maxCacheSize: 1000 * 100, // 100MB, TODO make this adjustable
iosCategory: IOSCategory.Playback,
iosCategoryOptions: [
IOSCategoryOptions.AllowAirPlay,
IOSCategoryOptions.AllowBluetooth,
]
})
return await TrackPlayer.updateOptions({
progressUpdateEventInterval: 1,
capabilities: CAPABILITIES,
notificationCapabilities: CAPABILITIES,
compactCapabilities: CAPABILITIES,
// ratingType: RatingType.Heart,
// likeOptions: {
// isActive: false,
// title: "Favorite"
// },
// dislikeOptions: {
// isActive: true,
// title: "Unfavorite"
// }
});
}
});
import TrackPlayer, { RatingType } from "react-native-track-player"
import { CAPABILITIES } from "../constants";
export const useUpdateOptions = async (isFavorite: boolean) => {
return await TrackPlayer.updateOptions({
+8 -14
View File
@@ -3,13 +3,13 @@ import { JellifyTrack } from "../types/JellifyTrack";
import { storage } from "../constants/storage";
import { MMKVStorageKeys } from "../enums/mmkv-storage-keys";
import { findPlayNextIndexStart, findPlayQueueIndexStart } from "./helpers/index";
import TrackPlayer, { Event, Progress, State, usePlaybackState, useProgress, useTrackPlayerEvents } from "react-native-track-player";
import TrackPlayer, { Event, IOSCategory, IOSCategoryOptions, Progress, State, usePlaybackState, useProgress, useTrackPlayerEvents } from "react-native-track-player";
import { isEqual, isUndefined } from "lodash";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
import { handlePlaybackProgressUpdated, handlePlaybackState } from "./handlers";
import { useSetupPlayer, useUpdateOptions } from "../player/hooks";
import { useUpdateOptions } from "../player/hooks";
import { UPDATE_INTERVAL } from "./config";
import { useMutation, UseMutationResult } from "@tanstack/react-query";
import { useMutation, UseMutationResult, useQuery } from "@tanstack/react-query";
import { mapDtoToTrack } from "../helpers/mappings";
import { QueuingType } from "../enums/queuing-type";
import { trigger } from "react-native-haptic-feedback";
@@ -21,6 +21,8 @@ import { Section } from "../components/Player/types";
import { Queue } from "./types/queue-item";
import { markItemPlayed } from "../api/mutations/functions/item";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { QueryKeys } from "../enums/query-keys";
import { CAPABILITIES } from "./constants";
interface PlayerContext {
initialized: boolean;
@@ -234,7 +236,7 @@ const PlayerContextInitializer = () => {
//#endregion
//#region RNTP Setup
const isPlayerReady = useSetupPlayer().isSuccess;
const { state: playbackState } = usePlaybackState();
const progress = useProgress(UPDATE_INTERVAL);
@@ -298,21 +300,13 @@ const PlayerContextInitializer = () => {
}
});
useEffect(() => {
if (isPlayerReady)
console.debug("Player is ready")
else
console.warn("Player could not be setup")
}, [
isPlayerReady
])
//#endregion RNTP Setup
//#region useEffects
useEffect(() => {
storage.set(MMKVStorageKeys.Queue, JSON.stringify(playQueue))
storage.set(MMKVStorageKeys.Queue, JSON.stringify(queue))
}, [
playQueue
queue
])
useEffect(() => {
+1 -1
View File
@@ -1,3 +1,3 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
export type Queue = BaseItemDto | "Recently Played" | "Queue";
export type Queue = BaseItemDto | "Recently Played" | "Search" | "Favorite Tracks";