diff --git a/App.tsx b/App.tsx index 35d23c98..3ec0c775 100644 --- a/App.tsx +++ b/App.tsx @@ -19,6 +19,7 @@ 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, useSettingsContext } from './src/providers/Settings' export const backgroundRuntime = createWorkletRuntime('background') @@ -27,7 +28,6 @@ export default function App(): React.JSX.Element { const performanceMetrics = usePerformanceMonitor('App', 3) const [playerIsReady, setPlayerIsReady] = useState(false) - const isDarkMode = useColorScheme() === 'dark' TrackPlayer.setupPlayer({ autoHandleInterruptions: true, @@ -65,31 +65,51 @@ export default function App(): React.JSX.Element { - - - - - - {playerIsReady && } - - - - - + + + ) } + +function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Element { + const { theme } = useSettingsContext() + + const isDarkMode = useColorScheme() === 'dark' + + return ( + + + + + {playerIsReady && } + + + + + ) +} diff --git a/maestro/flows/flow-1.yaml b/maestro/flows/flow-1.yaml index eeea47f1..761c7fb3 100644 --- a/maestro/flows/flow-1.yaml +++ b/maestro/flows/flow-1.yaml @@ -1,3 +1,4 @@ appId: com.jellify --- +- runFlow: ../tests/library.yaml - runFlow: ../tests/settings.yaml diff --git a/maestro/tests/musiclibrary.yaml b/maestro/tests/musiclibrary.yaml new file mode 100644 index 00000000..0c4c4711 --- /dev/null +++ b/maestro/tests/musiclibrary.yaml @@ -0,0 +1,17 @@ +appId: com.jellify +--- +# Wait for app to be ready, then navigate to Settings tab +- assertVisible: + id: "library-tab-button" + +# Navigate to Library tab using text +- tapOn: + id: "library-tab-button" + +# Verify we're on the library page +- assertVisible: + id: "library-artists-tab-button" + +# Navigate to Artists tab +- tapOn: + id: "library-artists-tab-button" diff --git a/maestro/tests/settings.yaml b/maestro/tests/settings.yaml index 0a070791..1efc7e9a 100644 --- a/maestro/tests/settings.yaml +++ b/maestro/tests/settings.yaml @@ -3,7 +3,6 @@ appId: com.jellify # Wait for app to be ready, then navigate to Settings tab - assertVisible: id: "settings-tab-button" -# timeout: 5000 # Navigate to Settings tab using text - tapOn: @@ -12,7 +11,6 @@ appId: com.jellify # Verify we're on the settings page - assertVisible: text: "App" -# timeout: 3000 # Test App (Preferences) Tab - should already be selected - assertVisible: @@ -23,6 +21,14 @@ appId: com.jellify text: "Reduce Haptics" - assertVisible: text: "Reduce haptic feedback" +- assertVisible: + text: "Theme" +- assertVisible: + text: "System" +- assertVisible: + text: "Light" +- assertVisible: + text: "Dark" # Test Player (Playback) Tab - tapOn: diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index 507c4c63..47423486 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -1,13 +1,12 @@ import React, { useEffect, useRef } from 'react' -import { getToken, getTokenValue, Separator, XStack, YStack } from 'tamagui' +import { getToken, getTokenValue, Separator, useTheme, XStack, YStack } from 'tamagui' import { Text } from '../Global/helpers/text' import { RefreshControl } from 'react-native' import { ArtistsProps } from '../types' import ItemRow from '../Global/components/item-row' -import { useLibrarySortAndFilterContext } from '../../providers/Library' +import { useLibraryContext, useLibrarySortAndFilterContext } from '../../providers/Library' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto' import { FlashList } from '@shopify/flash-list' -import { useLibraryContext } from '../../providers/Library' import { AZScroller } from '../Global/components/alphabetical-selector' import { useMutation } from '@tanstack/react-query' @@ -17,7 +16,7 @@ export default function Artists({ showAlphabeticalSelector, }: ArtistsProps): React.JSX.Element { const { artistPageParams } = useLibraryContext() - + const theme = useTheme() const { isFavorites } = useLibrarySortAndFilterContext() const sectionListRef = useRef>(null) @@ -32,23 +31,25 @@ export default function Artists({ console.debug(`Alphabetical Selector Callback: ${letter}`) do { + if (artistPageParams.current.includes(letter)) break await artistsInfiniteQuery.fetchNextPage({ cancelRefetch: true }) } while ( - artistsRef.current.indexOf(letter) === -1 && + !artistsRef.current.includes(letter) && artistsInfiniteQuery.hasNextPage && - !artistsInfiniteQuery.isFetchNextPageError && - !artistsInfiniteQuery.isFetchingNextPage + (!artistsInfiniteQuery.isFetchNextPageError || artistsInfiniteQuery.isFetchingNextPage) ) } const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } = useMutation({ mutationFn: (letter: string) => alphabeticalSelectorCallback(letter), onSuccess: (data, letter) => { - sectionListRef.current?.scrollToIndex({ - index: artistsRef.current!.indexOf(letter), - viewPosition: 0.1, - animated: true, - }) + setTimeout(() => { + sectionListRef.current?.scrollToIndex({ + index: artistsRef.current!.indexOf(letter), + viewPosition: 0.1, + animated: true, + }) + }, 500) }, }) @@ -82,7 +83,7 @@ export default function Artists({ data={artistsInfiniteQuery.data} refreshControl={ diff --git a/src/components/Library/component.tsx b/src/components/Library/component.tsx index 83dad3ac..ba6d9dfe 100644 --- a/src/components/Library/component.tsx +++ b/src/components/Library/component.tsx @@ -44,6 +44,7 @@ export default function Library({ small /> ), + tabBarButtonTestID: 'library-artists-tab-button', }} /> @@ -58,6 +59,7 @@ export default function Library({ small /> ), + tabBarButtonTestID: 'library-albums-tab-button', }} initialParams={{ navigation }} /> @@ -73,6 +75,7 @@ export default function Library({ small /> ), + tabBarButtonTestID: 'library-tracks-tab-button', }} /> @@ -87,6 +90,7 @@ export default function Library({ small /> ), + tabBarButtonTestID: 'library-playlists-tab-button', }} initialParams={{ navigation }} /> diff --git a/src/components/Settings/components/playback-tab.tsx b/src/components/Settings/components/playback-tab.tsx index 8cad6238..89216e7d 100644 --- a/src/components/Settings/components/playback-tab.tsx +++ b/src/components/Settings/components/playback-tab.tsx @@ -1,14 +1,11 @@ -import { SafeAreaView } from 'react-native-safe-area-context' import SettingsListGroup from './settings-list-group' import { RadioGroup, YStack } from 'tamagui' import { RadioGroupItemWithLabel } from '../../Global/helpers/radio-group-item-with-label' import { Text } from '../../Global/helpers/text' -import { useJellifyContext } from '../../../providers/index' import { getQualityLabel, getBandwidthEstimate } from '../utils/quality' import { StreamingQuality, useSettingsContext } from '../../../providers/Settings' export default function PlaybackTab(): React.JSX.Element { - const { server } = useJellifyContext() const { streamingQuality, setStreamingQuality } = useSettingsContext() return ( @@ -21,10 +18,7 @@ export default function PlaybackTab(): React.JSX.Element { iconColor: getStreamingQualityIconColor(streamingQuality), children: ( - - Streaming Quality: - - + Higher quality uses more bandwidth. Changes apply to new tracks. + + setTheme(value as Theme)} + > + + + + + ), }, { @@ -35,6 +43,20 @@ export default function PreferencesTab(): React.JSX.Element { /> ), }, + { + title: 'Send Metrics and Crash Reports', + iconName: sendMetrics ? 'bug-check' : 'bug', + iconColor: sendMetrics ? '$success' : '$borderColor', + subTitle: 'Send anonymous usage and crash data', + children: ( + + ), + }, ]} /> ) diff --git a/src/components/Settings/components/storage-tab.tsx b/src/components/Settings/components/storage-tab.tsx index 1fd43dfa..7e6f290f 100644 --- a/src/components/Settings/components/storage-tab.tsx +++ b/src/components/Settings/components/storage-tab.tsx @@ -1,21 +1,15 @@ 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 { useSettingsContext, DownloadQuality, StreamingQuality } from '../../../providers/Settings' +import { useSettingsContext, DownloadQuality } from '../../../providers/Settings' import { useNetworkContext } from '../../../providers/Network' import { RadioGroup, YStack } from 'tamagui' import { Text } from '../../Global/helpers/text' import { getQualityLabel } from '../utils/quality' export default function StorageTab(): React.JSX.Element { - const { - autoDownload, - setAutoDownload, - downloadQuality, - setDownloadQuality, - streamingQuality, - setStreamingQuality, - } = useSettingsContext() - const { downloadedTracks, storageUsage } = useNetworkContext() + const { autoDownload, setAutoDownload, downloadQuality, setDownloadQuality } = + useSettingsContext() + const { downloadedTracks } = useNetworkContext() return ( - - Download Quality: - - + Quality used when saving tracks for offline use. + @@ -35,8 +38,7 @@ export default function Jellify(): React.JSX.Element { - - + ) } @@ -67,6 +69,7 @@ function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): Rea function App(): React.JSX.Element { const { sendMetrics } = useSettingsContext() const telemetrydeck = useTelemetryDeck() + const theme = useTheme() useEffect(() => { if (sendMetrics) { @@ -83,6 +86,7 @@ function App(): React.JSX.Element { + ) } diff --git a/src/enums/mmkv-storage-keys.ts b/src/enums/mmkv-storage-keys.ts index b4168d8e..6e7ab3f9 100644 --- a/src/enums/mmkv-storage-keys.ts +++ b/src/enums/mmkv-storage-keys.ts @@ -23,4 +23,5 @@ export enum MMKVStorageKeys { Shuffled = 'Shuffled', RepeatMode = 'RepeatMode', ReducedHaptics = 'ReducedHaptics', + Theme = 'Theme', } diff --git a/src/providers/Settings/index.tsx b/src/providers/Settings/index.tsx index 2e475c31..74e382b4 100644 --- a/src/providers/Settings/index.tsx +++ b/src/providers/Settings/index.tsx @@ -5,6 +5,7 @@ import { createContext, useContext, useEffect, useState, useMemo } from 'react' export type DownloadQuality = 'original' | 'high' | 'medium' | 'low' export type StreamingQuality = 'original' | 'high' | 'medium' | 'low' +export type Theme = 'system' | 'light' | 'dark' interface SettingsContext { sendMetrics: boolean @@ -19,6 +20,8 @@ interface SettingsContext { setStreamingQuality: React.Dispatch> reducedHaptics: boolean setReducedHaptics: React.Dispatch> + theme: Theme + setTheme: React.Dispatch> } /** @@ -39,6 +42,7 @@ const SettingsContextInitializer = () => { 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, @@ -67,6 +71,8 @@ const SettingsContextInitializer = () => { reducedHapticsInit ?? (Platform.OS !== 'ios' && Math.random() > 0.7), ) + const [theme, setTheme] = useState(themeInit ?? 'system') + useEffect(() => { storage.set(MMKVStorageKeys.SendMetrics, sendMetrics) }, [sendMetrics]) @@ -91,6 +97,10 @@ const SettingsContextInitializer = () => { storage.set(MMKVStorageKeys.ReducedHaptics, reducedHaptics) }, [reducedHaptics]) + useEffect(() => { + storage.set(MMKVStorageKeys.Theme, theme) + }, [theme]) + return { sendMetrics, setSendMetrics, @@ -104,6 +114,8 @@ const SettingsContextInitializer = () => { setStreamingQuality, reducedHaptics, setReducedHaptics, + theme, + setTheme, } } @@ -120,6 +132,8 @@ export const SettingsContext = createContext({ setStreamingQuality: () => {}, reducedHaptics: false, setReducedHaptics: () => {}, + theme: 'system', + setTheme: () => {}, }) export const SettingsProvider = ({ children }: { children: React.ReactNode }) => { @@ -135,6 +149,7 @@ export const SettingsProvider = ({ children }: { children: React.ReactNode }) => context.downloadQuality, context.streamingQuality, context.reducedHaptics, + context.theme, ], ) diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 80bdb257..3e6af5cf 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -1,8 +1,8 @@ import _, { isUndefined } from 'lodash' -import ServerAuthentication from '../../components/Login/screens/server-authentication' -import ServerAddress from '../../components/Login/screens/server-address' +import ServerAuthentication from './server-authentication' +import ServerAddress from './server-address' import { createNativeStackNavigator } from '@react-navigation/native-stack' -import ServerLibrary from '../../components/Login/screens/server-library' +import ServerLibrary from './server-library' import { useJellifyContext } from '../../providers' const LoginStack = createNativeStackNavigator() diff --git a/src/components/Login/screens/server-address.tsx b/src/screens/Login/server-address.tsx similarity index 88% rename from src/components/Login/screens/server-address.tsx rename to src/screens/Login/server-address.tsx index 630c149d..594157e0 100644 --- a/src/components/Login/screens/server-address.tsx +++ b/src/screens/Login/server-address.tsx @@ -1,23 +1,23 @@ import React, { useEffect, useState } from 'react' import _, { isUndefined } from 'lodash' import { useMutation } from '@tanstack/react-query' -import { JellifyServer } from '../../../types/JellifyServer' +import { JellifyServer } from '../../types/JellifyServer' import { Input, ListItem, Separator, Spinner, XStack, YGroup, YStack } from 'tamagui' -import { SwitchWithLabel } from '../../Global/helpers/switch-with-label' -import { H2, Text } from '../../Global/helpers/text' -import Button from '../../Global/helpers/button' -import { http, https } from '../utils/constants' +import { SwitchWithLabel } from '../../components/Global/helpers/switch-with-label' +import { H2, Text } from '../../components/Global/helpers/text' +import Button from '../../components/Global/helpers/button' +import { http, https } from '../../components/Login/utils/constants' import { SafeAreaView } from 'react-native-safe-area-context' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { StackParamList } from '../../types' +import { StackParamList } from '../../components/types' import Toast from 'react-native-toast-message' -import { useJellifyContext } from '../../../providers' -import { useSettingsContext } from '../../../providers/Settings' -import Icon from '../../Global/components/icon' +import { useJellifyContext } from '../../providers' +import { useSettingsContext } 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 { connectToServer } from '../../api/mutations/login' +import { IS_MAESTRO_BUILD } from '../../configs/config' +import { sleepify } from '../../utils/sleep' export default function ServerAddress({ navigation, @@ -44,7 +44,7 @@ export default function ServerAddress({ }, [serverAddress]) useEffect(() => { - sleepify(250).then(() => signOut()) + sleepify(500).then(() => signOut()) }, []) const useServerMutation = useMutation({ diff --git a/src/components/Login/screens/server-authentication.tsx b/src/screens/Login/server-authentication.tsx similarity index 88% rename from src/components/Login/screens/server-authentication.tsx rename to src/screens/Login/server-authentication.tsx index 926b0323..cc243055 100644 --- a/src/components/Login/screens/server-authentication.tsx +++ b/src/screens/Login/server-authentication.tsx @@ -1,19 +1,19 @@ import React, { useState } from 'react' import { useMutation } from '@tanstack/react-query' import _ from 'lodash' -import { JellyfinCredentials } from '../../../api/types/jellyfin-credentials' +import { JellyfinCredentials } from '../../api/types/jellyfin-credentials' import { getToken, H6, Spacer, Spinner, XStack, YStack } from 'tamagui' -import { H2, H5, Text } from '../../Global/helpers/text' -import Button from '../../Global/helpers/button' +import { H2, H5, Text } from '../../components/Global/helpers/text' +import Button from '../../components/Global/helpers/button' import { SafeAreaView } from 'react-native-safe-area-context' -import { JellifyUser } from '../../../types/JellifyUser' -import { StackParamList } from '../../types' -import Input from '../../../components/Global/helpers/input' -import Icon from '../../Global/components/icon' -import { useJellifyContext } from '../../../providers' +import { JellifyUser } from '../../types/JellifyUser' +import { StackParamList } from '../../components/types' +import Input from '../../components/Global/helpers/input' +import Icon from '../../components/Global/components/icon' +import { useJellifyContext } from '../../providers' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import Toast from 'react-native-toast-message' -import { IS_MAESTRO_BUILD } from '../../../configs/config' +import { IS_MAESTRO_BUILD } from '../../configs/config' export default function ServerAuthentication({ navigation, diff --git a/src/components/Login/screens/server-library.tsx b/src/screens/Login/server-library.tsx similarity index 86% rename from src/components/Login/screens/server-library.tsx rename to src/screens/Login/server-library.tsx index c8fdd16c..624c1b52 100644 --- a/src/components/Login/screens/server-library.tsx +++ b/src/screens/Login/server-library.tsx @@ -1,9 +1,9 @@ import React from 'react' import { NativeStackNavigationProp } from '@react-navigation/native-stack' -import { StackParamList } from '../../../components/types' -import { useJellifyContext } from '../../../providers' +import { StackParamList } from '../../components/types' +import { useJellifyContext } from '../../providers' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' -import LibrarySelector from '../../Global/components/library-selector' +import LibrarySelector from '../../components/Global/components/library-selector' export default function ServerLibrary({ navigation, diff --git a/src/screens/Settings/sign-out-modal.tsx b/src/screens/Settings/sign-out-modal.tsx index cefa42fd..ae028b31 100644 --- a/src/screens/Settings/sign-out-modal.tsx +++ b/src/screens/Settings/sign-out-modal.tsx @@ -38,13 +38,13 @@ export default function SignOutModal({ navigation }: SignOutModalProps): React.J color={'$danger'} borderColor={'$danger'} onPress={() => { - clearDownloads() navigation.goBack() navigation.navigate('Login', { screen: 'ServerAddress', }) - TrackPlayer.reset() + clearDownloads() + resetQueue() }} >