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)))] +}