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:
Violet Caulfield
2025-08-28 22:24:53 -05:00
committed by GitHub
parent bb895a580e
commit 74efea0ebc
46 changed files with 487 additions and 648 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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) {},

View File

@@ -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,

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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],

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"

View File

@@ -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,

View File

@@ -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>
)

View File

@@ -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'

View File

@@ -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'}>

View File

@@ -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}

View File

@@ -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' />

View File

@@ -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}>

View File

@@ -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>
),
},
]}
/>
)
}

View 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>
),
},
]}
/>
)
}

View File

@@ -2,7 +2,7 @@ export type SettingsTabList = {
title: string
iconName: string
iconColor: ThemeTokens
subTitle: string
subTitle?: string
children?: React.ReactNode
onPress?: () => void
}[]

View 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

View File

@@ -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()