setting query client persister, player backend changes to work with MMKV, react navigation theming

This commit is contained in:
Violet Caulfield
2024-11-29 08:49:53 -06:00
parent c22ecad73d
commit 7259c1c006
14 changed files with 227 additions and 102 deletions

15
App.tsx
View File

@@ -1,22 +1,25 @@
import './gesture-handler';
import React from 'react';
import "react-native-url-polyfill/auto";
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import Jellify from './components/jellify';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TamaguiProvider, Theme } from 'tamagui';
import { ToastProvider } from '@tamagui/toast'
import { useColorScheme } from 'react-native';
import jellifyConfig from './tamagui.config';
import { clientPersister } from './constants/storage';
import { queryClient } from './constants/query-client';
export default function App(): React.JSX.Element {
const queryClient = new QueryClient();
const isDarkMode = useColorScheme() === 'dark';
return (
<QueryClientProvider client={queryClient}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister: clientPersister
}}>
<TamaguiProvider config={jellifyConfig}>
<Theme name={isDarkMode ? 'dark' : 'light'}>
<ToastProvider
@@ -27,6 +30,6 @@ export default function App(): React.JSX.Element {
</ToastProvider>
</Theme>
</TamaguiProvider>
</QueryClientProvider>
</PersistQueryClientProvider>
);
}

View File

