diff --git a/App.tsx b/App.tsx
index 6e236d1e..70b68377 100644
--- a/App.tsx
+++ b/App.tsx
@@ -6,7 +6,7 @@ import Jellify from './src/components/jellify'
import { TamaguiProvider } from 'tamagui'
import { Platform, useColorScheme } from 'react-native'
import jellifyConfig from './tamagui.config'
-import { clientPersister } from './src/constants/storage'
+import { queryClientPersister } from './src/constants/storage'
import { ONE_DAY, queryClient } from './src/constants/query-client'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import TrackPlayer, {
@@ -22,9 +22,9 @@ import { requestStoragePermission } from './src/utils/permisson-helpers'
import ErrorBoundary from './src/components/ErrorBoundary'
import OTAUpdateScreen from './src/components/OtaUpdates'
import { usePerformanceMonitor } from './src/hooks/use-performance-monitor'
-import { SettingsProvider, useThemeSettingContext } from './src/providers/Settings'
import navigationRef from './navigation'
import { PROGRESS_UPDATE_EVENT_INTERVAL } from './src/player/config'
+import { useThemeSetting } from './src/stores/settings/app'
export default function App(): React.JSX.Element {
// Add performance monitoring to track app-level re-renders
@@ -80,7 +80,7 @@ export default function App(): React.JSX.Element {
-
-
-
+
@@ -99,7 +97,7 @@ export default function App(): React.JSX.Element {
}
function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Element {
- const theme = useThemeSettingContext()
+ const [theme] = useThemeSetting()
const isDarkMode = useColorScheme() === 'dark'
diff --git a/src/api/queries/patrons/index.ts b/src/api/queries/patrons/index.ts
new file mode 100644
index 00000000..8ee22a85
--- /dev/null
+++ b/src/api/queries/patrons/index.ts
@@ -0,0 +1,19 @@
+import { useQuery } from '@tanstack/react-query'
+import { QueryKeys } from '../../../enums/query-keys'
+import { useJellifyContext } from '../../../providers'
+import fetchPatrons from './utils'
+import { ONE_DAY } from '../../../constants/query-client'
+
+const usePatronsQuery = () => {
+ const { api } = useJellifyContext()
+
+ return useQuery({
+ queryKey: [QueryKeys.Patrons],
+ queryFn: () => fetchPatrons(api),
+ staleTime: ONE_DAY,
+ })
+}
+
+const usePatrons = () => usePatronsQuery().data
+
+export default usePatrons
diff --git a/src/api/queries/patrons.ts b/src/api/queries/patrons/utils/index.ts
similarity index 82%
rename from src/api/queries/patrons.ts
rename to src/api/queries/patrons/utils/index.ts
index f0ca1dc8..36d0f8b5 100644
--- a/src/api/queries/patrons.ts
+++ b/src/api/queries/patrons/utils/index.ts
@@ -1,5 +1,7 @@
import { Api } from '@jellyfin/sdk'
+const PATRON_API_ENDPOINT = 'https://patrons.jellify.app'
+
interface Patron {
fullName: string
}
@@ -9,7 +11,7 @@ export default async function fetchPatrons(api: Api | undefined): Promise {
const patrons = res.data as Patron[]
resolve(patrons)
diff --git a/src/components/Album/index.tsx b/src/components/Album/index.tsx
index b84d075f..8702e6b7 100644
--- a/src/components/Album/index.tsx
+++ b/src/components/Album/index.tsx
@@ -15,7 +15,6 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context'
import Icon from '../Global/components/icon'
import { mapDtoToTrack } from '../../utils/mappings'
import { useNetworkContext } from '../../providers/Network'
-import { useDownloadQualityContext } from '../../providers/Settings'
import { useLoadNewQueue } from '../../providers/Player/hooks/mutations'
import { QueuingType } from '../../enums/queuing-type'
import { useAlbumContext } from '../../providers/Album'
@@ -42,7 +41,6 @@ export function Album(): React.JSX.Element {
const { api } = useJellifyContext()
const { useDownloadMultiple, pendingDownloads, networkStatus } = useNetworkContext()
- const downloadQuality = useDownloadQualityContext()
const streamingDeviceProfile = useStreamingDeviceProfile()
const downloadingDeviceProfile = useDownloadingDeviceProfile()
const { mutate: loadNewQueue } = useLoadNewQueue()
@@ -66,10 +64,8 @@ export function Album(): React.JSX.Element {
loadNewQueue({
api,
- downloadedTracks,
networkStatus,
deviceProfile: streamingDeviceProfile,
- downloadQuality,
track: allTracks[0],
index: 0,
tracklist: allTracks,
diff --git a/src/components/Artist/tab-bar.tsx b/src/components/Artist/tab-bar.tsx
index 1d93252f..145247f2 100644
--- a/src/components/Artist/tab-bar.tsx
+++ b/src/components/Artist/tab-bar.tsx
@@ -17,10 +17,8 @@ import { QueuingType } from '../../enums/queuing-type'
import { fetchAlbumDiscs } from '../../api/queries/item'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../screens/types'
-import { useDownloadQualityContext } from '../../providers/Settings'
import { useNetworkContext } from '../../providers/Network'
import useStreamingDeviceProfile from '../../stores/device-profile'
-import { useAllDownloadedTracks } from '../../api/queries/download'
export default function ArtistTabBar({
stackNavigation,
@@ -35,12 +33,8 @@ export default function ArtistTabBar({
const deviceProfile = useStreamingDeviceProfile()
- const downloadQuality = useDownloadQualityContext()
-
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const { width } = useSafeAreaFrame()
const theme = useTheme()
@@ -62,10 +56,8 @@ export default function ArtistTabBar({
loadNewQueue({
api,
- downloadedTracks,
networkStatus,
deviceProfile,
- downloadQuality,
track: allTracks[0],
index: 0,
tracklist: allTracks,
diff --git a/src/components/CarPlay/Home.tsx b/src/components/CarPlay/Home.tsx
index 4f1da4b9..583ba35e 100644
--- a/src/components/CarPlay/Home.tsx
+++ b/src/components/CarPlay/Home.tsx
@@ -9,18 +9,14 @@ import { InfiniteData } from '@tanstack/react-query'
import { QueueMutation } from '../../providers/Player/interfaces'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { Api } from '@jellyfin/sdk'
-import { JellifyDownload } from '../../types/JellifyDownload'
import { networkStatusTypes } from '../Network/internetConnectionWatcher'
-import { DownloadQuality } from '../../providers/Settings'
const CarPlayHome = (
library: JellifyLibrary,
loadQueue: (mutation: QueueMutation) => void,
api: Api | undefined,
- downloadedTracks: JellifyDownload[] | undefined,
networkStatus: networkStatusTypes | null,
deviceProfile: DeviceProfile | undefined,
- downloadQuality: DownloadQuality,
) =>
new ListTemplate({
id: uuid.v4(),
@@ -69,10 +65,8 @@ const CarPlayHome = (
loadQueue,
'Recently Played',
api,
- downloadedTracks,
networkStatus,
deviceProfile,
- downloadQuality,
),
)
break
@@ -100,10 +94,8 @@ const CarPlayHome = (
loadQueue,
'On Repeat',
api,
- downloadedTracks,
networkStatus,
deviceProfile,
- downloadQuality,
),
)
break
diff --git a/src/components/CarPlay/Navigation.tsx b/src/components/CarPlay/Navigation.tsx
index f515b259..ff0e46ff 100644
--- a/src/components/CarPlay/Navigation.tsx
+++ b/src/components/CarPlay/Navigation.tsx
@@ -5,33 +5,21 @@ import uuid from 'react-native-uuid'
import { QueueMutation } from '../../providers/Player/interfaces'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { Api } from '@jellyfin/sdk'
-import { JellifyDownload } from '@/src/types/JellifyDownload'
import { networkStatusTypes } from '../Network/internetConnectionWatcher'
-import { DownloadQuality } from '../../providers/Settings'
import { DeviceProfile } from '@jellyfin/sdk/lib/generated-client'
const CarPlayNavigation = (
library: JellifyLibrary,
loadQueue: (mutation: QueueMutation) => void,
api: Api | undefined,
- downloadedTracks: JellifyDownload[] | undefined,
networkStatus: networkStatusTypes | null,
deviceProfile: DeviceProfile | undefined,
- downloadQuality: DownloadQuality,
) =>
new TabBarTemplate({
id: uuid.v4(),
title: 'Tabs',
templates: [
- CarPlayHome(
- library,
- loadQueue,
- api,
- downloadedTracks,
- networkStatus,
- deviceProfile,
- downloadQuality,
- ),
+ CarPlayHome(library, loadQueue, api, networkStatus, deviceProfile),
CarPlayDiscover,
],
onTemplateSelect(template, e) {},
diff --git a/src/components/CarPlay/Tracks.tsx b/src/components/CarPlay/Tracks.tsx
index 7f48efc1..4335ea34 100644
--- a/src/components/CarPlay/Tracks.tsx
+++ b/src/components/CarPlay/Tracks.tsx
@@ -6,19 +6,15 @@ import { Queue } from '../../player/types/queue-item'
import { QueueMutation } from '../../providers/Player/interfaces'
import { QueuingType } from '../../enums/queuing-type'
import { Api } from '@jellyfin/sdk'
-import { JellifyDownload } from '../../types/JellifyDownload'
import { networkStatusTypes } from '../Network/internetConnectionWatcher'
-import { DownloadQuality } from '../../providers/Settings'
const TracksTemplate = (
items: BaseItemDto[],
loadQueue: (mutation: QueueMutation) => void,
queuingRef: Queue,
api: Api | undefined,
- downloadedTracks: JellifyDownload[] | undefined,
networkStatus: networkStatusTypes | null,
deviceProfile: DeviceProfile | undefined,
- downloadQuality: DownloadQuality,
) =>
new ListTemplate({
id: uuid.v4(),
@@ -38,8 +34,6 @@ const TracksTemplate = (
api,
networkStatus,
deviceProfile,
- downloadQuality,
- downloadedTracks,
queuingType: QueuingType.FromSelection,
index,
tracklist: items,
diff --git a/src/components/Context/index.tsx b/src/components/Context/index.tsx
index 19a6ed5b..bd25dcaf 100644
--- a/src/components/Context/index.tsx
+++ b/src/components/Context/index.tsx
@@ -3,13 +3,10 @@ import {
BaseItemKind,
MediaSourceInfo,
} from '@jellyfin/sdk/lib/generated-client/models'
-import { getToken, ListItem, ScrollView, Spinner, View, YGroup } from 'tamagui'
+import { ListItem, ScrollView, Spinner, View, YGroup } from 'tamagui'
import { BaseStackParamList, RootStackParamList } from '../../screens/types'
import { Text } from '../Global/helpers/text'
import FavoriteContextMenuRow from '../Global/components/favorite-context-menu-row'
-import { useColorScheme } from 'react-native'
-import { useDownloadQualityContext, useThemeSettingContext } from '../../providers/Settings'
-import LinearGradient from 'react-native-linear-gradient'
import Icon from '../Global/components/icon'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useQuery } from '@tanstack/react-query'
@@ -33,7 +30,7 @@ import { useAddToQueue } from '../../providers/Player/hooks/mutations'
import { useNetworkContext } from '../../providers/Network'
import { mapDtoToTrack } from '../../utils/mappings'
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../../stores/device-profile'
-import { useAllDownloadedTracks, useIsDownloaded } from '../../api/queries/download'
+import { useIsDownloaded } from '../../api/queries/download'
import { useDeleteDownloads } from '../../api/mutations/download'
type StackNavigation = Pick, 'navigate' | 'dispatch'>
@@ -169,10 +166,6 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
- const downloadQuality = useDownloadQualityContext()
-
const deviceProfile = useStreamingDeviceProfile()
const { mutate: addToQueue } = useAddToQueue()
@@ -180,9 +173,7 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
const mutation: AddToQueueMutation = {
api,
networkStatus,
- downloadedTracks,
deviceProfile,
- downloadQuality,
tracks,
queuingType: QueuingType.DirectlyQueued,
}
@@ -206,21 +197,6 @@ function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Ele
)
}
-function BackgroundGradient(): React.JSX.Element {
- const themeSetting = useThemeSettingContext()
-
- const colorScheme = useColorScheme()
-
- const isDarkMode =
- (themeSetting === 'system' && colorScheme === 'dark') || themeSetting === 'dark'
-
- const gradientColors = isDarkMode
- ? [getToken('$black'), getToken('$black75')]
- : [getToken('$lightTranslucent'), getToken('$lightTranslucent')]
-
- return
-}
-
function DownloadMenuRow({ items }: { items: BaseItemDto[] }): React.JSX.Element {
const { api } = useJellifyContext()
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
diff --git a/src/components/Global/components/alphabetical-selector.tsx b/src/components/Global/components/alphabetical-selector.tsx
index 460e0c69..6513e295 100644
--- a/src/components/Global/components/alphabetical-selector.tsx
+++ b/src/components/Global/components/alphabetical-selector.tsx
@@ -11,7 +11,7 @@ import Animated, {
import { Text } from '../helpers/text'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { trigger } from 'react-native-haptic-feedback'
-import { useReducedHapticsContext } from '../../../providers/Settings'
+import { useReducedHapticsSetting } from '../../../stores/settings/app'
const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
/**
@@ -27,7 +27,7 @@ const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
export function AZScroller({ onLetterSelect }: { onLetterSelect: (letter: string) => void }) {
const { width, height } = useSafeAreaFrame()
const theme = useTheme()
- const reducedHaptics = useReducedHapticsContext()
+ const [reducedHaptics] = useReducedHapticsSetting()
const overlayOpacity = useSharedValue(0)
diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx
index c948db2a..aebf1cc3 100644
--- a/src/components/Global/components/item-row.tsx
+++ b/src/components/Global/components/item-row.tsx
@@ -14,9 +14,7 @@ import { BaseStackParamList } from '../../../screens/types'
import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
import { useJellifyContext } from '../../../providers'
import { useNetworkContext } from '../../../providers/Network'
-import { useDownloadQualityContext } from '../../../providers/Settings'
import useStreamingDeviceProfile from '../../../stores/device-profile'
-import { useAllDownloadedTracks } from '../../../api/queries/download'
interface ItemRowProps {
item: BaseItemDto
@@ -47,12 +45,8 @@ export default function ItemRow({
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const deviceProfile = useStreamingDeviceProfile()
- const downloadQuality = useDownloadQualityContext()
-
const { mutate: loadNewQueue } = useLoadNewQueue()
const gestureCallback = () => {
@@ -60,10 +54,8 @@ export default function ItemRow({
case 'Audio': {
loadNewQueue({
api,
- downloadedTracks,
networkStatus,
deviceProfile,
- downloadQuality,
track: item,
tracklist: [item],
index: 0,
diff --git a/src/components/Global/components/track.tsx b/src/components/Global/components/track.tsx
index ac264b81..40fa8f11 100644
--- a/src/components/Global/components/track.tsx
+++ b/src/components/Global/components/track.tsx
@@ -17,11 +17,10 @@ import ItemImage from './image'
import useItemContext from '../../../hooks/use-item-context'
import { useNowPlaying, useQueue } from '../../../providers/Player/hooks/queries'
import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
-import { useDownloadQualityContext } from '../../../providers/Settings'
import { useJellifyContext } from '../../../providers'
import useStreamingDeviceProfile from '../../../stores/device-profile'
import useStreamedMediaInfo from '../../../api/queries/media'
-import { useAllDownloadedTracks, useDownloadedTrack } from '../../../api/queries/download'
+import { useDownloadedTrack } from '../../../api/queries/download'
export interface TrackProps {
track: BaseItemDto
@@ -61,8 +60,6 @@ export default function Track({
const deviceProfile = useStreamingDeviceProfile()
- const downloadQuality = useDownloadQualityContext()
-
const { data: nowPlaying } = useNowPlaying()
const { data: playQueue } = useQueue()
const { mutate: loadNewQueue } = useLoadNewQueue()
@@ -70,8 +67,6 @@ export default function Track({
const { data: mediaInfo } = useStreamedMediaInfo(track.Id)
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const offlineAudio = useDownloadedTrack(track.Id)
useItemContext(track)
@@ -100,9 +95,7 @@ export default function Track({
} else {
loadNewQueue({
api,
- downloadedTracks,
deviceProfile,
- downloadQuality,
networkStatus,
track,
index,
@@ -112,7 +105,7 @@ export default function Track({
startPlayback: true,
})
}
- }, [onPress, track, index, memoizedTracklist, queue, useLoadNewQueue, downloadedTracks])
+ }, [onPress, track, index, memoizedTracklist, queue, useLoadNewQueue])
const handleLongPress = useCallback(() => {
if (onLongPress) {
diff --git a/src/components/Home/helpers/frequent-tracks.tsx b/src/components/Home/helpers/frequent-tracks.tsx
index e5cc28f1..cc218cb9 100644
--- a/src/components/Home/helpers/frequent-tracks.tsx
+++ b/src/components/Home/helpers/frequent-tracks.tsx
@@ -12,22 +12,16 @@ import HomeStackParamList from '../../../screens/Home/types'
import { useNavigation } from '@react-navigation/native'
import { RootStackParamList } from '../../../screens/types'
import { useJellifyContext } from '../../../providers'
-import { useDownloadQualityContext } from '../../../providers/Settings'
import { useNetworkContext } from '../../../providers/Network'
import useStreamingDeviceProfile from '../../../stores/device-profile'
-import { useAllDownloadedTracks } from '../../../api/queries/download'
export default function FrequentlyPlayedTracks(): React.JSX.Element {
const { api } = useJellifyContext()
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const deviceProfile = useStreamingDeviceProfile()
- const downloadQuality = useDownloadQualityContext()
-
const {
frequentlyPlayed,
fetchNextFrequentlyPlayed,
@@ -76,8 +70,6 @@ export default function FrequentlyPlayedTracks(): React.JSX.Element {
loadNewQueue({
api,
deviceProfile,
- downloadQuality,
- downloadedTracks,
networkStatus,
track,
index,
diff --git a/src/components/Home/helpers/recently-played.tsx b/src/components/Home/helpers/recently-played.tsx
index 04954e9e..9b61c549 100644
--- a/src/components/Home/helpers/recently-played.tsx
+++ b/src/components/Home/helpers/recently-played.tsx
@@ -15,21 +15,15 @@ import HomeStackParamList from '../../../screens/Home/types'
import { useNowPlaying } from '../../../providers/Player/hooks/queries'
import { useJellifyContext } from '../../../providers'
import { useNetworkContext } from '../../../providers/Network'
-import { useDownloadQualityContext } from '../../../providers/Settings'
import useStreamingDeviceProfile from '../../../stores/device-profile'
-import { useAllDownloadedTracks } from '../../../api/queries/download'
export default function RecentlyPlayed(): React.JSX.Element {
const { api } = useJellifyContext()
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const deviceProfile = useStreamingDeviceProfile()
- const downloadQuality = useDownloadQualityContext()
-
const { data: nowPlaying } = useNowPlaying()
const navigation = useNavigation>()
@@ -76,10 +70,8 @@ export default function RecentlyPlayed(): React.JSX.Element {
onPress={() => {
loadNewQueue({
api,
- downloadedTracks,
deviceProfile,
networkStatus,
- downloadQuality,
track: recentlyPlayedTrack,
index: index,
tracklist: recentTracks ?? [recentlyPlayedTrack],
diff --git a/src/components/Library/tab-bar.tsx b/src/components/Library/tab-bar.tsx
index 5e28c4f5..11dffc4a 100644
--- a/src/components/Library/tab-bar.tsx
+++ b/src/components/Library/tab-bar.tsx
@@ -7,13 +7,13 @@ import { Text } from '../Global/helpers/text'
import { isUndefined } from 'lodash'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { trigger } from 'react-native-haptic-feedback'
-import { useReducedHapticsContext } from '../../providers/Settings'
+import { useReducedHapticsSetting } from '../../stores/settings/app'
function LibraryTabBar(props: MaterialTopTabBarProps) {
const { isFavorites, setIsFavorites, isDownloaded, setIsDownloaded } =
useLibrarySortAndFilterContext()
- const reducedHaptics = useReducedHapticsContext()
+ const [reducedHaptics] = useReducedHapticsSetting()
const insets = useSafeAreaInsets()
diff --git a/src/components/Player/components/blurred-background.tsx b/src/components/Player/components/blurred-background.tsx
index 93e1c389..d451e38d 100644
--- a/src/components/Player/components/blurred-background.tsx
+++ b/src/components/Player/components/blurred-background.tsx
@@ -2,11 +2,11 @@ import React, { memo } from 'react'
import { getToken, useTheme, View, YStack, ZStack } from 'tamagui'
import { useColorScheme } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
-import { useThemeSettingContext } from '../../../providers/Settings'
import { getPrimaryBlurhashFromDto } from '../../../utils/blurhash'
import { Blurhash } from 'react-native-blurhash'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import { useNowPlaying } from '../../../providers/Player/hooks/queries'
+import { useThemeSetting } from '../../../stores/settings/app'
function BlurredBackground({
width,
@@ -17,7 +17,7 @@ function BlurredBackground({
}): React.JSX.Element {
const { data: nowPlaying } = useNowPlaying()
- const themeSetting = useThemeSettingContext()
+ const [themeSetting] = useThemeSetting()
const theme = useTheme()
const colorScheme = useColorScheme()
diff --git a/src/components/Player/components/scrubber.tsx b/src/components/Player/components/scrubber.tsx
index c7f87719..1094683f 100644
--- a/src/components/Player/components/scrubber.tsx
+++ b/src/components/Player/components/scrubber.tsx
@@ -8,10 +8,10 @@ import { useSeekTo } from '../../../providers/Player/hooks/mutations'
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
import { UPDATE_INTERVAL } from '../../../player/config'
import { ProgressMultiplier } from '../component.config'
-import { useReducedHapticsContext } from '../../../providers/Settings'
import { useNowPlaying, useProgress } from '../../../providers/Player/hooks/queries'
import QualityBadge from './quality-badge'
-import { useDisplayAudioQualityBadge } from '../../../stores/player-settings'
+import { useDisplayAudioQualityBadge } from '../../../stores/settings/player'
+import { useReducedHapticsSetting } from '../../../stores/settings/app'
// Create a simple pan gesture
const scrubGesture = Gesture.Pan().runOnJS(true)
@@ -20,7 +20,7 @@ export default function Scrubber(): React.JSX.Element {
const { mutate: seekTo, isPending: seekPending, mutateAsync: seekToAsync } = useSeekTo()
const { data: nowPlaying } = useNowPlaying()
const { width } = useSafeAreaFrame()
- const reducedHaptics = useReducedHapticsContext()
+ const reducedHaptics = useReducedHapticsSetting()
// Get progress from the track player with the specified update interval
// We *don't* use the duration from this hook because it will have a value of "0"
diff --git a/src/components/Playlist/components/header.tsx b/src/components/Playlist/components/header.tsx
index 92b890c3..8d6bb66e 100644
--- a/src/components/Playlist/components/header.tsx
+++ b/src/components/Playlist/components/header.tsx
@@ -15,14 +15,12 @@ import { useNetworkContext } from '../../../../src/providers/Network'
import { ActivityIndicator } from 'react-native'
import { mapDtoToTrack } from '../../../utils/mappings'
import { QueuingType } from '../../../enums/queuing-type'
-import { useDownloadQualityContext } from '../../../providers/Settings'
import { useNavigation } from '@react-navigation/native'
import LibraryStackParamList from '@/src/screens/Library/types'
import { useLoadNewQueue } from '../../../providers/Player/hooks/mutations'
import useStreamingDeviceProfile, {
useDownloadingDeviceProfile,
} from '../../../stores/device-profile'
-import { useAllDownloadedTracks } from '../../../api/queries/download'
export default function PlayliistTracklistHeader(
playlist: BaseItemDto,
@@ -152,7 +150,6 @@ function PlaylistHeaderControls({
canEdit: boolean | undefined
}): React.JSX.Element {
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
- const downloadQuality = useDownloadQualityContext()
const streamingDeviceProfile = useStreamingDeviceProfile()
const downloadingDeviceProfile = useDownloadingDeviceProfile()
const { mutate: loadNewQueue } = useLoadNewQueue()
@@ -161,8 +158,6 @@ function PlaylistHeaderControls({
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const navigation = useNavigation>()
const downloadPlaylist = () => {
@@ -178,9 +173,7 @@ function PlaylistHeaderControls({
loadNewQueue({
api,
- downloadQuality,
networkStatus,
- downloadedTracks,
deviceProfile: streamingDeviceProfile,
track: playlistTracks[0],
index: 0,
diff --git a/src/components/Settings/component.tsx b/src/components/Settings/component.tsx
index 8059b517..ca4e5e48 100644
--- a/src/components/Settings/component.tsx
+++ b/src/components/Settings/component.tsx
@@ -3,21 +3,17 @@ import { createMaterialTopTabNavigator } from '@react-navigation/material-top-ta
import { getToken, useTheme } from 'tamagui'
import AccountTab from './components/account-tab'
import Icon from '../Global/components/icon'
-import LabsTab from './components/labs-tab'
import PreferencesTab from './components/preferences-tab'
import PlaybackTab from './components/playback-tab'
import InfoTab from './components/info-tab'
import SettingsTabBar from './components/tab-bar'
-import StorageTab from './components/storage-tab'
-import { useDevToolsContext } from '../../providers/Settings'
+import StorageTab from './components/usage-tab'
import { SafeAreaView } from 'react-native-safe-area-context'
const SettingsTabsNavigator = createMaterialTopTabNavigator()
export default function Settings(): React.JSX.Element {
const theme = useTheme()
- const devTools = useDevToolsContext()
-
return (
- {devTools && (
+ {/*
- )}
+ ) */}
)
diff --git a/src/components/Settings/components/account-tab.tsx b/src/components/Settings/components/account-tab.tsx
index d84138fd..f06e68fa 100644
--- a/src/components/Settings/components/account-tab.tsx
+++ b/src/components/Settings/components/account-tab.tsx
@@ -1,6 +1,5 @@
import React from 'react'
import { useJellifyContext } from '../../../providers'
-import { SafeAreaView } from 'react-native-safe-area-context'
import SignOut from './sign-out-button'
import { SettingsStackParamList } from '../../../screens/Settings/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
diff --git a/src/components/Settings/components/info/index.tsx b/src/components/Settings/components/info/index.tsx
index 44c5ac6d..4c60176c 100644
--- a/src/components/Settings/components/info/index.tsx
+++ b/src/components/Settings/components/info/index.tsx
@@ -1,42 +1,33 @@
-import { SafeAreaView } from 'react-native-safe-area-context'
import { version } from '../../../../../package.json'
-import { H5, Text } from '../../../Global/helpers/text'
+import { Text } from '../../../Global/helpers/text'
import SettingsListGroup from '../settings-list-group'
-import { InfoTabNativeStackNavigationProp } from './types'
-import { useQuery } from '@tanstack/react-query'
-import { QueryKeys } from '../../../../enums/query-keys'
-import { useJellifyContext } from '../../../../providers'
-import fetchPatrons from '../../../../api/queries/patrons'
import { FlatList, Linking } from 'react-native'
-import { H6, ScrollView, Separator, XStack, YStack } from 'tamagui'
+import { ScrollView, Separator, XStack, YStack } from 'tamagui'
import Icon from '../../../Global/components/icon'
-import { useEffect, useState } from 'react'
-import { useSetDevToolsContext } from '../../../../providers/Settings'
-export default function InfoTabIndex({ navigation }: InfoTabNativeStackNavigationProp) {
- const { api } = useJellifyContext()
+import usePatrons from '../../../../api/queries/patrons'
+import { useQuery } from '@tanstack/react-query'
+import INFO_CAPTIONS from '../../utils/info-caption'
+import { ONE_HOUR } from '../../../../constants/query-client'
+import { pickRandomItemFromArray } from '../../../../utils/random'
+import { FlashList } from '@shopify/flash-list'
- const setDevTools = useSetDevToolsContext()
+export default function InfoTabIndex() {
+ const patrons = usePatrons()
- const [versionNumberPresses, setVersionNumberPresses] = useState(0)
-
- const { data: patrons } = useQuery({
- queryKey: [QueryKeys.Patrons],
- queryFn: () => fetchPatrons(api),
+ const { data: caption } = useQuery({
+ queryKey: ['Info_Caption'],
+ queryFn: () => `${pickRandomItemFromArray(INFO_CAPTIONS)}!`,
+ staleTime: ONE_HOUR,
+ initialData: 'Live and in stereo',
})
- useEffect(() => {
- if (versionNumberPresses > 5) {
- setDevTools(true)
- }
- }, [versionNumberPresses])
-
return (
+
+ Linking.openURL(
+ 'https://github.com/sponsors/anultravioletaurora/',
+ )
+ }
+ >
+
+ Sponsors
+
+
+ Linking.openURL('https://patreon.com/anultravioletaurora')
+ }
+ >
+
+ Patreon
+
+
+ ),
+ },
+ {
+ title: 'Patreon Wall of Fame',
+ subTitle: 'Thank you to these paid members',
+ iconName: 'patreon',
+ iconColor: '$primary',
+ children: (
+
-
-
- Linking.openURL(
- 'https://github.com/sponsors/anultravioletaurora/',
- )
- }
- >
-
- Sponsors
-
-
- Linking.openURL(
- 'https://patreon.com/anultravioletaurora',
- )
- }
- >
-
- Patreon
-
-
-
-
-
- Patreon Wall of Fame
-
- }
numColumns={1}
renderItem={({ item }) => (
diff --git a/src/components/Settings/components/playback-tab.tsx b/src/components/Settings/components/playback-tab.tsx
index 570e4a81..f9b6403c 100644
--- a/src/components/Settings/components/playback-tab.tsx
+++ b/src/components/Settings/components/playback-tab.tsx
@@ -1,20 +1,15 @@
import SettingsListGroup from './settings-list-group'
-import { RadioGroup, YStack } from 'tamagui'
+import { RadioGroup } from 'tamagui'
import { RadioGroupItemWithLabel } from '../../Global/helpers/radio-group-item-with-label'
-import { Text } from '../../Global/helpers/text'
import {
StreamingQuality,
- useSetStreamingQualityContext,
- useStreamingQualityContext,
-} from '../../../providers/Settings'
-import useStreamingDeviceProfile from '../../../stores/device-profile'
-import { useDisplayAudioQualityBadge } from '../../../stores/player-settings'
+ useDisplayAudioQualityBadge,
+ useStreamingQuality,
+} from '../../../stores/settings/player'
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
export default function PlaybackTab(): React.JSX.Element {
- const deviceProfile = useStreamingDeviceProfile()
- const streamingQuality = useStreamingQualityContext()
- const setStreamingQuality = useSetStreamingQualityContext()
+ const [streamingQuality, setStreamingQuality] = useStreamingQuality()
const [displayAudioQualityBadge, setDisplayAudioQualityBadge] = useDisplayAudioQualityBadge()
@@ -23,49 +18,45 @@ export default function PlaybackTab(): React.JSX.Element {
settingsList={[
{
title: 'Streaming Quality',
- subTitle: `${deviceProfile?.Name ?? 'Not set'}`,
+ subTitle: `Changes apply to new tracks`,
iconName: 'radio-tower',
- iconColor: '$borderColor',
+ iconColor:
+ streamingQuality === StreamingQuality.Original ? '$primary' : '$danger',
children: (
-
-
- Higher quality uses more bandwidth. Changes apply to new tracks.
-
-
- setStreamingQuality(value as StreamingQuality)
- }
- >
-
-
-
-
-
-
+
+ setStreamingQuality(value as StreamingQuality)
+ }
+ >
+
+
+
+
+
),
},
{
title: 'Show Audio Quality Badge',
subTitle: 'Displays audio quality in the player',
iconName: 'sine-wave',
- iconColor: '$borderColor',
+ iconColor: displayAudioQualityBadge ? '$success' : '$borderColor',
children: (
setThemeSetting(value as Theme)}
+ onValueChange={(value) => setThemeSetting(value as ThemeSetting)}
>
diff --git a/src/components/Settings/components/settings-list-group.tsx b/src/components/Settings/components/settings-list-group.tsx
index 521734fe..b4f85b0f 100644
--- a/src/components/Settings/components/settings-list-group.tsx
+++ b/src/components/Settings/components/settings-list-group.tsx
@@ -17,7 +17,12 @@ export default function SettingsListGroup({
}: SettingsListGroupProps): React.JSX.Element {
return (
-
+
{settingsList.map((setting, index, self) => (
<>
diff --git a/src/components/Settings/components/storage-tab.tsx b/src/components/Settings/components/storage-tab.tsx
deleted file mode 100644
index 1bad1964..00000000
--- a/src/components/Settings/components/storage-tab.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import SettingsListGroup from './settings-list-group'
-import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
-import { RadioGroupItemWithLabel } from '../../Global/helpers/radio-group-item-with-label'
-import {
- DownloadQuality,
- useAutoDownloadContext,
- useSetAutoDownloadContext,
- useDownloadQualityContext,
- useSetDownloadQualityContext,
-} from '../../../providers/Settings'
-import { useNetworkContext } from '../../../providers/Network'
-import { RadioGroup, YStack } from 'tamagui'
-import { Text } from '../../Global/helpers/text'
-import { getQualityLabel } from '../utils/quality'
-import { useAllDownloadedTracks } from '../../../api/queries/download'
-export default function StorageTab(): React.JSX.Element {
- const autoDownload = useAutoDownloadContext()
- const setAutoDownload = useSetAutoDownloadContext()
- const downloadQuality = useDownloadQualityContext()
- const setDownloadQuality = useSetDownloadQualityContext()
-
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
- return (
- setAutoDownload(!autoDownload)}
- />
- ),
- },
- {
- title: 'Download Quality',
- subTitle: `Current: ${getQualityLabel(downloadQuality)} • For offline tracks`,
- iconName: 'file-download',
- iconColor: '$primary',
- children: (
-
-
- Quality used when saving tracks for offline use.
-
-
- setDownloadQuality(value as DownloadQuality)
- }
- >
-
-
-
-
-
-
- ),
- },
- ]}
- />
- )
-}
diff --git a/src/components/Settings/components/usage-tab.tsx b/src/components/Settings/components/usage-tab.tsx
new file mode 100644
index 00000000..9af2b5ef
--- /dev/null
+++ b/src/components/Settings/components/usage-tab.tsx
@@ -0,0 +1,74 @@
+import SettingsListGroup from './settings-list-group'
+import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
+import { RadioGroupItemWithLabel } from '../../Global/helpers/radio-group-item-with-label'
+import { RadioGroup } from 'tamagui'
+import { useAllDownloadedTracks } from '../../../api/queries/download'
+import {
+ DownloadQuality,
+ useAutoDownload,
+ useDownloadQuality,
+} from '../../../stores/settings/usage'
+export default function StorageTab(): React.JSX.Element {
+ const [autoDownload, setAutoDownload] = useAutoDownload()
+ const [downloadQuality, setDownloadQuality] = useDownloadQuality()
+
+ const { data: downloadedTracks } = useAllDownloadedTracks()
+
+ return (
+ setAutoDownload(!autoDownload)}
+ />
+ ),
+ },
+ {
+ title: 'Download Quality',
+ subTitle: `Quality used when downloading tracks`,
+ iconName: 'file-download',
+ iconColor: '$primary',
+ children: (
+ setDownloadQuality(value as DownloadQuality)}
+ >
+
+
+
+
+
+ ),
+ },
+ ]}
+ />
+ )
+}
diff --git a/src/components/Settings/types.d.ts b/src/components/Settings/types.d.ts
index 44da3dba..e1999c0f 100644
--- a/src/components/Settings/types.d.ts
+++ b/src/components/Settings/types.d.ts
@@ -2,7 +2,7 @@ export type SettingsTabList = {
title: string
iconName: string
iconColor: ThemeTokens
- subTitle: string
+ subTitle?: string
children?: React.ReactNode
onPress?: () => void
}[]
diff --git a/src/components/Settings/utils/info-caption.ts b/src/components/Settings/utils/info-caption.ts
new file mode 100644
index 00000000..656a848c
--- /dev/null
+++ b/src/components/Settings/utils/info-caption.ts
@@ -0,0 +1,23 @@
+const INFO_CAPTIONS = [
+ // Wholesome stuff
+ 'Made with love',
+
+ // Outside jokes (that anyone can get)
+ 'Not made with real jellyfish',
+
+ // Inside Jokes (that the internal Jellify team will get)
+ 'Thank you, Pikachu',
+
+ // Movie Quotes
+ 'Groovy, baby!', // Austin Powers
+ 'Turned up to eleven!', // This is Spinal Tap
+ 'Be excellent to each other!', // Bill and Ted's Excellent Adventure
+ 'Party on, dude!', // Bill and Ted's Excellent Adventure
+ 'WYLD STALLYNS!!!', // Bill and Ted's Excellent Adventure
+
+ // ASCII Art
+ '─=≡Σ((( つ•̀ω•́)つ',
+ '・:*+.\\(( °ω° ))/.:+',
+]
+
+export default INFO_CAPTIONS
diff --git a/src/components/jellify.tsx b/src/components/jellify.tsx
index 9efb4afa..2822b3fd 100644
--- a/src/components/jellify.tsx
+++ b/src/components/jellify.tsx
@@ -6,7 +6,6 @@ import { JellifyProvider, useJellifyContext } from '../providers'
import { JellifyUserDataProvider } from '../providers/UserData'
import { NetworkContextProvider } from '../providers/Network'
import { DisplayProvider } from '../providers/Display/display-provider'
-import { useSendMetricsContext, useThemeSettingContext } from '../providers/Settings'
import {
createTelemetryDeck,
TelemetryDeckProvider,
@@ -21,12 +20,13 @@ import JellifyToastConfig from '../constants/toast.config'
import { useColorScheme } from 'react-native'
import { CarPlayProvider } from '../providers/CarPlay'
import { useSelectPlayerEngine } from '../stores/player-engine'
+import { useSendMetricsSetting, useThemeSetting } from '../stores/settings/app'
/**
* The main component for the Jellify app. Children are wrapped in the {@link JellifyProvider}
* @returns The {@link Jellify} component
*/
export default function Jellify(): React.JSX.Element {
- const theme = useThemeSettingContext()
+ const [theme] = useThemeSetting()
const isDarkMode = useColorScheme() === 'dark'
useSelectPlayerEngine()
@@ -45,7 +45,7 @@ export default function Jellify(): React.JSX.Element {
}
function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): React.JSX.Element {
- const sendMetrics = useSendMetricsContext()
+ const [sendMetrics] = useSendMetricsSetting()
/**
* Create the TelemetryDeck instance, which is used to send telemetry data to the server
@@ -69,7 +69,7 @@ function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): Rea
* @returns The {@link App} component
*/
function App(): React.JSX.Element {
- const sendMetrics = useSendMetricsContext()
+ const [sendMetrics] = useSendMetricsSetting()
const telemetrydeck = useTelemetryDeck()
const theme = useTheme()
diff --git a/src/constants/storage.ts b/src/constants/storage.ts
index 17a52067..7da6bf6f 100644
--- a/src/constants/storage.ts
+++ b/src/constants/storage.ts
@@ -1,11 +1,13 @@
import { MMKV } from 'react-native-mmkv'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
+import { AsyncStorage } from '@tanstack/react-query-persist-client'
+import { StateStorage } from 'zustand/middleware'
console.debug(`Building MMKV storage`)
export const storage = new MMKV()
-const clientStorage = {
+const storageFunctions = {
setItem: (key: string, value: string) => {
storage.set(key, value)
},
@@ -18,6 +20,10 @@ const clientStorage = {
},
}
-export const clientPersister = createAsyncStoragePersister({
+const clientStorage: AsyncStorage = storageFunctions
+
+export const queryClientPersister = createAsyncStoragePersister({
storage: clientStorage,
})
+
+export const stateStorage: StateStorage = storageFunctions
diff --git a/src/providers/CarPlay/index.tsx b/src/providers/CarPlay/index.tsx
index 9784d569..7d8a5aec 100644
--- a/src/providers/CarPlay/index.tsx
+++ b/src/providers/CarPlay/index.tsx
@@ -5,9 +5,7 @@ import { CarPlay } from 'react-native-carplay'
import { useJellifyContext } from '../index'
import { useLoadNewQueue } from '../Player/hooks/mutations'
import { useNetworkContext } from '../Network'
-import { useDownloadQualityContext } from '../Settings'
import useStreamingDeviceProfile from '../../stores/device-profile'
-import { useAllDownloadedTracks } from '../../api/queries/download'
interface CarPlayContext {
carplayConnected: boolean
@@ -19,10 +17,7 @@ const CarPlayContextInitializer = () => {
const { networkStatus } = useNetworkContext()
- const { data: downloadedTracks } = useAllDownloadedTracks()
-
const deviceProfile = useStreamingDeviceProfile()
- const downloadQuality = useDownloadQualityContext()
const { mutate: loadNewQueue } = useLoadNewQueue()
@@ -32,15 +27,7 @@ const CarPlayContextInitializer = () => {
if (api && library) {
CarPlay.setRootTemplate(
- CarPlayNavigation(
- library,
- loadNewQueue,
- api,
- downloadedTracks,
- networkStatus,
- deviceProfile,
- downloadQuality,
- ),
+ CarPlayNavigation(library, loadNewQueue, api, networkStatus, deviceProfile),
)
if (Platform.OS === 'ios') {
diff --git a/src/providers/Player/functions/queue.ts b/src/providers/Player/functions/queue.ts
index 8df57ce0..5bcd8229 100644
--- a/src/providers/Player/functions/queue.ts
+++ b/src/providers/Player/functions/queue.ts
@@ -7,8 +7,13 @@ import { shuffleJellifyTracks } from '../utils/shuffle'
import TrackPlayer from 'react-native-track-player'
import Toast from 'react-native-toast-message'
import { findPlayQueueIndexStart } from '../utils'
-import JellifyTrack from '@/src/types/JellifyTrack'
+import JellifyTrack from '../../../types/JellifyTrack'
import { setPlayQueue, setQueueRef, setShuffled, setUnshuffledQueue } from '.'
+import { JellifyDownload } from '../../../types/JellifyDownload'
+
+type LoadQueueOperation = QueueMutation & {
+ downloadedTracks: JellifyDownload[] | undefined
+}
export async function loadQueue({
index,
@@ -19,7 +24,7 @@ export async function loadQueue({
deviceProfile,
networkStatus = networkStatusTypes.ONLINE,
downloadedTracks,
-}: QueueMutation) {
+}: LoadQueueOperation) {
setQueueRef(queueRef)
setShuffled(shuffled)
@@ -97,6 +102,10 @@ export async function loadQueue({
return finalStartIndex
}
+
+type PlayNextOperation = AddToQueueMutation & {
+ downloadedTracks: JellifyDownload[] | undefined
+}
/**
* Inserts a track at the next index in the queue
*
@@ -109,7 +118,7 @@ export const playNextInQueue = async ({
downloadedTracks,
deviceProfile,
tracks,
-}: AddToQueueMutation) => {
+}: PlayNextOperation) => {
console.debug(`Playing item next in queue`)
const tracksToPlayNext = tracks.map((item) =>
@@ -135,12 +144,16 @@ export const playNextInQueue = async ({
})
}
+type QueueOperation = AddToQueueMutation & {
+ downloadedTracks: JellifyDownload[] | undefined
+}
+
export const playInQueue = async ({
api,
deviceProfile,
downloadedTracks,
tracks,
-}: AddToQueueMutation) => {
+}: QueueOperation) => {
const playQueue = await TrackPlayer.getQueue()
const currentIndex = await TrackPlayer.getActiveTrackIndex()
diff --git a/src/providers/Player/hooks/mutations.ts b/src/providers/Player/hooks/mutations.ts
index 2c4ce01e..91eec913 100644
--- a/src/providers/Player/hooks/mutations.ts
+++ b/src/providers/Player/hooks/mutations.ts
@@ -4,7 +4,7 @@ import { loadQueue, playInQueue, playNextInQueue } from '../functions/queue'
import { trigger } from 'react-native-haptic-feedback'
import { isUndefined } from 'lodash'
import { previous, skip } from '../functions/controls'
-import { AddToQueueMutation, QueueOrderMutation } from '../interfaces'
+import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../interfaces'
import { refetchNowPlaying, refetchPlayerQueue, invalidateRepeatMode } from '../functions/queries'
import { QueuingType } from '../../../enums/queuing-type'
import Toast from 'react-native-toast-message'
@@ -18,12 +18,13 @@ import {
import { handleDeshuffle, handleShuffle } from '../functions/shuffle'
import JellifyTrack from '@/src/types/JellifyTrack'
import calculateTrackVolume from '../utils/normalization'
-import { useNowPlaying, usePlaybackState } from './queries'
+import { usePlaybackState } from './queries'
import usePlayerEngineStore, { PlayerEngine } from '../../../stores/player-engine'
import { useRemoteMediaClient } from 'react-native-google-cast'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { RootStackParamList } from '../../../screens/types'
import { useNavigation } from '@react-navigation/native'
+import { useAllDownloadedTracks } from '../../../api/queries/download'
const PLAYER_MUTATION_OPTIONS = {
retry: false,
@@ -170,12 +171,14 @@ const useSeekBy = () =>
},
})
-export const useAddToQueue = () =>
- useMutation({
+export const useAddToQueue = () => {
+ const downloadedTracks = useAllDownloadedTracks().data
+
+ return useMutation({
mutationFn: (variables: AddToQueueMutation) =>
variables.queuingType === QueuingType.PlayingNext
- ? playNextInQueue(variables)
- : playInQueue(variables),
+ ? playNextInQueue({ ...variables, downloadedTracks })
+ : playInQueue({ ...variables, downloadedTracks }),
onSuccess: (data: void, { queuingType }: AddToQueueMutation) => {
trigger('notificationSuccess')
console.debug(
@@ -202,6 +205,7 @@ export const useAddToQueue = () =>
},
onSettled: refetchPlayerQueue,
})
+}
export const useLoadNewQueue = () => {
const isCasting =
@@ -209,12 +213,14 @@ export const useLoadNewQueue = () => {
const remoteClient = useRemoteMediaClient()
const navigation = useNavigation>()
+ const { data: downloadedTracks } = useAllDownloadedTracks()
+
return useMutation({
onMutate: async () => {
trigger('impactLight')
await TrackPlayer.pause()
},
- mutationFn: loadQueue,
+ mutationFn: (variables: QueueMutation) => loadQueue({ ...variables, downloadedTracks }),
onSuccess: async (finalStartIndex, { startPlayback }) => {
console.debug('Successfully loaded new queue')
if (isCasting && remoteClient) {
diff --git a/src/providers/Player/index.tsx b/src/providers/Player/index.tsx
index 35d99ad4..ad090030 100644
--- a/src/providers/Player/index.tsx
+++ b/src/providers/Player/index.tsx
@@ -6,7 +6,6 @@ import { useEffect } from 'react'
import { useAudioNormalization, useInitialization } from './hooks/mutations'
import { useCurrentIndex, useNowPlaying, useQueue } from './hooks/queries'
import { handleActiveTrackChanged } from './functions'
-import { useAutoDownloadContext } from '../Settings'
import JellifyTrack from '../../types/JellifyTrack'
import { useIsRestoring } from '@tanstack/react-query'
import {
@@ -15,6 +14,7 @@ import {
useReportPlaybackStopped,
} from '../../api/mutations/playback'
import { useDownloadAudioItem } from '../../api/mutations/download'
+import { useAutoDownload } from '../../stores/settings/usage'
const PLAYER_EVENTS: Event[] = [
Event.PlaybackActiveTrackChanged,
@@ -27,7 +27,7 @@ interface PlayerContext {}
export const PlayerContext = createContext({})
export const PlayerProvider: () => React.JSX.Element = () => {
- const autoDownload = useAutoDownloadContext()
+ const [autoDownload] = useAutoDownload()
usePerformanceMonitor('PlayerProvider', 3)
diff --git a/src/providers/Player/interfaces.ts b/src/providers/Player/interfaces.ts
index c42f5006..c8106b8b 100644
--- a/src/providers/Player/interfaces.ts
+++ b/src/providers/Player/interfaces.ts
@@ -4,7 +4,6 @@ import { Queue } from '../../player/types/queue-item'
import { Api } from '@jellyfin/sdk'
import { networkStatusTypes } from '../../components/Network/internetConnectionWatcher'
import { JellifyDownload } from '@/src/types/JellifyDownload'
-import { DownloadQuality, StreamingQuality } from '../Settings'
/**
* A mutation to handle loading a new queue.
@@ -21,10 +20,6 @@ export interface QueueMutation {
*/
networkStatus: networkStatusTypes | null
- downloadedTracks: JellifyDownload[] | undefined
-
- downloadQuality: DownloadQuality
-
deviceProfile: DeviceProfile | undefined
/**
@@ -76,10 +71,6 @@ export interface AddToQueueMutation {
*/
networkStatus: networkStatusTypes | null
- downloadedTracks: JellifyDownload[] | undefined
-
- downloadQuality: DownloadQuality
-
deviceProfile: DeviceProfile | undefined
/**
diff --git a/src/providers/Settings/index.tsx b/src/providers/Settings/index.tsx
deleted file mode 100644
index 5560cadc..00000000
--- a/src/providers/Settings/index.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { Platform } from 'react-native'
-import { storage } from '../../constants/storage'
-import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
-import { useEffect, useState, useMemo } from 'react'
-import { createContext, useContextSelector } from 'use-context-selector'
-import {
- useDownloadingDeviceProfileStore,
- useStreamingDeviceProfileStore,
-} from '../../stores/device-profile'
-import { getDeviceProfile } from './utils'
-
-export type DownloadQuality = 'original' | 'high' | 'medium' | 'low'
-export type StreamingQuality = 'original' | 'high' | 'medium' | 'low'
-export type Theme = 'system' | 'light' | 'dark'
-
-interface SettingsContext {
- sendMetrics: boolean
- setSendMetrics: React.Dispatch>
- autoDownload: boolean
- setAutoDownload: React.Dispatch>
- devTools: boolean
- setDevTools: React.Dispatch>
- downloadQuality: DownloadQuality
- setDownloadQuality: React.Dispatch>
- streamingQuality: StreamingQuality
- setStreamingQuality: React.Dispatch>
- reducedHaptics: boolean
- setReducedHaptics: React.Dispatch>
- theme: Theme
- setTheme: React.Dispatch>
-}
-
-/**
- * Initializes the settings context
- *
- * By default, auto-download is enabled on iOS and Android
- *
- * By default, metrics and logs are not sent
- *
- * By default, streaming quality is set to 'high' for good balance of quality and bandwidth
- *
- * Settings are saved to the device storage
- *
- * @returns The settings context
- */
-const SettingsContextInitializer = () => {
- const sendMetricsInit = storage.getBoolean(MMKVStorageKeys.SendMetrics)
- const autoDownloadInit = storage.getBoolean(MMKVStorageKeys.AutoDownload)
- const devToolsInit = storage.getBoolean(MMKVStorageKeys.DevTools)
- const reducedHapticsInit = storage.getBoolean(MMKVStorageKeys.ReducedHaptics)
- const themeInit = storage.getString(MMKVStorageKeys.Theme) as Theme
-
- const downloadQualityInit = storage.getString(
- MMKVStorageKeys.DownloadQuality,
- ) as DownloadQuality
-
- const streamingQualityInit = storage.getString(
- MMKVStorageKeys.StreamingQuality,
- ) as StreamingQuality
-
- const [sendMetrics, setSendMetrics] = useState(sendMetricsInit ?? false)
-
- const [autoDownload, setAutoDownload] = useState(
- autoDownloadInit ?? ['ios', 'android'].includes(Platform.OS),
- )
- const [devTools, setDevTools] = useState(false)
-
- const [downloadQuality, setDownloadQuality] = useState(
- downloadQualityInit ?? 'original',
- )
-
- const [streamingQuality, setStreamingQuality] = useState(
- streamingQualityInit ?? 'original',
- )
-
- const [reducedHaptics, setReducedHaptics] = useState(
- reducedHapticsInit ?? (Platform.OS !== 'ios' && Math.random() > 0.7),
- )
-
- const [theme, setTheme] = useState(themeInit ?? 'system')
-
- const setStreamingDeviceProfile = useStreamingDeviceProfileStore(
- (state) => state.setDeviceProfile,
- )
- const setDownloadingDeviceProfile = useDownloadingDeviceProfileStore(
- (state) => state.setDeviceProfile,
- )
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.SendMetrics, sendMetrics)
- }, [sendMetrics])
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.AutoDownload, autoDownload)
- }, [autoDownload])
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.DownloadQuality, downloadQuality)
-
- setDownloadingDeviceProfile(getDeviceProfile(downloadQuality, 'download'))
- }, [downloadQuality])
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.StreamingQuality, streamingQuality)
-
- setStreamingDeviceProfile(getDeviceProfile(streamingQuality, 'stream'))
- }, [streamingQuality])
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.DevTools, devTools)
- }, [devTools])
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.ReducedHaptics, reducedHaptics)
- }, [reducedHaptics])
-
- useEffect(() => {
- storage.set(MMKVStorageKeys.Theme, theme)
- }, [theme])
-
- return {
- sendMetrics,
- setSendMetrics,
- autoDownload,
- setAutoDownload,
- devTools,
- setDevTools,
- downloadQuality,
- setDownloadQuality,
- streamingQuality,
- setStreamingQuality,
- reducedHaptics,
- setReducedHaptics,
- theme,
- setTheme,
- }
-}
-
-export const SettingsContext = createContext({
- sendMetrics: false,
- setSendMetrics: () => {},
- autoDownload: false,
- setAutoDownload: () => {},
- devTools: false,
- setDevTools: () => {},
- downloadQuality: 'medium',
- setDownloadQuality: () => {},
- streamingQuality: 'high',
- setStreamingQuality: () => {},
- reducedHaptics: false,
- setReducedHaptics: () => {},
- theme: 'system',
- setTheme: () => {},
-})
-
-export const SettingsProvider = ({ children }: { children: React.ReactNode }) => {
- const context = SettingsContextInitializer()
-
- // Memoize the context value to prevent unnecessary re-renders
- const value = useMemo(
- () => context,
- [
- context.sendMetrics,
- context.autoDownload,
- context.devTools,
- context.downloadQuality,
- context.streamingQuality,
- context.reducedHaptics,
- context.theme,
- ],
- )
-
- return {children}
-}
-
-export const useSendMetricsContext = () =>
- useContextSelector(SettingsContext, (context) => context.sendMetrics)
-export const useSetSendMetricsContext = () =>
- useContextSelector(SettingsContext, (context) => context.setSendMetrics)
-
-export const useAutoDownloadContext = () =>
- useContextSelector(SettingsContext, (context) => context.autoDownload)
-export const useSetAutoDownloadContext = () =>
- useContextSelector(SettingsContext, (context) => context.setAutoDownload)
-
-export const useDevToolsContext = () =>
- useContextSelector(SettingsContext, (context) => context.devTools)
-export const useSetDevToolsContext = () =>
- useContextSelector(SettingsContext, (context) => context.setDevTools)
-
-export const useDownloadQualityContext = () =>
- useContextSelector(SettingsContext, (context) => context.downloadQuality)
-export const useSetDownloadQualityContext = () =>
- useContextSelector(SettingsContext, (context) => context.setDownloadQuality)
-
-export const useStreamingQualityContext = () =>
- useContextSelector(SettingsContext, (context) => context.streamingQuality)
-export const useSetStreamingQualityContext = () =>
- useContextSelector(SettingsContext, (context) => context.setStreamingQuality)
-
-export const useReducedHapticsContext = () =>
- useContextSelector(SettingsContext, (context) => context.reducedHaptics)
-export const useSetReducedHapticsContext = () =>
- useContextSelector(SettingsContext, (context) => context.setReducedHaptics)
-
-export const useThemeSettingContext = () =>
- useContextSelector(SettingsContext, (context) => context.theme)
-export const useSetThemeSettingContext = () =>
- useContextSelector(SettingsContext, (context) => context.setTheme)
diff --git a/src/screens/Login/server-address.tsx b/src/screens/Login/server-address.tsx
index 995331cf..f1ca776b 100644
--- a/src/screens/Login/server-address.tsx
+++ b/src/screens/Login/server-address.tsx
@@ -12,13 +12,13 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { RootStackParamList } from '../types'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '../../providers'
-import { useSendMetricsContext, useSetSendMetricsContext } from '../../providers/Settings'
import Icon from '../../components/Global/components/icon'
import { PublicSystemInfo } from '@jellyfin/sdk/lib/generated-client/models'
import { connectToServer } from '../../api/mutations/login'
import { IS_MAESTRO_BUILD } from '../../configs/config'
import { sleepify } from '../../utils/sleep'
import LoginStackParamList from './types'
+import { useSendMetricsSetting } from '../../stores/settings/app'
export default function ServerAddress({
navigation,
@@ -34,8 +34,7 @@ export default function ServerAddress({
const { server, setServer, signOut } = useJellifyContext()
- const sendMetrics = useSendMetricsContext()
- const setSendMetrics = useSetSendMetricsContext()
+ const [sendMetrics, setSendMetrics] = useSendMetricsSetting()
useEffect(() => {
setServerAddressContainsProtocol(
diff --git a/src/stores/device-profile.ts b/src/stores/device-profile.ts
index b1bc3aed..ea2c9ff4 100644
--- a/src/stores/device-profile.ts
+++ b/src/stores/device-profile.ts
@@ -1,6 +1,7 @@
import { DeviceProfile } from '@jellyfin/sdk/lib/generated-client'
import { create } from 'zustand'
-import { devtools, persist } from 'zustand/middleware'
+import { createJSONStorage, devtools, persist } from 'zustand/middleware'
+import { stateStorage } from '../constants/storage'
type DeviceProfileStore = {
deviceProfile: DeviceProfile
@@ -16,6 +17,7 @@ export const useStreamingDeviceProfileStore = create()(
}),
{
name: 'streaming-device-profile-storage',
+ storage: createJSONStorage(() => stateStorage),
},
),
),
@@ -36,6 +38,7 @@ export const useDownloadingDeviceProfileStore = create()(
}),
{
name: 'downloading-device-profile-storage',
+ storage: createJSONStorage(() => stateStorage),
},
),
),
diff --git a/src/stores/player-settings.ts b/src/stores/player-settings.ts
deleted file mode 100644
index 594ac3fb..00000000
--- a/src/stores/player-settings.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { create } from 'zustand'
-import { devtools, persist } from 'zustand/middleware'
-
-type PlayerSettingsStore = {
- displayAudioQualityBadge: boolean
- setDisplayAudioQualityBadge: (displayAudioQualityBadge: boolean) => void
-}
-
-export const usePlayerSettingsStore = create()(
- devtools(
- persist(
- (set) => ({
- displayAudioQualityBadge: false,
- setDisplayAudioQualityBadge: (displayAudioQualityBadge) =>
- set({ displayAudioQualityBadge }),
- }),
- {
- name: 'player-settings-storage',
- },
- ),
- ),
-)
-
-export const useDisplayAudioQualityBadge: () => [
- boolean,
- (displayAudioQualityBadge: boolean) => void,
-] = () => {
- const displayAudioQualityBadge = usePlayerSettingsStore(
- (state) => state.displayAudioQualityBadge,
- )
-
- const setDisplayAudioQualityBadge = usePlayerSettingsStore(
- (state) => state.setDisplayAudioQualityBadge,
- )
-
- return [displayAudioQualityBadge, setDisplayAudioQualityBadge]
-}
-export const useSetDisplayAudioQualityBadge = () =>
- usePlayerSettingsStore((state) => state.setDisplayAudioQualityBadge)
diff --git a/src/stores/settings/app.ts b/src/stores/settings/app.ts
new file mode 100644
index 00000000..4bdbbd6f
--- /dev/null
+++ b/src/stores/settings/app.ts
@@ -0,0 +1,60 @@
+import { stateStorage } from '../../constants/storage'
+import { create } from 'zustand'
+import { createJSONStorage, devtools, persist } from 'zustand/middleware'
+
+export type ThemeSetting = 'system' | 'light' | 'dark'
+
+type AppSettingsStore = {
+ sendMetrics: boolean
+ setSendMetrics: (sendMetrics: boolean) => void
+
+ reducedHaptics: boolean
+ setReducedHaptics: (reducedHaptics: boolean) => void
+
+ theme: ThemeSetting
+ setTheme: (theme: ThemeSetting) => void
+}
+
+export const useAppSettingsStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ sendMetrics: false,
+ setSendMetrics: (sendMetrics) => set({ sendMetrics }),
+
+ reducedHaptics: false,
+ setReducedHaptics: (reducedHaptics) => set({ reducedHaptics }),
+
+ theme: 'system',
+ setTheme: (theme) => set({ theme }),
+ }),
+ {
+ name: 'app-settings-storage',
+ storage: createJSONStorage(() => stateStorage),
+ },
+ ),
+ ),
+)
+
+export const useThemeSetting: () => [ThemeSetting, (theme: ThemeSetting) => void] = () => {
+ const theme = useAppSettingsStore((state) => state.theme)
+
+ const setTheme = useAppSettingsStore((state) => state.setTheme)
+
+ return [theme, setTheme]
+}
+export const useReducedHapticsSetting: () => [boolean, (reducedHaptics: boolean) => void] = () => {
+ const reducedHaptics = useAppSettingsStore((state) => state.reducedHaptics)
+
+ const setReducedHaptics = useAppSettingsStore((state) => state.setReducedHaptics)
+
+ return [reducedHaptics, setReducedHaptics]
+}
+
+export const useSendMetricsSetting: () => [boolean, (sendMetrics: boolean) => void] = () => {
+ const sendMetrics = useAppSettingsStore((state) => state.sendMetrics)
+
+ const setSendMetrics = useAppSettingsStore((state) => state.setSendMetrics)
+
+ return [sendMetrics, setSendMetrics]
+}
diff --git a/src/stores/settings/player.ts b/src/stores/settings/player.ts
new file mode 100644
index 00000000..78019e3f
--- /dev/null
+++ b/src/stores/settings/player.ts
@@ -0,0 +1,74 @@
+import { create } from 'zustand'
+import { createJSONStorage, devtools, persist } from 'zustand/middleware'
+import { stateStorage } from '../../constants/storage'
+import { useStreamingDeviceProfileStore } from '../device-profile'
+import { useEffect } from 'react'
+import { getDeviceProfile } from '../../utils/device-profiles'
+
+export enum StreamingQuality {
+ Original = 'original', // Direct Play
+ High = 'high', // 320
+ Medium = 'medium', // 256
+ Low = 'low', // 128
+}
+
+type PlayerSettingsStore = {
+ streamingQuality: StreamingQuality
+ setStreamingQuality: (streamingQuality: StreamingQuality) => void
+
+ displayAudioQualityBadge: boolean
+ setDisplayAudioQualityBadge: (displayAudioQualityBadge: boolean) => void
+}
+
+export const usePlayerSettingsStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ streamingQuality: StreamingQuality.Original,
+ setStreamingQuality: (streamingQuality) => set({ streamingQuality }),
+
+ displayAudioQualityBadge: false,
+ setDisplayAudioQualityBadge: (displayAudioQualityBadge) =>
+ set({ displayAudioQualityBadge }),
+ }),
+ {
+ name: 'player-settings-storage',
+ storage: createJSONStorage(() => stateStorage),
+ },
+ ),
+ ),
+)
+
+export const useStreamingQuality: () => [
+ StreamingQuality,
+ (streamingQuality: StreamingQuality) => void,
+] = () => {
+ const streamingQuality = usePlayerSettingsStore((state) => state.streamingQuality)
+
+ const setStreamingQuality = usePlayerSettingsStore((state) => state.setStreamingQuality)
+
+ const setStreamingDeviceProfile = useStreamingDeviceProfileStore(
+ (state) => state.setDeviceProfile,
+ )
+
+ useEffect(() => {
+ setStreamingDeviceProfile(getDeviceProfile(streamingQuality, 'stream'))
+ }, [streamingQuality])
+
+ return [streamingQuality, setStreamingQuality]
+}
+
+export const useDisplayAudioQualityBadge: () => [
+ boolean,
+ (displayAudioQualityBadge: boolean) => void,
+] = () => {
+ const displayAudioQualityBadge = usePlayerSettingsStore(
+ (state) => state.displayAudioQualityBadge,
+ )
+
+ const setDisplayAudioQualityBadge = usePlayerSettingsStore(
+ (state) => state.setDisplayAudioQualityBadge,
+ )
+
+ return [displayAudioQualityBadge, setDisplayAudioQualityBadge]
+}
diff --git a/src/stores/settings/usage.ts b/src/stores/settings/usage.ts
new file mode 100644
index 00000000..0c32a3f9
--- /dev/null
+++ b/src/stores/settings/usage.ts
@@ -0,0 +1,52 @@
+import { create } from 'zustand'
+import { StreamingQuality } from './player'
+import { createJSONStorage, devtools, persist } from 'zustand/middleware'
+import { Platform } from 'react-native'
+import { stateStorage } from '../../constants/storage'
+
+export type DownloadQuality = StreamingQuality
+
+type UsageSettingsStore = {
+ downloadQuality: DownloadQuality
+ setDownloadQuality: (downloadQuality: DownloadQuality) => void
+
+ autoDownload: boolean
+ setAutoDownload: (autoDownload: boolean) => void
+}
+
+export const useUsageSettingsStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ downloadQuality: StreamingQuality.Original,
+ setDownloadQuality: (downloadQuality: DownloadQuality) => set({ downloadQuality }),
+
+ autoDownload: Platform.OS === 'android' || Platform.OS === 'ios',
+ setAutoDownload: (autoDownload) => set({ autoDownload }),
+ }),
+ {
+ name: 'usage-settings-storage',
+ storage: createJSONStorage(() => stateStorage),
+ },
+ ),
+ ),
+)
+
+export const useAutoDownload: () => [boolean, (autoDownload: boolean) => void] = () => {
+ const autoDownload = useUsageSettingsStore((state) => state.autoDownload)
+
+ const setAutoDownload = useUsageSettingsStore((state) => state.setAutoDownload)
+
+ return [autoDownload, setAutoDownload]
+}
+
+export const useDownloadQuality: () => [
+ DownloadQuality,
+ (downloadQuality: DownloadQuality) => void,
+] = () => {
+ const downloadQuality = useUsageSettingsStore((state) => state.downloadQuality)
+
+ const setDownloadQuality = useUsageSettingsStore((state) => state.setDownloadQuality)
+
+ return [downloadQuality, setDownloadQuality]
+}
diff --git a/src/stores/streaming-quality.ts b/src/stores/streaming-quality.ts
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/providers/Settings/utils/index.ts b/src/utils/device-profiles.ts
similarity index 97%
rename from src/providers/Settings/utils/index.ts
rename to src/utils/device-profiles.ts
index bfd03351..86e14207 100644
--- a/src/providers/Settings/utils/index.ts
+++ b/src/utils/device-profiles.ts
@@ -19,11 +19,11 @@ import {
EncodingContext,
MediaStreamProtocol,
} from '@jellyfin/sdk/lib/generated-client'
-import { StreamingQuality } from '..'
+import { StreamingQuality } from '../stores/settings/player'
import { Platform } from 'react-native'
-import { getQualityParams } from '../../../utils/mappings'
+import { getQualityParams } from './mappings'
import { capitalize } from 'lodash'
-import { SourceType } from '../../../types/JellifyTrack'
+import { SourceType } from '../types/JellifyTrack'
/**
* A constant that defines the options for the {@link useDeviceProfile} hook - building the
diff --git a/src/utils/mappings.ts b/src/utils/mappings.ts
index 90577b38..fea35623 100644
--- a/src/utils/mappings.ts
+++ b/src/utils/mappings.ts
@@ -13,13 +13,14 @@ import { AudioApi } from '@jellyfin/sdk/lib/generated-client/api'
import { JellifyDownload } from '../types/JellifyDownload'
import { Api } from '@jellyfin/sdk/lib/api'
import RNFS from 'react-native-fs'
-import { DownloadQuality, StreamingQuality } from '../providers/Settings'
+import { StreamingQuality } from '../stores/settings/player'
import { AudioQuality } from '../types/AudioQuality'
import { queryClient } from '../constants/query-client'
import { QueryKeys } from '../enums/query-keys'
import { isUndefined } from 'lodash'
import uuid from 'react-native-uuid'
import { convertRunTimeTicksToSeconds } from './runtimeticks'
+import { DownloadQuality } from '../stores/settings/usage'
/**
* Gets quality-specific parameters for transcoding
diff --git a/src/utils/random.ts b/src/utils/random.ts
new file mode 100644
index 00000000..40dab9d9
--- /dev/null
+++ b/src/utils/random.ts
@@ -0,0 +1,3 @@
+export function pickRandomItemFromArray(array: readonly T[]): T {
+ return array[Math.max(0, Math.min(array.length - 1, Math.floor(Math.random() * array.length)))]
+}