mirror of
https://github.com/Jellify-Music/App.git
synced 2025-12-30 23:39:51 -06:00
Settings refactor to zustand + MMKV (#495)
*Mostly* backend work in the settings tab Migrate settings context over to MMKV backed Zustand store. Update hooks as needed Update info tab styling - making the patreon patrons list it's own section. A perfect time to remind everyone you can sponsor Jellify on GitHub or Patreon :) Joining the patreon as a paid member will get your name displayed in the info area of the settings tab
This commit is contained in:
12
App.tsx
12
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 {
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{
|
||||
persister: clientPersister,
|
||||
persister: queryClientPersister,
|
||||
|
||||
/**
|
||||
* Maximum query data age of one day
|
||||
@@ -88,9 +88,7 @@ export default function App(): React.JSX.Element {
|
||||
maxAge: Infinity,
|
||||
}}
|
||||
>
|
||||
<SettingsProvider>
|
||||
<Container playerIsReady={playerIsReady} />
|
||||
</SettingsProvider>
|
||||
<Container playerIsReady={playerIsReady} />
|
||||
</PersistQueryClientProvider>
|
||||
</ErrorBoundary>
|
||||
</SafeAreaProvider>
|
||||
@@ -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'
|
||||
|
||||
|
||||
19
src/api/queries/patrons/index.ts
Normal file
19
src/api/queries/patrons/index.ts
Normal file
@@ -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
|
||||
@@ -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<Patron
|
||||
if (!api) return reject(new Error('No API instance provided'))
|
||||
|
||||
api.axiosInstance
|
||||
.get('https://patrons.jellify.app')
|
||||
.get(PATRON_API_ENDPOINT)
|
||||
.then((res) => {
|
||||
const patrons = res.data as Patron[]
|
||||
resolve(patrons)
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<NativeStackNavigationProp<BaseStackParamList>, '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 <LinearGradient style={{ flex: 1 }} colors={gradientColors} />
|
||||
}
|
||||
|
||||
function DownloadMenuRow({ items }: { items: BaseItemDto[] }): React.JSX.Element {
|
||||
const { api } = useJellifyContext()
|
||||
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<NativeStackNavigationProp<HomeStackParamList>>()
|
||||
@@ -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],
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<NativeStackNavigationProp<LibraryStackParamList>>()
|
||||
|
||||
const downloadPlaylist = () => {
|
||||
@@ -178,9 +173,7 @@ function PlaylistHeaderControls({
|
||||
|
||||
loadNewQueue({
|
||||
api,
|
||||
downloadQuality,
|
||||
networkStatus,
|
||||
downloadedTracks,
|
||||
deviceProfile: streamingDeviceProfile,
|
||||
track: playlistTracks[0],
|
||||
index: 0,
|
||||
|
||||
@@ -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 (
|
||||
<SafeAreaView style={{ flex: 1 }} edges={['top']}>
|
||||
<SettingsTabsNavigator.Navigator
|
||||
@@ -108,7 +104,7 @@ export default function Settings(): React.JSX.Element {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{devTools && (
|
||||
{/*
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Labs'
|
||||
component={LabsTab}
|
||||
@@ -122,7 +118,7 @@ export default function Settings(): React.JSX.Element {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) */}
|
||||
</SettingsTabsNavigator.Navigator>
|
||||
</SafeAreaView>
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 (
|
||||
<ScrollView contentInsetAdjustmentBehavior='automatic'>
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: `Jellify ${version}`,
|
||||
subTitle: 'Made with love',
|
||||
subTitle: caption,
|
||||
iconName: 'jellyfish-outline',
|
||||
iconColor: '$secondary',
|
||||
children: (
|
||||
@@ -83,50 +74,42 @@ export default function InfoTabIndex({ navigation }: InfoTabNativeStackNavigatio
|
||||
},
|
||||
{
|
||||
title: 'Powered by listeners like you',
|
||||
subTitle: 'Sponsor Jellify on GitHub or Patreon',
|
||||
subTitle: 'Sponsor on GitHub or Patreon',
|
||||
iconName: 'heart',
|
||||
iconColor: '$primary',
|
||||
children: (
|
||||
<FlatList
|
||||
<XStack justifyContent='flex-start' gap={'$4'} marginVertical={'$2'}>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
'https://github.com/sponsors/anultravioletaurora/',
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon name='github' small color='$borderColor' />
|
||||
<Text>Sponsors</Text>
|
||||
</XStack>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
onPress={() =>
|
||||
Linking.openURL('https://patreon.com/anultravioletaurora')
|
||||
}
|
||||
>
|
||||
<Icon name='patreon' small color='$borderColor' />
|
||||
<Text>Patreon</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Patreon Wall of Fame',
|
||||
subTitle: 'Thank you to these paid members',
|
||||
iconName: 'patreon',
|
||||
iconColor: '$primary',
|
||||
children: (
|
||||
<FlashList
|
||||
data={patrons}
|
||||
ListHeaderComponent={
|
||||
<YStack>
|
||||
<XStack
|
||||
justifyContent='flex-start'
|
||||
gap={'$4'}
|
||||
marginVertical={'$2'}
|
||||
>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
gap={'$2'}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
'https://github.com/sponsors/anultravioletaurora/',
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon name='github' small color='$borderColor' />
|
||||
<Text>Sponsors</Text>
|
||||
</XStack>
|
||||
<XStack
|
||||
alignItems='center'
|
||||
gap={'$2'}
|
||||
onPress={() =>
|
||||
Linking.openURL(
|
||||
'https://patreon.com/anultravioletaurora',
|
||||
)
|
||||
}
|
||||
>
|
||||
<Icon name='patreon' small color='$borderColor' />
|
||||
<Text>Patreon</Text>
|
||||
</XStack>
|
||||
</XStack>
|
||||
|
||||
<Separator marginBottom={'$3'} />
|
||||
|
||||
<Text fontSize={'$5'}>Patreon Wall of Fame</Text>
|
||||
</YStack>
|
||||
}
|
||||
numColumns={1}
|
||||
renderItem={({ item }) => (
|
||||
<XStack alignItems='flex-start' maxWidth={'$20'}>
|
||||
|
||||
@@ -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: (
|
||||
<YStack gap='$2' paddingVertical='$2'>
|
||||
<Text fontSize='$3' marginBottom='$2'>
|
||||
Higher quality uses more bandwidth. Changes apply to new tracks.
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={streamingQuality}
|
||||
onValueChange={(value) =>
|
||||
setStreamingQuality(value as StreamingQuality)
|
||||
}
|
||||
>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='original'
|
||||
label='Original Quality (Highest bandwidth)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='high'
|
||||
label='High (320kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='medium'
|
||||
label='Medium (192kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='low'
|
||||
label='Low (128kbps)'
|
||||
/>
|
||||
</RadioGroup>
|
||||
</YStack>
|
||||
<RadioGroup
|
||||
value={streamingQuality}
|
||||
onValueChange={(value) =>
|
||||
setStreamingQuality(value as StreamingQuality)
|
||||
}
|
||||
>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value={StreamingQuality.Original}
|
||||
label='Original Quality (Highest bandwidth)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value={StreamingQuality.High}
|
||||
label='High (320kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value={StreamingQuality.Medium}
|
||||
label='Medium (192kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value={StreamingQuality.Low}
|
||||
label='Low (128kbps)'
|
||||
/>
|
||||
</RadioGroup>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Show Audio Quality Badge',
|
||||
subTitle: 'Displays audio quality in the player',
|
||||
iconName: 'sine-wave',
|
||||
iconColor: '$borderColor',
|
||||
iconColor: displayAudioQualityBadge ? '$success' : '$borderColor',
|
||||
children: (
|
||||
<SwitchWithLabel
|
||||
onCheckedChange={setDisplayAudioQualityBadge}
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { RadioGroup, YStack } from 'tamagui'
|
||||
import {
|
||||
Theme,
|
||||
useReducedHapticsContext,
|
||||
useSendMetricsContext,
|
||||
useSetReducedHapticsContext,
|
||||
useSetSendMetricsContext,
|
||||
useSetThemeSettingContext,
|
||||
useThemeSettingContext,
|
||||
} from '../../../providers/Settings'
|
||||
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
import { RadioGroupItemWithLabel } from '../../Global/helpers/radio-group-item-with-label'
|
||||
import {
|
||||
ThemeSetting,
|
||||
useReducedHapticsSetting,
|
||||
useSendMetricsSetting,
|
||||
useThemeSetting,
|
||||
} from '../../../stores/settings/app'
|
||||
|
||||
export default function PreferencesTab(): React.JSX.Element {
|
||||
const setSendMetrics = useSetSendMetricsContext()
|
||||
const sendMetrics = useSendMetricsContext()
|
||||
const setReducedHaptics = useSetReducedHapticsContext()
|
||||
const reducedHaptics = useReducedHapticsContext()
|
||||
const themeSetting = useThemeSettingContext()
|
||||
const setThemeSetting = useSetThemeSettingContext()
|
||||
const [sendMetrics, setSendMetrics] = useSendMetricsSetting()
|
||||
const [reducedHaptics, setReducedHaptics] = useReducedHapticsSetting()
|
||||
const [themeSetting, setThemeSetting] = useThemeSetting()
|
||||
|
||||
return (
|
||||
<SettingsListGroup
|
||||
@@ -32,7 +26,7 @@ export default function PreferencesTab(): React.JSX.Element {
|
||||
<YStack gap='$2' paddingVertical='$2'>
|
||||
<RadioGroup
|
||||
value={themeSetting}
|
||||
onValueChange={(value) => setThemeSetting(value as Theme)}
|
||||
onValueChange={(value) => setThemeSetting(value as ThemeSetting)}
|
||||
>
|
||||
<RadioGroupItemWithLabel size='$3' value='system' label='System' />
|
||||
<RadioGroupItemWithLabel size='$3' value='light' label='Light' />
|
||||
|
||||
@@ -17,7 +17,12 @@ export default function SettingsListGroup({
|
||||
}: SettingsListGroupProps): React.JSX.Element {
|
||||
return (
|
||||
<ScrollView>
|
||||
<YGroup alignSelf='center'>
|
||||
<YGroup
|
||||
alignSelf='center'
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
margin={'$3'}
|
||||
>
|
||||
{settingsList.map((setting, index, self) => (
|
||||
<>
|
||||
<YGroup.Item key={setting.title}>
|
||||
|
||||
@@ -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 (
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Downloaded Tracks',
|
||||
subTitle: `${downloadedTracks?.length ?? '0'} ${
|
||||
downloadedTracks?.length === 1 ? 'song' : 'songs'
|
||||
} in your pocket`,
|
||||
iconName: 'harddisk',
|
||||
iconColor: '$borderColor',
|
||||
},
|
||||
{
|
||||
title: 'Automatically Cache Tracks',
|
||||
subTitle: 'Download tracks as they are played',
|
||||
iconName: autoDownload ? 'cloud-download' : 'cloud-off-outline',
|
||||
iconColor: autoDownload ? '$success' : '$borderColor',
|
||||
children: (
|
||||
<SwitchWithLabel
|
||||
size={'$2'}
|
||||
label={autoDownload ? 'Enabled' : 'Disabled'}
|
||||
checked={autoDownload}
|
||||
onCheckedChange={() => setAutoDownload(!autoDownload)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Download Quality',
|
||||
subTitle: `Current: ${getQualityLabel(downloadQuality)} • For offline tracks`,
|
||||
iconName: 'file-download',
|
||||
iconColor: '$primary',
|
||||
children: (
|
||||
<YStack gap='$2' paddingVertical='$2'>
|
||||
<Text fontSize='$3' marginBottom='$2'>
|
||||
Quality used when saving tracks for offline use.
|
||||
</Text>
|
||||
<RadioGroup
|
||||
value={downloadQuality}
|
||||
onValueChange={(value) =>
|
||||
setDownloadQuality(value as DownloadQuality)
|
||||
}
|
||||
>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='original'
|
||||
label='Original Quality'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='high'
|
||||
label='High (320kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='medium'
|
||||
label='Medium (192kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='low'
|
||||
label='Low (128kbps)'
|
||||
/>
|
||||
</RadioGroup>
|
||||
</YStack>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
74
src/components/Settings/components/usage-tab.tsx
Normal file
74
src/components/Settings/components/usage-tab.tsx
Normal file
@@ -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 (
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Downloaded Tracks',
|
||||
subTitle: `${downloadedTracks?.length ?? '0'} ${
|
||||
downloadedTracks?.length === 1 ? 'song' : 'songs'
|
||||
} in your pocket`,
|
||||
iconName: 'harddisk',
|
||||
iconColor: '$primary',
|
||||
},
|
||||
{
|
||||
title: 'Automatically Cache Tracks',
|
||||
subTitle: 'Download tracks as they are played',
|
||||
iconName: autoDownload ? 'cloud-download' : 'cloud-off-outline',
|
||||
iconColor: autoDownload ? '$success' : '$borderColor',
|
||||
children: (
|
||||
<SwitchWithLabel
|
||||
size={'$2'}
|
||||
label={autoDownload ? 'Enabled' : 'Disabled'}
|
||||
checked={autoDownload}
|
||||
onCheckedChange={() => setAutoDownload(!autoDownload)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Download Quality',
|
||||
subTitle: `Quality used when downloading tracks`,
|
||||
iconName: 'file-download',
|
||||
iconColor: '$primary',
|
||||
children: (
|
||||
<RadioGroup
|
||||
value={downloadQuality}
|
||||
onValueChange={(value) => setDownloadQuality(value as DownloadQuality)}
|
||||
>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='original'
|
||||
label='Original Quality'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='high'
|
||||
label='High (320kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel
|
||||
size='$3'
|
||||
value='medium'
|
||||
label='Medium (192kbps)'
|
||||
/>
|
||||
<RadioGroupItemWithLabel size='$3' value='low' label='Low (128kbps)' />
|
||||
</RadioGroup>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
2
src/components/Settings/types.d.ts
vendored
2
src/components/Settings/types.d.ts
vendored
@@ -2,7 +2,7 @@ export type SettingsTabList = {
|
||||
title: string
|
||||
iconName: string
|
||||
iconColor: ThemeTokens
|
||||
subTitle: string
|
||||
subTitle?: string
|
||||
children?: React.ReactNode
|
||||
onPress?: () => void
|
||||
}[]
|
||||
|
||||
23
src/components/Settings/utils/info-caption.ts
Normal file
23
src/components/Settings/utils/info-caption.ts
Normal file
@@ -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
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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<string> = storageFunctions
|
||||
|
||||
export const queryClientPersister = createAsyncStoragePersister({
|
||||
storage: clientStorage,
|
||||
})
|
||||
|
||||
export const stateStorage: StateStorage = storageFunctions
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<NativeStackNavigationProp<RootStackParamList>>()
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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<PlayerContext>({})
|
||||
|
||||
export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
const autoDownload = useAutoDownloadContext()
|
||||
const [autoDownload] = useAutoDownload()
|
||||
|
||||
usePerformanceMonitor('PlayerProvider', 3)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<React.SetStateAction<boolean>>
|
||||
autoDownload: boolean
|
||||
setAutoDownload: React.Dispatch<React.SetStateAction<boolean>>
|
||||
devTools: boolean
|
||||
setDevTools: React.Dispatch<React.SetStateAction<boolean>>
|
||||
downloadQuality: DownloadQuality
|
||||
setDownloadQuality: React.Dispatch<React.SetStateAction<DownloadQuality>>
|
||||
streamingQuality: StreamingQuality
|
||||
setStreamingQuality: React.Dispatch<React.SetStateAction<StreamingQuality>>
|
||||
reducedHaptics: boolean
|
||||
setReducedHaptics: React.Dispatch<React.SetStateAction<boolean>>
|
||||
theme: Theme
|
||||
setTheme: React.Dispatch<React.SetStateAction<Theme>>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<DownloadQuality>(
|
||||
downloadQualityInit ?? 'original',
|
||||
)
|
||||
|
||||
const [streamingQuality, setStreamingQuality] = useState<StreamingQuality>(
|
||||
streamingQualityInit ?? 'original',
|
||||
)
|
||||
|
||||
const [reducedHaptics, setReducedHaptics] = useState(
|
||||
reducedHapticsInit ?? (Platform.OS !== 'ios' && Math.random() > 0.7),
|
||||
)
|
||||
|
||||
const [theme, setTheme] = useState<Theme>(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<SettingsContext>({
|
||||
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 <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -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(
|
||||
|
||||
@@ -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<DeviceProfileStore>()(
|
||||
}),
|
||||
{
|
||||
name: 'streaming-device-profile-storage',
|
||||
storage: createJSONStorage(() => stateStorage),
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -36,6 +38,7 @@ export const useDownloadingDeviceProfileStore = create<DeviceProfileStore>()(
|
||||
}),
|
||||
{
|
||||
name: 'downloading-device-profile-storage',
|
||||
storage: createJSONStorage(() => stateStorage),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<PlayerSettingsStore>()(
|
||||
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)
|
||||
60
src/stores/settings/app.ts
Normal file
60
src/stores/settings/app.ts
Normal file
@@ -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<AppSettingsStore>()(
|
||||
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]
|
||||
}
|
||||
74
src/stores/settings/player.ts
Normal file
74
src/stores/settings/player.ts
Normal file
@@ -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<PlayerSettingsStore>()(
|
||||
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]
|
||||
}
|
||||
52
src/stores/settings/usage.ts
Normal file
52
src/stores/settings/usage.ts
Normal file
@@ -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<UsageSettingsStore>()(
|
||||
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]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
3
src/utils/random.ts
Normal file
3
src/utils/random.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function pickRandomItemFromArray<T>(array: readonly T[]): T {
|
||||
return array[Math.max(0, Math.min(array.length - 1, Math.floor(Math.random() * array.length)))]
|
||||
}
|
||||
Reference in New Issue
Block a user