@@ -18,7 +18,7 @@ export default function Artist({ artistId, artistName }: { artistId: string, art
<FlatList
data={albums}
numColumns={3} // TODO: Make this adjustable
numColumns={2} // TODO: Make this adjustable
renderItem={({ item: album }) => {
return (
<Avatar itemId={album.Id!} subheading={album.Name}>

View File

@@ -1,6 +1,6 @@
import { ScrollView, YStack } from "tamagui";
import _ from "lodash";
import { H2, Text } from "../Global/text";
import { H2 } from "../Global/text";
import RecentlyPlayed from "./helpers/recently-played";
import { useApiClientContext } from "../jellyfin-api-provider";
import RecentArtists from "./helpers/recent-artists";
@@ -10,22 +10,20 @@ import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { HomeStackParamList, ProvidedHomeProps } from "./types";
import { HomeArtistScreen } from "./screens/artist";
import { SafeAreaView } from "react-native-safe-area-context";
import { Colors } from "../../enums/colors";
export const Stack = createNativeStackNavigator<HomeStackParamList>();
export const HomeStack = createNativeStackNavigator<HomeStackParamList>();
export default function Home(): React.JSX.Element {
return (
<HomeProvider>
<Stack.Navigator
<HomeStack.Navigator
id="Home"
initialRouteName="Home"
screenOptions={{
navigationBarColor: Colors.Primary
}}
>
<Stack.Screen
<HomeStack.Screen
name="Home"
component={ProvidedHome}
options={{
@@ -33,7 +31,7 @@ export default function Home(): React.JSX.Element {
}}
/>
<Stack.Screen
<HomeStack.Screen
name="Artist"
component={HomeArtistScreen}
options={({ route }) => ({
@@ -44,7 +42,7 @@ export default function Home(): React.JSX.Element {
}
})}
/>
</Stack.Navigator>
</HomeStack.Navigator>
</HomeProvider>
);
}

View File

@@ -8,6 +8,8 @@ import Navigation from "./navigation";
import Login from "./Login/component";
import { JellyfinAuthenticationProvider } from "./Login/provider";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { JellifyTheme } from "./theme";
import { PlayerProvider } from "../player/provider";
export default function Jellify(): React.JSX.Element {
@@ -22,15 +24,17 @@ export default function Jellify(): React.JSX.Element {
function App(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
// If library hasn't been set, we haven't completed the auth flow
const { library } = useApiClientContext();
return (
<NavigationContainer theme={isDarkMode ? DarkTheme : DefaultTheme}>
<NavigationContainer theme={JellifyTheme}>
<SafeAreaProvider>
{ library ? <Navigation /> : (
{ library ? (
<PlayerProvider>
<Navigation />
</PlayerProvider>
) : (
<JellyfinAuthenticationProvider>
<Login />
</JellyfinAuthenticationProvider>

10
components/theme.ts Normal file
View File

@@ -0,0 +1,10 @@
import { DarkTheme } from "@react-navigation/native";
import { Colors } from "../enums/colors";
export const JellifyTheme = {
...DarkTheme,
colors: {
...DarkTheme.colors,
primary: Colors.Primary,
},
};

View File

@@ -0,0 +1,9 @@
import { QueryClient } from "@tanstack/react-query";
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: (1000 * 60 * 24 * 24) * 5 // 5 days
}
}
})

View File

@@ -1,3 +1,19 @@
import { MMKV } from "react-native-mmkv";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
export const storage = new MMKV();
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);
},
};
export const clientPersister = createSyncStoragePersister({ storage: clientStorage });

62
package-lock.json generated
View File

@@ -19,7 +19,9 @@
"@react-navigation/stack": "^6.4.1",
"@tamagui/config": "^1.115.5",
"@tamagui/toast": "^1.115.5",
"@tanstack/query-sync-storage-persister": "^5.62.0",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-query-persist-client": "^5.62.0",
"burnt": "^0.12.2",
"lodash": "^4.17.21",
"react": "18.3.1",
@@ -8977,22 +8979,49 @@
}
},
"node_modules/@tanstack/query-core": {
"version": "5.59.9",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.9.tgz",
"integrity": "sha512-vFGnblfJOKlOPyTR5M0ohWKb/03eGubh5KuGyzsDfc7VQ6F0nsB75kQIoLpwp3Wfj6fKv0wGoTUX8BsIfhxDfw==",
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.0.tgz",
"integrity": "sha512-sx38bGrqF9bop92AXOvzDr0L9fWDas5zXdPglxa9cuqeVSWS7lY6OnVyl/oodfXjgOGRk79IfCpgVmxrbHuFHg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.59.9",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.9.tgz",
"integrity": "sha512-g2cbiw/ZIIrnUaQqhGtarTAsuLdKDNLtY5HNfRHVWY9kHDj96M4qs4ygJxHc119tPQpzZe4i9W7d2Gc2Gvng2A==",
"node_modules/@tanstack/query-persist-client-core": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.62.0.tgz",
"integrity": "sha512-yEDvJjVHqo+IizSjNkdu5fSL4ANOd16o8Tk2NOeaD7kGS1fXjf47j3XOXLIypZ7nmPTjZf9vyZi1WP3VB4saGg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.59.9"
"@tanstack/query-core": "5.62.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-sync-storage-persister": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.62.0.tgz",
"integrity": "sha512-VDTdldUrthWE4th6FjwziYgADaYkOzN2UbOw8gBZejVnGJrSnBZ/ZC+0SBv7Gy09TU5wKaDlT69RNk15dlmJ1g==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.62.0",
"@tanstack/query-persist-client-core": "5.62.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.0.tgz",
"integrity": "sha512-tj2ltjAn2a3fs+Dqonlvs6GyLQ/LKVJE2DVSYW+8pJ3P6/VCVGrfqv5UEchmlP7tLOvvtZcOuSyI2ooVlR5Yqw==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.62.0"
},
"funding": {
"type": "github",
@@ -9002,6 +9031,23 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "5.62.0",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.62.0.tgz",
"integrity": "sha512-yv7Oe0vNkGDTVFkq9bbdGQO9EoHwXYKIYuj845v1bOmK/Npvcov3yaS0AirOlNklsapDtr0v+x1ev2aJBIbm7A==",
"license": "MIT",
"dependencies": {
"@tanstack/query-persist-client-core": "5.62.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.62.0",
"react": "^18 || ^19"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",

View File

@@ -21,7 +21,9 @@
"@react-navigation/stack": "^6.4.1",
"@tamagui/config": "^1.115.5",
"@tamagui/toast": "^1.115.5",
"@tanstack/query-sync-storage-persister": "^5.62.0",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-query-persist-client": "^5.62.0",
"burnt": "^0.12.2",
"lodash": "^4.17.21",
"react": "18.3.1",

View File

@@ -1,23 +0,0 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { AsyncStorageKeys } from "../../../../enums/async-storage-keys";
import { JellifyTrack } from "../../../../types/JellifyTrack";
/**
* Stores the play queue for referencing in the UI and for loading at launch
* @param queue The queue of tracks to store
* @returns
*/
export async function storePlayQueue(queue: JellifyTrack[]) : Promise<void> {
return AsyncStorage.setItem(AsyncStorageKeys.PlayQueue, JSON.stringify(queue));
}
/**
* Fetches the stored play queue for referencing in the UI and for loading at launch
* @returns An array of the stored tracks or an empty array if nothing is stored
*/
export async function fetchPlayQueue() : Promise<JellifyTrack[]> {
let storedQueue = await AsyncStorage.getItem(AsyncStorageKeys.PlayQueue);
return storedQueue != null ? JSON.parse(storedQueue as string) : [];
}

View File

@@ -3,20 +3,17 @@
*/
import { useMutation } from "@tanstack/react-query";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { MMKVStorageKeys } from "../../enums/mmkv-storage-keys";
import { JellifyTrack } from "../../types/JellifyTrack";
import { add, getQueue, remove, removeUpcomingTracks } from "react-native-track-player/lib/src/trackPlayer";
import { fetchPlayQueue, storePlayQueue } from "./helpers/storage";
import { add, remove, removeUpcomingTracks } from "react-native-track-player/lib/src/trackPlayer";
import { findPlayNextIndexStart, findPlayQueueIndexStart } from "./helpers";
import { usePlayerContext } from "../provider";
/**
*
*/
export const addToPlayNext = useMutation({
mutationFn: async (tracks: JellifyTrack[]) => {
let playQueue = await fetchPlayQueue();
const { queue : playQueue, setQueue } = usePlayerContext();
let insertIndex = findPlayNextIndexStart(playQueue);
add(tracks, insertIndex);
@@ -25,7 +22,7 @@ export const addToPlayNext = useMutation({
playQueue.splice(insertIndex, 0, track);
});
await storePlayQueue(playQueue)
setQueue(playQueue)
}
});
@@ -35,7 +32,7 @@ export const addToPlayNext = useMutation({
export const addToPlayQueue = useMutation({
mutationFn: async (tracks: JellifyTrack[]) => {
let playQueue = await fetchPlayQueue();
const { queue : playQueue, setQueue } = usePlayerContext();
let insertIndex = findPlayQueueIndexStart(playQueue);
add(tracks, insertIndex);
@@ -44,7 +41,7 @@ export const addToPlayQueue = useMutation({
playQueue.splice(insertIndex, 0, track);
});
await storePlayQueue(playQueue)
setQueue(playQueue)
}
});
@@ -52,20 +49,12 @@ export const removeFromPlayQueue = useMutation({
mutationFn: async (indexes: number[]) => {
// Remove from the player first thing
remove(indexes);
let cachedQueue = await AsyncStorage.getItem(MMKVStorageKeys.PlayQueue);
let { queue, setQueue } = usePlayerContext();
if (cachedQueue === null) {
// Warn, not a showstopper as we'll just cache it at the end of this, this should hopefully never happen
console.warn("Queue cache was null, setting...");
storePlayQueue((await getQueue()) as JellifyTrack[]);
} else {
let queue : Array<JellifyTrack> = JSON.parse(cachedQueue);
indexes.forEach(index => {
queue.splice(index, 1); // Returns deleted queue items
})
storePlayQueue(queue);
}
indexes.forEach(index => {
queue.splice(index, 1); // Returns deleted queue items
})
setQueue(queue);
},
onError: () => {
@@ -80,7 +69,8 @@ export const removeFromPlayQueue = useMutation({
export const clearPlayQueue = useMutation({
mutationFn: async () => {
const { setQueue } = usePlayerContext();
removeUpcomingTracks()
await storePlayQueue([]);
setQueue([]);
}
})

64
player/provider.tsx Normal file
View File

@@ -0,0 +1,64 @@
import { createContext, ReactNode, SetStateAction, useContext, useState } from "react";
import { JellifyTrack } from "../types/JellifyTrack";
import { storage } from "../constants/storage";
import { MMKVStorageKeys } from "../enums/mmkv-storage-keys";
interface PlayerContext {
showPlayer: boolean;
setShowPlayer: React.Dispatch<SetStateAction<boolean>>;
showMiniplayer: boolean;
setShowMiniplayer: React.Dispatch<SetStateAction<boolean>>;
queue: JellifyTrack[];
setQueue: React.Dispatch<SetStateAction<JellifyTrack[]>>;
}
const PlayerContextInitializer = () => {
const queueJson = storage.getString(MMKVStorageKeys.PlayQueue);
const [showPlayer, setShowPlayer] = useState<boolean>(false);
const [showMiniplayer, setShowMiniplayer] = useState<boolean>(false);
const [queue, setQueue] = useState<JellifyTrack[]>(queueJson ? JSON.parse(queueJson) : []);
return {
showPlayer,
setShowPlayer,
showMiniplayer,
setShowMiniplayer,
queue,
setQueue
}
}
export const PlayerContext = createContext<PlayerContext>({
showPlayer: false,
setShowPlayer: () => {},
showMiniplayer: false,
setShowMiniplayer: () => {},
queue: [],
setQueue: () => {}
});
export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
const {
showPlayer,
setShowPlayer,
showMiniplayer,
setShowMiniplayer,
queue,
setQueue
} = PlayerContextInitializer();
return <PlayerContext.Provider value={{
showPlayer,
setShowPlayer,
showMiniplayer,
setShowMiniplayer,
queue,
setQueue
}}>
{ children }
</PlayerContext.Provider>
}
export const usePlayerContext = () => useContext(PlayerContext);

View File

@@ -1,9 +1,10 @@
import { useQueries, useQuery, UseQueryOptions, UseQueryResult } from "@tanstack/react-query"
import { getActiveTrack, getProgress, pause, play, removeUpcomingTracks, setupPlayer } from "react-native-track-player/lib/src/trackPlayer"
import { getActiveTrack, getProgress, pause, play } from "react-native-track-player/lib/src/trackPlayer"
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api/playstate-api"
import { useApi } from "../../api/queries";
import { Progress, Track } from "react-native-track-player";
import { QueryKeys } from "../../enums/query-keys";
import { Api } from "@jellyfin/sdk";
import { useApiClientContext } from "../../components/jellyfin-api-provider";
const usePause : UseQueryOptions = {
queryKey: [QueryKeys.Pause],
@@ -26,11 +27,11 @@ const useProgress : UseQueryResult<Progress, Error> = useQuery({
.then((progress => {
if (!!!progress)
throw new Error("Tried to fetch progress when there wasn't a currently active track");
}))
}));
}
})
});
const useReportPlaybackStarted : UseQueryOptions = {
const useReportPlaybackStarted = {
queryKey: [QueryKeys.ReportPlaybackStarted],
queryFn: () => {
getActiveTrack()
@@ -41,12 +42,18 @@ const useReportPlaybackStarted : UseQueryOptions = {
return track as Track;
})
.then(track => {
getPlaystateApi(useApi.data!).reportPlaybackStart({playbackStartInfo: { ItemId: track.id, PositionTicks: useProgress.data!.position }})
})
getPlaystateApi(useApiClientContext().apiClient!)
.reportPlaybackStart({
playbackStartInfo: {
ItemId: track.id,
PositionTicks: useProgress.data!.position
}
});
});
}
}
const useReportPlaybackStopped : UseQueryOptions = {
const useReportPlaybackStopped = {
queryKey: [QueryKeys.ReportPlaybackStopped],
queryFn: () => {
getActiveTrack()
@@ -57,12 +64,18 @@ const useReportPlaybackStopped : UseQueryOptions = {
return track as Track;
})
.then(track => {
getPlaystateApi(useApi.data!).reportPlaybackStopped({playbackStopInfo: { ItemId: track.id, PositionTicks: useProgress.data!.position }})
})
getPlaystateApi(useApiClientContext().apiClient!)
.reportPlaybackStopped({
playbackStopInfo: {
ItemId: track.id,
PositionTicks: useProgress.data!.position
}
});
});
}
}
export const useReportPlaybackProgress : UseQueryResult = useQuery({
export const useReportPlaybackProgress = {
queryKey: [QueryKeys.ReportPlaybackPosition],
queryFn: () => {
getActiveTrack()
@@ -73,15 +86,21 @@ export const useReportPlaybackProgress : UseQueryResult = useQuery({
return track as Track;
})
.then(track => {
getPlaystateApi(useApi.data!).reportPlaybackProgress({playbackProgressInfo: { ItemId: track.id, PositionTicks: useProgress.data!.position }})
})
getPlaystateApi(useApiClientContext().apiClient!)
.reportPlaybackProgress({
playbackProgressInfo: {
ItemId: track.id,
PositionTicks: useProgress.data!.position
}
});
});
}
});
};
export const usePauseAndReportPlaybackStopped = useQueries({
queries: [useReportPlaybackStopped, usePause]
})
export const usePlayAndReportPlayback = useQueries({
export const usePlayAndReportPlayback = (api: Api) => useQueries({
queries: [useReportPlaybackStarted, usePlay]
})

View File

@@ -1,13 +0,0 @@
import { useQuery, UseQueryResult } from "@tanstack/react-query";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { MMKVStorageKeys } from "../../enums/mmkv-storage-keys";
import { JellifyTrack } from "../../types/JellifyTrack";
import { QueryKeys } from "../../enums/query-keys";
export const useStoredQueue: UseQueryResult<JellifyTrack[]> = useQuery({
queryKey: [QueryKeys.PlayQueue],
queryFn: (() => {
return AsyncStorage.getItem(MMKVStorageKeys.PlayQueue);
})
});