navigation stack refactor

This commit is contained in:
Violet Caulfield
2025-08-14 06:29:01 -05:00
parent ba0d4a5d37
commit aefcef4aed
117 changed files with 1184 additions and 1556 deletions

11
App.tsx
View File

@@ -3,7 +3,7 @@ import React, { useState } from 'react'
import 'react-native-url-polyfill/auto'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import Jellify from './src/components/jellify'
import { TamaguiProvider, Theme } from 'tamagui'
import { TamaguiProvider } from 'tamagui'
import { Platform, useColorScheme } from 'react-native'
import jellifyConfig from './tamagui.config'
import { clientPersister } from './src/constants/storage'
@@ -15,7 +15,6 @@ import TrackPlayer, {
IOSCategoryOptions,
} from 'react-native-track-player'
import { CAPABILITIES } from './src/player/constants'
import { createWorkletRuntime } from 'react-native-reanimated'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { NavigationContainer } from '@react-navigation/native'
import { JellifyDarkTheme, JellifyLightTheme } from './src/components/theme'
@@ -23,9 +22,8 @@ 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')
import { SettingsProvider, useThemeSettingContext } from './src/providers/Settings'
import { navigationRef } from './navigation'
export default function App(): React.JSX.Element {
// Add performance monitoring to track app-level re-renders
@@ -88,12 +86,13 @@ export default function App(): React.JSX.Element {
}
function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Element {
const { theme } = useSettingsContext()
const theme = useThemeSettingContext()
const isDarkMode = useColorScheme() === 'dark'
return (
<NavigationContainer
ref={navigationRef}
theme={
theme === 'system'
? isDarkMode

View File

@@ -217,6 +217,7 @@ GEM
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
arm64-darwin-24
x64-mingw-ucrt
x86_64-linux

View File

@@ -4,6 +4,9 @@ import App from './App'
import { name as appName } from './app.json'
import { PlaybackService } from './src/player/service'
import TrackPlayer from 'react-native-track-player'
import { enableFreeze } from 'react-native-screens'
enableFreeze(true)
AppRegistry.registerComponent(appName, () => App)
AppRegistry.registerComponent('RNCarPlayScene', () => App)

View File

@@ -61,30 +61,11 @@
<key>NSLocalNetworkUsageDescription</key>
<string>${PRODUCT_NAME} uses the local network to connect to one's Jellyfin server for streaming music</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<string/>
<key>RCTNewArchEnabled</key>
<true/>
<key>UIAppFonts</key>
<array>
<string>AntDesign.ttf</string>
<string>Entypo.ttf</string>
<string>EvilIcons.ttf</string>
<string>Feather.ttf</string>
<string>FontAwesome.ttf</string>
<string>FontAwesome5_Brands.ttf</string>
<string>FontAwesome5_Regular.ttf</string>
<string>FontAwesome5_Solid.ttf</string>
<string>FontAwesome6_Brands.ttf</string>
<string>FontAwesome6_Regular.ttf</string>
<string>FontAwesome6_Solid.ttf</string>
<string>Foundation.ttf</string>
<string>Ionicons.ttf</string>
<string>MaterialIcons.ttf</string>
<string>MaterialCommunityIcons.ttf</string>
<string>SimpleLineIcons.ttf</string>
<string>Octicons.ttf</string>
<string>Zocial.ttf</string>
<string>Fontisto.ttf</string>
<string>Figtree-Black.otf</string>
<string>Figtree-BlackItalic.otf</string>
<string>Figtree-Bold.otf</string>
@@ -99,6 +80,7 @@
<string>Figtree-Regular.otf</string>
<string>Figtree-SemiBold.otf</string>
<string>Figtree-SemiBoldItalic.otf</string>
<string>MaterialDesignIcons.ttf</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
@@ -154,4 +136,4 @@
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>
</plist>

View File

@@ -2000,6 +2000,7 @@ PODS:
- SocketRocket
- SwiftAudioEx (= 1.1.0)
- Yoga
- react-native-vector-icons-material-design-icons (12.3.0)
- React-NativeModulesApple (0.81.0):
- boost
- DoubleConversion
@@ -2718,7 +2719,7 @@ PODS:
- RNWorklets
- SocketRocket
- Yoga
- RNScreens (4.13.1):
- RNScreens (4.14.0):
- boost
- DoubleConversion
- fast_float
@@ -2745,10 +2746,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- RNScreens/common (= 4.13.1)
- RNScreens/common (= 4.14.0)
- SocketRocket
- Yoga
- RNScreens/common (4.13.1):
- RNScreens/common (4.14.0):
- boost
- DoubleConversion
- fast_float
@@ -2807,34 +2808,6 @@ PODS:
- Sentry/HybridSDK (= 8.53.1)
- SocketRocket
- Yoga
- RNVectorIcons (10.2.0):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- RNWorklets (0.4.1):
- boost
- DoubleConversion
@@ -2987,6 +2960,7 @@ DEPENDENCIES:
- react-native-pager-view (from `../node_modules/react-native-pager-view`)
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
- react-native-track-player (from `../node_modules/react-native-track-player`)
- "react-native-vector-icons-material-design-icons (from `../node_modules/@react-native-vector-icons/material-design-icons`)"
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
@@ -3028,7 +3002,6 @@ DEPENDENCIES:
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
- "RNSentry (from `../node_modules/@sentry/react-native`)"
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- RNWorklets (from `../node_modules/react-native-worklets`)
- SDWebImage
- SocketRocket (~> 0.7.1)
@@ -3150,6 +3123,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-safe-area-context"
react-native-track-player:
:path: "../node_modules/react-native-track-player"
react-native-vector-icons-material-design-icons:
:path: "../node_modules/@react-native-vector-icons/material-design-icons"
React-NativeModulesApple:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-oscompat:
@@ -3232,8 +3207,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-screens"
RNSentry:
:path: "../node_modules/@sentry/react-native"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
RNWorklets:
:path: "../node_modules/react-native-worklets"
Yoga:
@@ -3293,6 +3266,7 @@ SPEC CHECKSUMS:
react-native-pager-view: 0b0b445d3cb9f8e9972842edf6ddf892b46bdc55
react-native-safe-area-context: a72764e0eb5d6b79b7450e5d0ae919eb1a4567b4
react-native-track-player: 89d8e641c83a89bea5dee43c381be743282553e9
react-native-vector-icons-material-design-icons: c502df5b988ce85d6c7d2b7ee909818315760b82
React-NativeModulesApple: b3766e1f87b08064ebc459b9e1538da2447ca874
React-oscompat: 34f3d3c06cadcbc470bc4509c717fb9b919eaa8b
React-perflogger: a1edb025fd5d44f61bf09307e248f7608d7b2dcf
@@ -3332,9 +3306,8 @@ SPEC CHECKSUMS:
RNGestureHandler: 3a73f098d74712952870e948b3d9cf7b6cae9961
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
RNReanimated: ee96d03fe3713993a30cc205522792b4cb08e4f9
RNScreens: 40264381e0eca3f0c368c78f192f547ef40caa39
RNScreens: 67ef075c463869c82f26744eee5f927edeea3190
RNSentry: 95e1ed0ede28a4af58aaafedeac9fcfaba0e89ce
RNVectorIcons: c13cc1db346e960ecd0aafcdd5d0bb458133b9c1
RNWorklets: e8335dff9d27004709f58316985769040cd1e8f2
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d

View File

@@ -41,7 +41,7 @@ jest.mock('../../src/providers/Network', () => ({
// Mock the SettingsProvider to avoid dependency issues
jest.mock('../../src/providers/Settings', () => ({
useSettingsContext: () => ({
useAutoDownloadContext: () => ({
autoDownload: false,
}),
}))

11
navigation.ts Normal file
View File

@@ -0,0 +1,11 @@
import { createNavigationContainerRef } from '@react-navigation/native'
import { RootStackParamList } from './src/screens/types'
export const navigationRef = createNavigationContainerRef<RootStackParamList>()
export default function navigate(
name: keyof RootStackParamList,
params?: RootStackParamList[keyof RootStackParamList],
) {
if (navigationRef.isReady()) navigationRef.navigate(name, params)
}

View File

@@ -13,6 +13,7 @@
"start": "react-native start",
"test": "jest",
"tsc": "tsc",
"codegen": "env DEBUG=metro:* react-native codegen",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "echo 'Please run `yarn pod:install:new-arch` to enable the new architecture'",
@@ -39,6 +40,7 @@
"@react-native-community/netinfo": "^11.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-native-picker/picker": "^2.11.1",
"@react-native-vector-icons/material-design-icons": "^12.3.0",
"@react-navigation/bottom-tabs": "^7.4.6",
"@react-navigation/material-top-tabs": "^7.3.6",
"@react-navigation/native": "^7.1.17",
@@ -59,6 +61,7 @@
"lodash": "^4.17.21",
"openai": "^5.12.2",
"react": "19.1.0",
"react-freeze": "^1.0.4",
"react-native": "0.81.0",
"react-native-background-actions": "^4.0.1",
"react-native-blob-util": "^0.22.2",
@@ -79,15 +82,14 @@
"react-native-pager-view": "^7.0.0",
"react-native-reanimated": "4.0.2",
"react-native-safe-area-context": "^5.6.0",
"react-native-screens": "^4.13.1",
"react-native-screens": "^4.14.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.15.0",
"react-native-toast-message": "^2.3.3",
"react-native-track-player": "5.0.0-alpha0",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"react-native-worklets": "0.4.1",
"react-native-worklets": "^0.4.1",
"ruby": "^0.6.1",
"scheduler": "^0.26.0",
"tamagui": "^1.132.18",

View File

@@ -1,38 +0,0 @@
diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp
index 94822c5..1499e11 100644
--- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp
+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp
@@ -14,9 +14,9 @@ Point RNSScreenShadowNode::getContentOriginOffset(
return stateData.contentOffset;
}
-std::optional<std::reference_wrapper<const ShadowNode::Shared>>
+std::optional<std::reference_wrapper<const std::shared_ptr<const ShadowNode>>>
findHeaderConfigChild(const YogaLayoutableShadowNode &screenShadowNode) {
- for (const ShadowNode::Shared &child : screenShadowNode.getChildren()) {
+ for (const std::shared_ptr<const ShadowNode> &child : screenShadowNode.getChildren()) {
if (std::strcmp(child->getComponentName(), "RNSScreenStackHeaderConfig") ==
0) {
return {std::cref(child)};
@@ -81,7 +81,7 @@ std::optional<float> findHeaderHeight(
}
#endif // ANDROID
-void RNSScreenShadowNode::appendChild(const ShadowNode::Shared &child) {
+void RNSScreenShadowNode::appendChild(const std::shared_ptr<const ShadowNode> &child) {
YogaLayoutableShadowNode::appendChild(child);
#ifdef ANDROID
const auto &stateData = getStateData();
diff --git a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h
index 6b4b72e..3379f89 100644
--- a/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h
+++ b/node_modules/react-native-screens/common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.h
@@ -28,7 +28,7 @@ class JSI_EXPORT RNSScreenShadowNode final : public ConcreteViewShadowNode<
Point getContentOriginOffset(bool includeTransform) const override;
- void appendChild(const ShadowNode::Shared &child) override;
+ void appendChild(const std::shared_ptr<const ShadowNode> &child) override;
void layout(LayoutContext layoutContext) override;

View File

@@ -1,4 +1,4 @@
import { JellifyUser } from '@/src/types/JellifyUser'
import { JellifyUser } from '../../types/JellifyUser'
import { Api } from '@jellyfin/sdk'
import { BaseItemDto, MediaType } from '@jellyfin/sdk/lib/generated-client/models'
import { getLibraryApi, getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api'

View File

@@ -1,4 +1,4 @@
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { Api } from '@jellyfin/sdk/lib/api'
import {
BaseItemDto,

View File

@@ -59,7 +59,7 @@ export async function fetchItems(
page: string | number = 0,
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
sortOrder: SortOrder[] = [SortOrder.Ascending],
isFavorite: boolean | undefined,
isFavorite?: boolean | undefined,
parentId?: string | undefined,
ids?: string[] | undefined,
): Promise<{ title: string | number; data: BaseItemDto[] }> {

View File

@@ -1,4 +1,4 @@
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
import { JellifyLibrary } from '../../types/JellifyLibrary'
import { Api } from '@jellyfin/sdk'
import {
BaseItemDto,

View File

@@ -1,5 +1,5 @@
import { AlbumProps, StackParamList } from '../types'
import { YStack, XStack, Separator, getToken, Spacer } from 'tamagui'
import { BaseStackParamList } from '../../screens/types'
import { YStack, XStack, Separator, getToken, Spacer, Spinner } from 'tamagui'
import { H5, Text } from '../Global/helpers/text'
import { ActivityIndicator, FlatList, SectionList } from 'react-native'
import { RunTimeTicks } from '../Global/helpers/time-codes'
@@ -16,10 +16,12 @@ 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 { useSettingsContext } from '../../providers/Settings'
import { useDownloadQualityContext, useStreamingQualityContext } from '../../providers/Settings'
import { useLoadQueueContext } from '../../providers/Player/queue'
import { QueuingType } from '../../enums/queuing-type'
import { useAlbumContext } from '../../providers/Album'
import { useNavigation } from '@react-navigation/native'
import { isUndefined } from 'lodash'
/**
* The screen for an Album's track list
@@ -29,18 +31,13 @@ import { useAlbumContext } from '../../providers/Album'
*
* @returns A React component
*/
export function Album({ navigation }: AlbumProps): React.JSX.Element {
export function Album(): React.JSX.Element {
const { album, discs, isPending } = useAlbumContext()
const { api, sessionId } = useJellifyContext()
const {
useDownloadMultiple,
pendingDownloads,
downloadingDownloads,
downloadedTracks,
failedDownloads,
} = useNetworkContext()
const { downloadQuality, streamingQuality } = useSettingsContext()
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
const downloadQuality = useDownloadQualityContext()
const streamingQuality = useStreamingQualityContext()
const useLoadNewQueue = useLoadQueueContext()
const downloadAlbum = (item: BaseItemDto[]) => {
@@ -54,7 +51,7 @@ export function Album({ navigation }: AlbumProps): React.JSX.Element {
const playAlbum = (shuffled: boolean = false) => {
if (!discs || discs.length === 0) return
const allTracks = discs.flatMap((disc) => disc.data)
const allTracks = discs?.flatMap((disc) => disc.data) ?? []
if (allTracks.length === 0) return
useLoadNewQueue({
@@ -71,14 +68,14 @@ export function Album({ navigation }: AlbumProps): React.JSX.Element {
return (
<SectionList
contentInsetAdjustmentBehavior='automatic'
sections={discs ? discs : [{ title: '1', data: [] }]}
sections={!isUndefined(discs) ? discs : []}
keyExtractor={(item, index) => item.Id! + index}
ItemSeparatorComponent={() => <Separator />}
renderSectionHeader={({ section }) => {
return (
<XStack
width='100%'
justifyContent={discs && discs.length >= 2 ? 'space-between' : 'flex-end'}
justifyContent={discs && discs?.length >= 2 ? 'space-between' : 'flex-end'}
alignItems='center'
backgroundColor={'$background'}
paddingHorizontal={'$4.5'}
@@ -103,21 +100,20 @@ export function Album({ navigation }: AlbumProps): React.JSX.Element {
</XStack>
)
}}
ListHeaderComponent={() => AlbumTrackListHeader(album, navigation, playAlbum)}
ListHeaderComponent={() => AlbumTrackListHeader(album, playAlbum)}
renderItem={({ item: track, index }) => (
<Track
track={track}
tracklist={discs?.flatMap((disc) => disc.data)}
index={discs?.flatMap((disc) => disc.data).indexOf(track) ?? index}
navigation={navigation}
queue={album}
/>
)}
ListFooterComponent={() => AlbumTrackListFooter(album, navigation)}
ListFooterComponent={() => AlbumTrackListFooter(album)}
ListEmptyComponent={() => (
<YStack>
{isPending ? (
<ActivityIndicator size='large' color={'$background'} />
<Spinner size='large' color={'$background'} />
) : (
<Text>No tracks found</Text>
)}
@@ -136,11 +132,12 @@ export function Album({ navigation }: AlbumProps): React.JSX.Element {
*/
function AlbumTrackListHeader(
album: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>,
playAlbum: (shuffled?: boolean) => void,
): React.JSX.Element {
const { width } = useSafeAreaFrame()
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
return (
<YStack marginTop={'$4'} alignItems='center'>
<XStack justifyContent='center'>
@@ -183,7 +180,7 @@ function AlbumTrackListHeader(
>
<FavoriteButton item={album} />
<InstantMixButton item={album} navigation={navigation} />
<InstantMixButton item={album} />
<Icon name='play' onPress={() => playAlbum(false)} small />
@@ -219,10 +216,9 @@ function AlbumTrackListHeader(
)
}
function AlbumTrackListFooter(
album: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>,
): React.JSX.Element {
function AlbumTrackListFooter(album: BaseItemDto): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
return (
<YStack marginLeft={'$2'}>
{album.ArtistItems && album.ArtistItems.length > 1 && (

View File

@@ -1,27 +1,33 @@
import { ActivityIndicator, RefreshControl } from 'react-native'
import { AlbumsProps } from '../types'
import { useDisplayContext } from '../../providers/Display/display-provider'
import { getToken, Separator, XStack, YStack } from 'tamagui'
import ItemRow from '../Global/components/item-row'
import React from 'react'
import { Text } from '../Global/helpers/text'
import { FlashList } from '@shopify/flash-list'
import { FetchNextPageOptions } from '@tanstack/react-query'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
interface AlbumsProps {
albums: (string | number | BaseItemDto)[] | undefined
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
showAlphabeticalSelector: boolean
}
export default function Albums({
albums,
navigation,
fetchNextPage,
hasNextPage,
isPending,
isFetchingNextPage,
showAlphabeticalSelector,
}: AlbumsProps): React.JSX.Element {
const { numberOfColumns } = useDisplayContext()
useDisplayContext()
const MemoizedItem = React.memo(ItemRow)
const itemHeight = getToken('$6')
return (
<XStack flex={1}>
<FlashList
@@ -30,7 +36,7 @@ export default function Albums({
}}
contentInsetAdjustmentBehavior='automatic'
data={albums ?? []}
renderItem={({ index, item: album }) =>
renderItem={({ item: album }) =>
typeof album === 'string' ? (
<XStack
padding={'$2'}
@@ -43,11 +49,7 @@ export default function Albums({
<Text>{album.toUpperCase()}</Text>
</XStack>
) : typeof album === 'number' ? null : typeof album === 'object' ? (
<MemoizedItem
item={album}
queueName={album.Name ?? 'Unknown Album'}
navigation={navigation}
/>
<MemoizedItem item={album} queueName={album.Name ?? 'Unknown Album'} />
) : null
}
ListEmptyComponent={
@@ -68,9 +70,7 @@ export default function Albums({
stickyHeaderIndices={
showAlphabeticalSelector
? albums
?.map((album, index, albums) =>
typeof album === 'string' ? index : 0,
)
?.map((album, index) => (typeof album === 'string' ? index : 0))
.filter((value, index, indices) => indices.indexOf(value) === index)
: []
}

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'
import { ItemCard } from '../Global/components/item-card'
import { ArtistAlbumsProps, ArtistEpsProps, ArtistFeaturedOnProps } from '../types'
import { ArtistAlbumsProps, ArtistEpsProps, ArtistFeaturedOnProps } from './types'
import { Text } from '../Global/helpers/text'
import { useArtistContext } from '../../providers/Artist'
import { convertRunTimeTicksToSeconds } from '../../utils/runtimeticks'

View File

@@ -2,23 +2,18 @@ import React from 'react'
import Albums from './albums'
import SimilarArtists from './similar'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import { StackParamList } from '../types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import ArtistTabBar from './tab-bar'
import { useArtistContext } from '../../providers/Artist'
import ArtistTabList from './types'
const ArtistTabs = createMaterialTopTabNavigator<StackParamList>()
const ArtistTabs = createMaterialTopTabNavigator<ArtistTabList>()
export default function ArtistNavigation({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
export default function ArtistNavigation(): React.JSX.Element {
const { featuredOn, artist } = useArtistContext()
return (
<ArtistTabs.Navigator
tabBar={(props) => ArtistTabBar(props, navigation)}
tabBar={(props) => ArtistTabBar(props)}
screenOptions={{
tabBarLabelStyle: {
fontFamily: 'Figtree-Bold',

View File

@@ -1,19 +1,14 @@
import { ItemCard } from '../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import { RouteProp } from '@react-navigation/native'
import { BaseStackParamList } from '../../screens/types'
import { useNavigation } from '@react-navigation/native'
import { Text } from '../Global/helpers/text'
import { useArtistContext } from '../../providers/Artist'
import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated'
import { ActivityIndicator } from 'react-native'
export default function SimilarArtists({
route,
navigation,
}: {
route: RouteProp<StackParamList, 'SimilarArtists'>
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
export default function SimilarArtists(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const { similarArtists, fetchingSimilarArtists, scroll } = useArtistContext()
const scrollHandler = useAnimatedScrollHandler({
onScroll: (event) => {

View File

@@ -10,18 +10,13 @@ import { ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { useArtistContext } from '../../providers/Artist'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { useJellifyContext } from '../../providers'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import React from 'react'
import Icon from '../Global/components/icon'
import { useLoadQueueContext } from '../../providers/Player/queue'
import { QueuingType } from '../../enums/queuing-type'
import { fetchAlbumDiscs } from '../../api/queries/item'
export default function ArtistTabBar(
props: MaterialTopTabBarProps,
stackNavigator: NativeStackNavigationProp<StackParamList>,
) {
export default function ArtistTabBar(props: MaterialTopTabBarProps) {
const { api } = useJellifyContext()
const { artist, scroll, albums } = useArtistContext()
const useLoadNewQueue = useLoadQueueContext()
@@ -102,7 +97,7 @@ export default function ArtistTabBar(
<XStack alignItems='center' justifyContent='center' flex={1} gap={'$6'}>
<FavoriteButton item={artist} />
<InstantMixButton item={artist} navigation={stackNavigator} />
<InstantMixButton item={artist} />
<Icon name='play' onPress={() => playArtist(false)} />

14
src/components/Artist/types.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import { MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
type ArtistTabList = {
ArtistAlbums: undefined
ArtistEps: undefined
ArtistFeaturedOn: undefined
SimilarArtists: undefined
}
export default ArtistTabList
export type ArtistAlbumsProps = MaterialTopTabBarProps<ArtistTabList>
export type ArtistEpsProps = MaterialTopTabBarProps<ArtistTabList>
export type ArtistFeaturedOnProps = MaterialTopTabBarProps<ArtistTabList>

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef } from 'react'
import { getToken, Separator, useTheme, XStack, YStack, Spinner } from 'tamagui'
import { getToken, Separator, useTheme, XStack } from 'tamagui'
import { Text } from '../Global/helpers/text'
import { ActivityIndicator, RefreshControl } from 'react-native'
import { ArtistsProps } from '../types'
import { RefreshControl } from 'react-native'
import { ArtistsProps } from '../../screens/types'
import ItemRow from '../Global/components/item-row'
import { useLibrarySortAndFilterContext } from '../../providers/Library'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
@@ -20,7 +20,6 @@ import { isString } from 'lodash'
*/
export default function Artists({
artistsInfiniteQuery,
navigation,
showAlphabeticalSelector,
artistPageParams,
}: ArtistsProps): React.JSX.Element {
@@ -145,7 +144,6 @@ export default function Artists({
circular
item={artist}
queueName={artist.Name ?? 'Unknown Artist'}
navigation={navigation}
/>
) : null
}

View File

@@ -1,15 +1,13 @@
import React from 'react'
import Artists from './component'
import { ArtistsProps } from '../types'
import { ArtistsProps } from '../../screens/types'
export default function ArtistsScreen({
navigation,
artistsInfiniteQuery: artistInfiniteQuery,
showAlphabeticalSelector,
}: ArtistsProps): React.JSX.Element {
return (
<Artists
navigation={navigation}
artistsInfiniteQuery={artistInfiniteQuery}
showAlphabeticalSelector={showAlphabeticalSelector}
/>

View File

@@ -1,38 +1,42 @@
import { ScrollView, View } from 'tamagui'
import { MultipleArtistsProps } from '../../types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import ItemRow from '../../Global/components/item-row'
import { useEffect } from 'react'
import { FlashList } from '@shopify/flash-list'
import { PlayerParamList } from '../../../screens/Player/types'
import { RouteProp } from '@react-navigation/native'
import navigate from '../../../../navigation'
interface MultipleArtistsProps {
navigation: NativeStackNavigationProp<PlayerParamList, 'MultipleArtists'>
route: RouteProp<PlayerParamList, 'MultipleArtists'>
}
export default function MultipleArtists({
navigation,
route,
}: MultipleArtistsProps): React.JSX.Element {
return (
<ScrollView>
{route.params.artists.map((artist) => {
return (
<ItemRow
circular
key={artist.Id}
item={artist}
queueName={''}
navigation={navigation}
onPress={() => {
navigation.goBack()
navigation.goBack()
navigation.navigate('Tabs', {
screen: 'Library',
<FlashList
data={route.params.artists}
renderItem={({ item: artist }) => (
<ItemRow
circular
key={artist.Id}
item={artist}
queueName={''}
onPress={() => {
navigation.goBack() // Dismiss multiple artists modal
navigation.goBack() // Dismiss player modal
navigate('Tabs', {
screen: 'Library',
param: {
screen: 'Artist',
params: {
screen: 'Artist',
params: {
artist: artist,
},
artist,
},
})
}}
/>
)
})}
</ScrollView>
},
})
}}
/>
)}
/>
)
}

View File

@@ -1,102 +1,235 @@
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
import { useNavigation } from '@react-navigation/native'
import { Separator, View, XStack, YGroup, YStack, ZStack } from 'tamagui'
import { StackParamList } from '../types'
import { getToken, ListItem, View, YGroup, ZStack } from 'tamagui'
import { BaseStackParamList, RootStackParamList } from '../../screens/types'
import { Text } from '../Global/helpers/text'
import ItemImage from '../Global/components/image'
import FavoriteContextMenuRow from '../Global/components/favorite-context-menu-row'
import { Blurhash } from 'react-native-blurhash'
import { getPrimaryBlurhashFromDto } from '../../utils/blurhash'
import { useColorScheme, useWindowDimensions } from 'react-native'
import { 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'
import { QueryKeys } from '../../enums/query-keys'
import { fetchItem } from '../../api/queries/item'
import { useJellifyContext } from '../../providers'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { useAddToQueueContext } from '../../providers/Player/queue'
import { AddToQueueMutation } from '../../providers/Player/interfaces'
import { QueuingType } from '../../enums/queuing-type'
import LibraryStackParamList from '../../screens/Library/types'
import navigate from '../../../navigation'
import DiscoverStackParamList from '@/src/screens/Discover/types'
import { screen } from '@testing-library/react-native'
import HomeStackParamList from '../../screens/Home/types'
interface ContextProps {
item: BaseItemDto
isNested?: boolean | undefined
navigation?: NativeStackNavigationProp<
HomeStackParamList | LibraryStackParamList | DiscoverStackParamList
>
}
export default function ItemContext({ item }: ContextProps): React.JSX.Element {
const navigation = useNavigation<StackParamList>()
export default function ItemContext({
item,
isNested,
navigation,
}: ContextProps): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const isArtist = item.Type === BaseItemKind.MusicArtist
const isAlbum = item.Type === BaseItemKind.MusicAlbum
const isTrack = item.Type === BaseItemKind.Audio
const isPlaylist = item.Type === BaseItemKind.Playlist
const albumArtists = item.AlbumArtists ?? []
const { data: album, isSuccess: albumFetchSuccess } = useQuery({
queryKey: [QueryKeys.Item, item.AlbumId],
queryFn: () => fetchItem(api, item.AlbumId!),
enabled: isTrack,
})
const { data: artist, isSuccess: artistFetchSuccess } = useQuery({
queryKey: [QueryKeys.ArtistById, albumArtists.length > 0 ? albumArtists[0].Id : item.Id],
queryFn: () => fetchItem(api, albumArtists[0].Id!),
enabled: (isTrack || isAlbum) && albumArtists.length > 0,
})
const { data: tracks, isSuccess: tracksFetchSuccess } = useQuery({
queryKey: [QueryKeys.ItemTracks, item.Id],
queryFn: () =>
getItemsApi(api!)
.getItems({ parentId: item.Id! })
.then(({ data }) => {
if (data.Items) return data.Items
else return []
}),
})
const renderAddToQueueRow = isTrack || isAlbum || isPlaylist
return (
<ZStack flex={1}>
<View flex={1}>{renderBackgroundBlur(item)}</View>
<ZStack>
<ItemContextBackground item={item} />
<View flex={1}>
{renderContextHeader(item)}
<YGroup unstyled flex={1} marginTop={'$8'}>
<FavoriteContextMenuRow item={item} />
<Separator />
{renderAddToQueueRow && tracks && (
<AddToQueueMenuRow tracks={isTrack ? [item] : tracks} />
)}
<YGroup>
<FavoriteContextMenuRow item={item} />
</YGroup>
</View>
{album && (
<ViewAlbumMenuRow item={album} isNested={isNested} navigation={navigation} />
)}
{artist && (
<ViewArtistMenuRow item={artist} isNested={isNested} navigation={navigation} />
)}
</YGroup>
</ZStack>
)
}
function renderBackgroundBlur(item: BaseItemDto): React.JSX.Element {
function ItemContextBackground({ item }: { item: BaseItemDto }): React.JSX.Element {
return (
<ZStack flex={1}>
<BackgroundBlur item={item} />
<BackgroundGradient />
</ZStack>
)
}
function BackgroundBlur({ item }: { item: BaseItemDto }): React.JSX.Element {
const blurhash = getPrimaryBlurhashFromDto(item)
return (
<Blurhash
blurhash={blurhash!}
style={{
height: '100%',
width: '100%',
flex: 1,
}}
/>
)
}
function renderContextHeader(item: BaseItemDto): React.JSX.Element {
const isArtist = item.Type === BaseItemKind.MusicArtist
const isAlbum = item.Type === BaseItemKind.MusicAlbum
const isTrack = item.Type === BaseItemKind.Audio
const isPlaylist = item.Type === BaseItemKind.Playlist
function AddToQueueMenuRow({ tracks }: { tracks: BaseItemDto[] }): React.JSX.Element {
const useAddToQueue = useAddToQueueContext()
const itemName = item.Name ?? getNamePlaceholder(item.Type)
const mutation: AddToQueueMutation = {
tracks,
queuingType: QueuingType.DirectlyQueued,
}
return (
<XStack alignItems='center' marginBottom={'$2'} margin={'$4'} gap={'$2'} minHeight={'$6'}>
<ItemImage item={item} circular={isArtist} />
<ListItem
animation={'quick'}
backgroundColor={'transparent'}
flex={1}
gap={'$2'}
justifyContent='flex-start'
onPress={() => {
useAddToQueue.mutate(mutation)
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon color='$primary' name='playlist-plus' />
<YStack gap={'$1'}>
<Text bold>{itemName}</Text>
{!isArtist && !isPlaylist ? (
isAlbum ? (
<Text>
{item.AlbumArtists?.map((artist) => artist.Name).join(', ') ||
'Unknown Artist'}
</Text>
) : (
<Text>
{item.ArtistItems?.map((artist) => artist.Name).join(', ') ||
'Unknown Artist'}
</Text>
)
) : (
<Text>{`${item.ChildCount?.toString() ?? '0'} ${item.ChildCount === 1 ? 'track' : 'tracks'}`}</Text>
)}
</YStack>
</XStack>
<Text bold>Add to Queue</Text>
</ListItem>
)
}
function getNamePlaceholder(type: BaseItemKind | undefined): string {
switch (type) {
case BaseItemKind.MusicArtist:
return 'Artist'
case BaseItemKind.MusicAlbum:
return 'Album'
case BaseItemKind.Audio:
return 'Track'
case BaseItemKind.Playlist:
return 'Playlist'
default:
return 'Item'
}
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 ViewAlbumMenuRow({ item: album, isNested, navigation }: ContextProps): React.JSX.Element {
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
return (
<ListItem
animation='quick'
backgroundColor={'transparent'}
gap={'$2'}
justifyContent='flex-start'
onPress={() => {
rootNavigation.goBack()
if (isNested) rootNavigation.goBack()
if (navigation) navigation?.navigate('Album', { album })
else
navigate('Tabs', {
screen: 'Library',
params: {
screen: 'Album',
params: {
album,
},
},
})
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon color='$primary' name='disc' />
<Text bold>Go to Album</Text>
</ListItem>
)
}
function ViewArtistMenuRow({
item: artist,
isNested,
navigation,
}: ContextProps): React.JSX.Element {
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
return (
<ListItem
animation={'quick'}
backgroundColor={'transparent'}
gap={'$2'}
justifyContent='flex-start'
onPress={() => {
rootNavigation.goBack()
if (isNested) rootNavigation.goBack()
if (navigation) navigation.navigate('Artist', { artist })
else
navigate('Tabs', {
screen: 'Library',
params: {
screen: 'Artist',
params: {
artist,
},
},
})
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon color='$primary' name='microphone-variant' />
<Text bold>Go to Artist</Text>
</ListItem>
)
}

View File

@@ -1,137 +0,0 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import TrackOptions from './helpers/TrackOptions'
import { getToken, ScrollView, Spacer, useTheme, View, XStack, YStack } from 'tamagui'
import { Text } from '../Global/helpers/text'
import FavoriteButton from '../Global/components/favorite-button'
import { useEffect } from 'react'
import { trigger } from 'react-native-haptic-feedback'
import TextTicker from 'react-native-text-ticker'
import { TextTickerConfig } from '../Player/component.config'
import FastImage from 'react-native-fast-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import JellifyToastConfig from '../../constants/toast.config'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '../../providers'
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models'
export default function ItemDetail({
item,
navigation,
isNested,
}: {
item: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
isNested?: boolean | undefined
}): React.JSX.Element {
let options: React.JSX.Element | undefined = undefined
const { api } = useJellifyContext()
useEffect(() => {
trigger('impactMedium')
}, [item])
const theme = useTheme()
switch (item.Type) {
case 'Audio': {
options = TrackOptions({ track: item, navigation, isNested })
break
}
case 'MusicAlbum': {
break
}
case 'MusicArtist': {
break
}
case 'Playlist': {
break
}
default: {
break
}
}
return (
<ScrollView contentInsetAdjustmentBehavior='automatic' removeClippedSubviews>
<YStack alignItems='center' flex={1} marginTop={'$4'}>
<XStack
justifyContent='center'
alignItems='center'
minHeight={getToken('$20') * 1.5}
>
<FastImage
source={{
uri:
getImageApi(api!).getItemImageUrlById(
item.Type === 'Audio' ? item.AlbumId! || item.Id! : item.Id!,
ImageType.Primary,
{
tag: item.ImageTags?.Primary,
},
) || '',
}}
style={{
width: getToken('$20') * 1.5,
height: getToken('$20') * 1.5,
borderRadius:
item.Type === 'MusicArtist'
? getToken('$20') * 1.5
: getToken('$5'),
alignSelf: 'center',
}}
/>
</XStack>
{/* Item Name, Artist, Album, and Favorite Button */}
<XStack maxWidth={getToken('$20') * 1.5}>
<YStack
marginLeft={'$0.5'}
alignItems='flex-start'
alignContent='flex-start'
justifyContent='flex-start'
flex={3}
>
<TextTicker {...TextTickerConfig}>
<Text bold fontSize={'$6'}>
{item.Name ?? 'Untitled Track'}
</Text>
</TextTicker>
<TextTicker {...TextTickerConfig}>
<Text
fontSize={'$6'}
onPress={() => {
if (item.ArtistItems) {
if (isNested) navigation.getParent()!.goBack()
navigation.goBack()
navigation.navigate('Artist', {
artist: item.ArtistItems[0],
})
}
}}
>
{item.Artists?.join(', ') ?? 'Unknown Artist'}
</Text>
</TextTicker>
</YStack>
<YStack flex={1} alignItems='flex-end' justifyContent='center'>
<FavoriteButton item={item} />
</YStack>
</XStack>
<Spacer />
{options ?? <View />}
</YStack>
<Toast config={JellifyToastConfig(theme)} />
</ScrollView>
)
}

View File

@@ -1,311 +0,0 @@
import { StackParamList } from '../../types'
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import {
Circle,
getToken,
getTokens,
ListItem,
Separator,
Spacer,
Spinner,
XStack,
YGroup,
YStack,
} from 'tamagui'
import { QueuingType } from '../../../enums/queuing-type'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import IconButton from '../../../components/Global/helpers/icon-button'
import { Text } from '../../../components/Global/helpers/text'
import React, { useMemo } from 'react'
import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'
import { AddToPlaylistMutation } from '../../../components/Detail/types'
import { addToPlaylist } from '../../../api/mutations/playlists'
import { trigger } from 'react-native-haptic-feedback'
import { queryClient } from '../../../constants/query-client'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchItem } from '../../../api/queries/item'
import { fetchUserPlaylists } from '../../../api/queries/playlists'
import { useJellifyContext } from '../../../providers'
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { useNetworkContext } from '../../../providers/Network'
import { useAddToQueueContext } from '../../../providers/Player/queue'
import Toast from 'react-native-toast-message'
import FastImage from 'react-native-fast-image'
import Icon from '../../../components/Global/components/icon'
import QueryConfig from '../../../api/queries/query.config'
interface TrackOptionsProps {
track: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
/**
* Whether this is nested in the player modal
*/
isNested: boolean | undefined
}
export default function TrackOptions({
track,
navigation,
isNested,
}: TrackOptionsProps): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const { data: album, isSuccess: albumFetchSuccess } = useQuery({
queryKey: [QueryKeys.Item, track.AlbumId!],
queryFn: () => fetchItem(api, track.AlbumId!),
})
const { useDownload, useRemoveDownload, downloadedTracks } = useNetworkContext()
const isDownloaded = downloadedTracks?.find((t) => t.item.Id === track.Id)?.item?.Id
const {
data: playlists,
isPending: playlistsFetchPending,
isSuccess: playlistsFetchSuccess,
} = useInfiniteQuery({
queryKey: [QueryKeys.Playlists],
queryFn: () => fetchUserPlaylists(api, user, library),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
return lastPage.length === QueryConfig.limits.library * 2
? lastPageParam + 1
: undefined
},
})
// Fetch all playlist tracks to check if the current track is already in any playlists
const playlistsWithTracks = useQuery({
queryKey: [QueryKeys.PlaylistItemCheckCache, playlists?.map((p) => p.Id).join(',')],
enabled: !!playlists && playlists.length > 0,
queryFn: () => {
console.debug('Fetching playlist contents')
return Promise.all(
playlists!.map(async (playlist) => {
const response = await getItemsApi(api!).getItems({
parentId: playlist.Id!,
})
return {
playlistId: playlist.Id!,
tracks: response.data.Items || [],
}
}),
)
},
})
// Check if a track is in a playlist
const isTrackInPlaylist = useMemo(() => {
if (!playlistsWithTracks.data) return {}
const result: Record<string, boolean> = {}
playlistsWithTracks.data.forEach((playlistData) => {
result[playlistData.playlistId] = playlistData.tracks.some(
(playlistTrack) => playlistTrack.Id === track.Id,
)
})
return result
}, [playlistsWithTracks.data, track.Id])
const useAddToQueue = useAddToQueueContext()
const { width } = useSafeAreaFrame()
const useAddToPlaylist = useMutation({
mutationFn: ({ track, playlist }: AddToPlaylistMutation) => {
trigger('impactLight')
return addToPlaylist(api, user, track, playlist)
},
onSuccess: (data, { playlist }) => {
Toast.show({
text1: 'Added to playlist',
type: 'success',
})
trigger('notificationSuccess')
queryClient.invalidateQueries({
queryKey: [QueryKeys.Playlists],
})
queryClient.invalidateQueries({
queryKey: [QueryKeys.ItemTracks, playlist.Id!],
})
// Invalidate our playlist check cache
queryClient.invalidateQueries({
queryKey: [QueryKeys.PlaylistItemCheckCache],
})
},
onError: () => {
Toast.show({
text1: 'Unable to add',
type: 'error',
})
trigger('notificationError')
},
})
return (
<YStack>
<XStack marginHorizontal={'$1'} justifyContent='space-between'>
{albumFetchSuccess && album ? (
<IconButton
name='music-box'
title='Go to Album'
onPress={() => {
if (isNested) navigation.goBack()
navigation.goBack()
if (isNested)
navigation.navigate('Tabs', {
screen: 'Home',
params: {
screen: 'Album',
params: {
album,
},
},
})
else
navigation.navigate('Album', {
album,
})
}}
size={getToken('$12') * 1.5}
/>
) : (
<Spacer />
)}
<IconButton
circular
name='table-column-plus-before'
title='Play Next'
onPress={() => {
useAddToQueue.mutate({
track: track,
queuingType: QueuingType.PlayingNext,
})
}}
size={getToken('$12') * 1.5}
/>
<IconButton
circular
name='table-column-plus-after'
title='Add to Queue'
onPress={() => {
useAddToQueue.mutate({
track: track,
})
}}
size={getToken('$12') * 1.5}
/>
{useDownload.isPending ? (
<Circle size={width / 6} disabled>
<Spinner marginHorizontal={10} size='small' color={'$primary'} />
</Circle>
) : (
<IconButton
disabled={!!isDownloaded}
name={isDownloaded ? 'delete' : 'download'}
title={isDownloaded ? 'Clear Download' : 'Download'}
onPress={() => {
if (isDownloaded) useRemoveDownload.mutate(track)
else useDownload.mutate(track)
}}
size={getToken('$12') * 1.5}
/>
)}
</XStack>
<Spacer />
{(playlistsFetchPending || playlistsWithTracks.isFetching) && <Spinner />}
{!playlistsFetchPending && playlistsFetchSuccess && (
<>
<Text bold fontSize={'$6'}>
Add to Playlist
</Text>
<YGroup separator={<Separator />}>
{playlists?.map((playlist) => {
const isInPlaylist = isTrackInPlaylist[playlist.Id!]
return (
<YGroup.Item key={playlist.Id!}>
<ListItem
hoverTheme
disabled={isInPlaylist}
opacity={isInPlaylist ? 0.7 : 1}
onPress={() => {
if (!isInPlaylist) {
useAddToPlaylist.mutate({
track,
playlist,
})
}
}}
>
<XStack alignItems='center'>
<YStack flex={1}>
<FastImage
source={{
uri:
getImageApi(api!).getItemImageUrlById(
playlist.Id!,
ImageType.Primary,
{
tag: playlist.ImageTags
?.Primary,
},
) || '',
}}
style={{
borderRadius: getToken('$1.5'),
width: getToken('$12'),
height: getToken('$12'),
marginRight: getToken('$2'),
}}
/>
</YStack>
<YStack alignItems='flex-start' flex={5}>
<Text bold fontSize={'$6'}>
{playlist.Name ?? 'Untitled Playlist'}
</Text>
<Text color={getTokens().color.amethyst.val}>{`${
playlist.ChildCount ?? 0
} tracks`}</Text>
</YStack>
{isInPlaylist ? (
<Icon
flex={1}
name='check-circle-outline'
color={'$success'}
/>
) : (
<Spacer flex={1} />
)}
</XStack>
</ListItem>
</YGroup.Item>
)
})}
</YGroup>
</>
)}
</YStack>
)
}

View File

@@ -1,6 +0,0 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
export interface AddToPlaylistMutation {
track: BaseItemDto
playlist: BaseItemDto
}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { getToken, ScrollView, Separator, View } from 'tamagui'
import RecentlyAdded from './helpers/just-added'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import { RootStackParamList } from '../../screens/types'
import { useDiscoverContext } from '../../providers/Discover'
import { RefreshControl } from 'react-native'
import PublicPlaylists from './helpers/public-playlists'
@@ -11,7 +11,7 @@ import SuggestedArtists from './helpers/suggested-artists'
export default function Index({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const { refreshing, refresh, recentlyAdded, publicPlaylists, suggestedArtistsInfiniteQuery } =
useDiscoverContext()

View File

@@ -1,4 +1,4 @@
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
@@ -10,7 +10,7 @@ import Icon from '../../Global/components/icon'
export default function RecentlyAdded({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const {
recentlyAdded,

View File

@@ -1,6 +1,6 @@
import { View, XStack } from 'tamagui'
import { useDiscoverContext } from '../../../providers/Discover'
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Icon from '../../Global/components/icon'
import { useJellifyContext } from '../../../providers'
@@ -12,7 +12,7 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context'
export default function PublicPlaylists({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}) {
const {
publicPlaylists,

View File

@@ -5,12 +5,12 @@ import { ItemCard } from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useDiscoverContext } from '../../../providers/Discover'
import { H4 } from '../../Global/helpers/text'
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
export default function SuggestedArtists({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const { suggestedArtistsInfiniteQuery } = useDiscoverContext()
return (

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 { useSettingsContext } from '../../../providers/Settings'
import { useReducedHapticsContext } from '../../../providers/Settings'
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 } = useSettingsContext()
const reducedHaptics = useReducedHapticsContext()
const overlayOpacity = useSharedValue(0)

View File

@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserData } from '../../../api/queries/favorites'
import { useJellifyContext } from '../../../providers'
import { ListItem, XStack, YGroup } from 'tamagui'
import { getToken, ListItem } from 'tamagui'
import Icon from './icon'
import { useJellifyUserDataContext } from '../../../providers/UserData'
import { useEffect, useState } from 'react'
@@ -28,11 +28,20 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }):
setIsFavorite(userData?.IsFavorite ?? false)
}, [userData])
return (
<YGroup.Item>
return isFavorite ? (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${item.Id}-remove-favorite-row`}
style={{
flex: 1,
}}
>
<ListItem
pressStyle={{ opacity: 0.5 }}
animation={'quick'}
backgroundColor={'transparent'}
gap={'$2'}
justifyContent='flex-start'
onPress={() => {
toggleFavorite(isFavorite, {
item,
@@ -40,37 +49,35 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }):
onToggle: () => refetch(),
})
}}
pressStyle={{ opacity: 0.5 }}
>
{isFavorite ? (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${item.Id}-remove-favorite-row`}
>
<XStack alignContent='center' justifyContent='flex-start' gap={'$2'}>
<Icon name={'heart'} small color={'$primary'} />
<Icon name={'heart'} small color={'$primary'} />
<Text marginVertical={'$1.5'} bold>
Remove from favorites
</Text>
</XStack>
</Animated.View>
) : (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${item.Id}-favorite-row`}
>
<XStack alignContent='center' justifyContent='flex-start' gap={'$2'}>
<Icon name={'heart-outline'} small color={'$primary'} />
<Text marginVertical={'$1.5'} bold>
Add to favorites
</Text>
</XStack>
</Animated.View>
)}
<Text bold>Remove from favorites</Text>
</ListItem>
</YGroup.Item>
</Animated.View>
) : (
<Animated.View entering={FadeIn} exiting={FadeOut} key={`${item.Id}-favorite-row`}>
<ListItem
animation={'quick'}
backgroundColor={'transparent'}
justifyContent='flex-start'
gap={'$2'}
onPress={() => {
toggleFavorite(isFavorite, {
item,
setFavorite: setIsFavorite,
onToggle: () => refetch(),
})
}}
pressStyle={{ opacity: 0.5 }}
>
<Icon name={'heart-outline'} small color={'$primary'} />
<Text marginVertical={'$1.5'} bold>
Add to favorites
</Text>
</ListItem>
</Animated.View>
)
}

View File

@@ -1,5 +1,4 @@
import React from 'react'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
import {
ColorTokens,
getToken,
@@ -10,6 +9,7 @@ import {
useTheme,
YStack,
} from 'tamagui'
import MaterialDesignIcon from '@react-native-vector-icons/material-design-icons'
const smallSize = 30
@@ -56,7 +56,7 @@ export default function Icon({
height={size + getToken('$1')}
flex={flex}
>
<MaterialCommunityIcons
<MaterialDesignIcon
color={
color && !disabled
? theme[color]?.val
@@ -64,7 +64,8 @@ export default function Icon({
? theme.neutral.val
: theme.color.val
}
name={name}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name={name as any}
size={size}
testID={testID ?? undefined}
/>

View File

@@ -1,21 +1,19 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import React from 'react'
import { StackParamList } from '../../types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { QueryKeys } from '../../../enums/query-keys'
import { useQuery } from '@tanstack/react-query'
import { fetchInstantMixFromItem } from '../../../api/queries/instant-mixes'
import Icon from './icon'
import { Spacer, Spinner } from 'tamagui'
import { useJellifyContext } from '../../../providers'
export default function InstantMixButton({
item,
navigation,
}: {
item: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../../../screens/types'
export default function InstantMixButton({ item }: { item: BaseItemDto }): React.JSX.Element {
const { api, user } = useJellifyContext()
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const { data, isFetching, refetch } = useQuery({
queryKey: [QueryKeys.InstantMix, item.Id!],
queryFn: () => fetchInstantMixFromItem(api, user, item),

View File

@@ -1,5 +1,5 @@
import React from 'react'
import type { CardProps as TamaguiCardProps } from 'tamagui'
import { CardProps as TamaguiCardProps } from 'tamagui'
import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { Text } from '../helpers/text'
@@ -9,7 +9,7 @@ import { useJellifyContext } from '../../../providers'
import { fetchMediaInfo } from '../../../api/queries/media'
import { QueryKeys } from '../../../enums/query-keys'
import { getQualityParams } from '../../../utils/mappings'
import { useSettingsContext } from '../../../providers/Settings'
import { useStreamingQualityContext } from '../../../providers/Settings'
import { useQuery } from '@tanstack/react-query'
interface CardProps extends TamaguiCardProps {
@@ -29,7 +29,7 @@ interface CardProps extends TamaguiCardProps {
*/
export function ItemCard(props: CardProps) {
const { api, user } = useJellifyContext()
const { streamingQuality } = useSettingsContext()
const streamingQuality = useStreamingQualityContext()
useQuery({
queryKey: [QueryKeys.MediaSources, streamingQuality, props.item.Id],

View File

@@ -1,4 +1,4 @@
import { StackParamList } from '../../types'
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { XStack, YStack } from 'tamagui'
@@ -16,7 +16,9 @@ import { useQuery } from '@tanstack/react-query'
import { fetchMediaInfo } from '../../../api/queries/media'
import { QueryKeys } from '../../../enums/query-keys'
import { useJellifyContext } from '../../../providers'
import { useSettingsContext } from '../../../providers/Settings'
import { useStreamingQualityContext } from '../../../providers/Settings'
import { useNavigation } from '@react-navigation/native'
import navigate from '../../../../navigation'
/**
* Displays an item as a row in a list.
@@ -32,19 +34,20 @@ import { useSettingsContext } from '../../../providers/Settings'
export default function ItemRow({
item,
queueName,
navigation,
onPress,
circular,
}: {
item: BaseItemDto
queueName: string
navigation: NativeStackNavigationProp<StackParamList>
onPress?: () => void
circular?: boolean
}): React.JSX.Element {
const useLoadNewQueue = useLoadQueueContext()
const { api, user } = useJellifyContext()
const { streamingQuality } = useSettingsContext()
const streamingQuality = useStreamingQualityContext()
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
useQuery({
queryKey: [QueryKeys.MediaSources, streamingQuality, item.Id],
@@ -84,9 +87,9 @@ export default function ItemRow({
minHeight={'$7'}
width={'100%'}
onLongPress={() => {
navigation.navigate('Details', {
rootNavigation.navigate('Context', {
item,
isNested: false,
navigation,
})
}}
onPress={() => {
@@ -97,16 +100,12 @@ export default function ItemRow({
switch (item.Type) {
case 'MusicArtist': {
navigation.navigate('Artist', {
artist: item,
})
navigation.navigate('Artist', { artist: item })
break
}
case 'MusicAlbum': {
navigation.navigate('Album', {
album: item,
})
navigation.navigate('Album', { album: item })
break
}
}
@@ -164,9 +163,8 @@ export default function ItemRow({
<Icon
name='dots-horizontal'
onPress={() => {
navigation.navigate('Details', {
rootNavigation.navigate('Context', {
item,
isNested: false,
})
}}
/>

View File

@@ -5,7 +5,7 @@ import { RunTimeTicks } from '../helpers/time-codes'
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import Icon from './icon'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../types'
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { QueuingType } from '../../../enums/queuing-type'
import { Queue } from '../../../player/types/queue-item'
import FavoriteIcon from './favorite-icon'
@@ -19,13 +19,13 @@ import DownloadedIcon from './downloaded-icon'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchMediaInfo } from '../../../api/queries/media'
import { useSettingsContext } from '../../../providers/Settings'
import { useStreamingQualityContext } from '../../../providers/Settings'
import { getQualityParams } from '../../../utils/mappings'
import { useNowPlayingContext } from '../../../providers/Player'
import { useNavigation } from '@react-navigation/native'
export interface TrackProps {
track: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
tracklist?: BaseItemDto[] | undefined
index: number
queue: Queue
@@ -43,7 +43,6 @@ export interface TrackProps {
export default function Track({
track,
tracklist,
navigation,
index,
queue,
showArtwork,
@@ -56,12 +55,16 @@ export default function Track({
onRemove,
}: TrackProps): React.JSX.Element {
const theme = useTheme()
const stackNavigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const { api, user } = useJellifyContext()
const nowPlaying = useNowPlayingContext()
const playQueue = usePlayQueueContext()
const useLoadNewQueue = useLoadQueueContext()
const { downloadedTracks, networkStatus } = useNetworkContext()
const { streamingQuality } = useSettingsContext()
const streamingQuality = useStreamingQualityContext()
const isPlaying = nowPlaying?.item.Id === track.Id
@@ -102,9 +105,8 @@ export default function Track({
onLongPress
? () => onLongPress()
: () => {
navigation.navigate('Details', {
rootNavigation.navigate('Context', {
item: track,
isNested: isNested,
})
}
}
@@ -202,9 +204,8 @@ export default function Track({
if (showRemove) {
if (onRemove) onRemove()
} else {
navigation.navigate('Details', {
rootNavigation.navigate('Context', {
item: track,
isNested: isNested,
})
}
}}

View File

@@ -1,5 +1,5 @@
import React from 'react'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import MaterialDesignIcons from '@react-native-vector-icons/material-design-icons'
import { CheckboxProps, XStack, Checkbox, Label } from 'tamagui'
export function CheckboxWithLabel({
@@ -12,7 +12,7 @@ export function CheckboxWithLabel({
<XStack width={150} alignItems='center' gap='$4'>
<Checkbox id={id} size={size} {...checkboxProps}>
<Checkbox.Indicator>
<Icon name='check' />
<MaterialDesignIcons name='check' />
</Checkbox.Indicator>
</Checkbox>

View File

@@ -1,5 +1,4 @@
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { StackParamList } from '../../types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React from 'react'
import { ItemCard } from '../../../components/Global/components/item-card'
@@ -9,12 +8,14 @@ import Icon from '../../Global/components/icon'
import { useHomeContext } from '../../../providers/Home'
import { ActivityIndicator } from 'react-native'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
import { RootStackParamList } from '../../../screens/types'
export default function FrequentArtists(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
export default function FrequentArtists({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { frequentArtistsInfiniteQuery } = useHomeContext()
const theme = useTheme()
const { horizontalItems } = useDisplayContext()
@@ -44,6 +45,12 @@ export default function FrequentArtists({
artist,
})
}}
onLongPress={() => {
rootNavigation.navigate('Context', {
item: artist,
navigation,
})
}}
size={'$11'}
/>
)}

View File

@@ -1,20 +1,17 @@
import { StackParamList } from '../../types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useHomeContext } from '../../../providers/Home'
import { View, XStack } from 'tamagui'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { ItemCard } from '../../../components/Global/components/item-card'
import { QueuingType } from '../../../enums/queuing-type'
import { trigger } from 'react-native-haptic-feedback'
import Icon from '../../Global/components/icon'
import { useLoadQueueContext } from '../../../providers/Player/queue'
import { H4 } from '../../../components/Global/helpers/text'
import { useDisplayContext } from '../../../providers/Display/display-provider'
export default function FrequentlyPlayedTracks({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
import HomeStackParamList from '../../../screens/Home/types'
import { useNavigation } from '@react-navigation/native'
import { RootStackParamList } from '../../../screens/types'
export default function FrequentlyPlayedTracks(): React.JSX.Element {
const {
frequentlyPlayed,
fetchNextFrequentlyPlayed,
@@ -22,6 +19,10 @@ export default function FrequentlyPlayedTracks({
isFetchingFrequentlyPlayed,
} = useHomeContext()
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const useLoadNewQueue = useLoadQueueContext()
const { horizontalItems } = useDisplayContext()
@@ -44,9 +45,9 @@ export default function FrequentlyPlayedTracks({
<HorizontalCardList
data={
(frequentlyPlayed?.pages.flatMap((page) => page).length ?? 0 > horizontalItems)
? frequentlyPlayed?.pages.flatMap((page) => page).slice(0, horizontalItems)
: frequentlyPlayed?.pages.flatMap((page) => page)
(frequentlyPlayed?.length ?? 0 > horizontalItems)
? frequentlyPlayed?.slice(0, horizontalItems)
: frequentlyPlayed
}
renderItem={({ item: track, index }) => (
<ItemCard
@@ -59,18 +60,16 @@ export default function FrequentlyPlayedTracks({
useLoadNewQueue({
track,
index,
tracklist: frequentlyPlayed?.pages.flatMap((page) => page) ?? [
track,
],
tracklist: frequentlyPlayed ?? [track],
queue: 'On Repeat',
queuingType: QueuingType.FromSelection,
startPlayback: true,
})
}}
onLongPress={() => {
trigger('impactMedium')
navigation.navigate('Context', {
rootNavigation.navigate('Context', {
item: track,
navigation,
})
}}
/>

View File

@@ -2,21 +2,23 @@ import React from 'react'
import { View, XStack } from 'tamagui'
import { useHomeContext } from '../../../providers/Home'
import { H4, Text } from '../../Global/helpers/text'
import { StackParamList } from '../../types'
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { ItemCard } from '../../Global/components/item-card'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import Icon from '../../Global/components/icon'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import { ActivityIndicator } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
export default function RecentArtists({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
export default function RecentArtists(): React.JSX.Element {
const { recentArtistsInfiniteQuery } = useHomeContext()
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const { horizontalItems } = useDisplayContext()
return (
<View>
@@ -43,6 +45,12 @@ export default function RecentArtists({
artist: recentArtist,
})
}}
onLongPress={() => {
rootNavigation.navigate('Context', {
item: recentArtist,
navigation,
})
}}
size={'$11'}
></ItemCard>
)}

View File

@@ -4,22 +4,22 @@ import { useHomeContext } from '../../../providers/Home'
import { H4 } from '../../Global/helpers/text'
import { ItemCard } from '../../Global/components/item-card'
import { useNowPlayingContext } from '../../../providers/Player'
import { StackParamList } from '../../types'
import { BaseStackParamList, RootStackParamList } from '../../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { trigger } from 'react-native-haptic-feedback'
import { QueuingType } from '../../../enums/queuing-type'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import Icon from '../../Global/components/icon'
import { useLoadQueueContext } from '../../../providers/Player/queue'
import { useDisplayContext } from '../../../providers/Display/display-provider'
import { useNavigation } from '@react-navigation/native'
import HomeStackParamList from '../../../screens/Home/types'
export default function RecentlyPlayed({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
export default function RecentlyPlayed(): React.JSX.Element {
const nowPlaying = useNowPlayingContext()
const navigation = useNavigation<NativeStackNavigationProp<HomeStackParamList>>()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const useLoadNewQueue = useLoadQueueContext()
const { recentTracks, fetchNextRecentTracks, hasNextRecentTracks, isFetchingRecentTracks } =
@@ -46,9 +46,9 @@ export default function RecentlyPlayed({
<HorizontalCardList
data={
(recentTracks?.pages.flatMap((page) => page).length ?? 0 > horizontalItems)
? recentTracks?.pages.flatMap((page) => page).slice(0, horizontalItems)
: recentTracks?.pages.flatMap((page) => page)
(recentTracks?.length ?? 0 > horizontalItems)
? recentTracks?.slice(0, horizontalItems)
: recentTracks
}
renderItem={({ index, item: recentlyPlayedTrack }) => (
<ItemCard
@@ -62,19 +62,16 @@ export default function RecentlyPlayed({
useLoadNewQueue({
track: recentlyPlayedTrack,
index: index,
tracklist: recentTracks?.pages.flatMap((page) => page) ?? [
recentlyPlayedTrack,
],
tracklist: recentTracks ?? [recentlyPlayedTrack],
queue: 'Recently Played',
queuingType: QueuingType.FromSelection,
startPlayback: true,
})
}}
onLongPress={() => {
trigger('impactMedium')
navigation.navigate('Details', {
rootNavigation.navigate('Context', {
item: recentlyPlayedTrack,
isNested: false,
navigation,
})
}}
/>

View File

@@ -1,22 +1,15 @@
import { StackParamList } from '../types'
import { ScrollView, RefreshControl } from 'react-native'
import { YStack, Separator, getToken } from 'tamagui'
import RecentArtists from './helpers/recent-artists'
import RecentlyPlayed from './helpers/recently-played'
import { useHomeContext } from '../../providers/Home'
import { H5 } from '../Global/helpers/text'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import FrequentArtists from './helpers/frequent-artists'
import FrequentlyPlayedTracks from './helpers/frequent-tracks'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useJellifyContext } from '../../providers'
import { usePreventRemove } from '@react-navigation/native'
export function ProvidedHome({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
export function ProvidedHome(): React.JSX.Element {
usePreventRemove(true, () => {})
const { user } = useJellifyContext()
const { refreshing: refetching, onRefresh } = useHomeContext()
@@ -32,19 +25,19 @@ export function ProvidedHome({
removeClippedSubviews // Save memory usage
>
<YStack alignContent='flex-start'>
<RecentArtists navigation={navigation} />
<RecentArtists />
<Separator marginVertical={'$3'} />
<RecentlyPlayed navigation={navigation} />
<RecentlyPlayed />
<Separator marginVertical={'$3'} />
<FrequentArtists navigation={navigation} />
<FrequentArtists />
<Separator marginVertical={'$3'} />
<FrequentlyPlayedTracks navigation={navigation} />
<FrequentlyPlayedTracks />
</YStack>
</ScrollView>
)

View File

@@ -1,10 +1,10 @@
import { InstantMixProps } from '../types'
import { InstantMixProps } from '../../screens/types'
import { FlatList } from 'react-native'
import Track from '../Global/components/track'
import { Separator } from 'tamagui'
export default function InstantMix({ route, navigation }: InstantMixProps): React.JSX.Element {
const { item, mix } = route.params
export default function InstantMix({ route }: InstantMixProps): React.JSX.Element {
const { mix } = route.params
return (
<FlatList
@@ -14,7 +14,6 @@ export default function InstantMix({ route, navigation }: InstantMixProps): Reac
<Track
showArtwork
track={item}
navigation={navigation}
index={index}
queue={'Instant Mix'}
tracklist={mix}

View File

@@ -1,4 +1,3 @@
import { StackParamList } from '../types'
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
import PlaylistsTab from './components/playlists-tab'
import { getToken, useTheme } from 'tamagui'
@@ -8,13 +7,17 @@ import ArtistsTab from './components/artists-tab'
import AlbumsTab from './components/albums-tab'
import LibraryTabBar from './tab-bar'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { RouteProp } from '@react-navigation/native'
import LibraryStackParamList from '../../screens/Library/types'
const LibraryTabsNavigator = createMaterialTopTabNavigator()
export default function Library({
route,
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
route: RouteProp<LibraryStackParamList, 'Library'>
navigation: NativeStackNavigationProp<LibraryStackParamList, 'Library'>
}): React.JSX.Element {
const theme = useTheme()

View File

@@ -1,18 +1,12 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Albums from '../../Albums/component'
import { StackParamList } from '../../types'
import { useAlbumsInfiniteQueryContext } from '../../../providers/Library'
import { useNavigation } from '@react-navigation/native'
export default function AlbumsTab(): React.JSX.Element {
const albumsInfiniteQuery = useAlbumsInfiniteQueryContext()
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
return (
<Albums
albums={albumsInfiniteQuery.data}
navigation={navigation}
fetchNextPage={albumsInfiniteQuery.fetchNextPage}
hasNextPage={albumsInfiniteQuery.hasNextPage}
isPending={albumsInfiniteQuery.isPending}

View File

@@ -1,21 +1,16 @@
import { useNavigation } from '@react-navigation/native'
import Artists from '../../Artists/component'
import {
useArtistPageParamsContext,
useArtistsInfiniteQueryContext,
} from '../../../providers/Library'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../types'
export default function ArtistsTab(): React.JSX.Element {
const artistsInfiniteQuery = useArtistsInfiniteQueryContext()
const artistPageParams = useArtistPageParamsContext()
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
return (
<Artists
artistsInfiniteQuery={artistsInfiniteQuery}
navigation={navigation}
showAlphabeticalSelector={true}
artistPageParams={artistPageParams}
/>

View File

@@ -1,18 +1,12 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../types'
import Playlists from '../../Playlists/component'
import React from 'react'
import { useNavigation } from '@react-navigation/native'
import { usePlaylistsInfiniteQueryContext } from '../../../providers/Library'
export default function PlaylistsTab(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
const playlistsInfiniteQuery = usePlaylistsInfiniteQueryContext()
return (
<Playlists
navigation={navigation}
playlists={playlistsInfiniteQuery.data}
refetch={playlistsInfiniteQuery.refetch}
fetchNextPage={playlistsInfiniteQuery.fetchNextPage}

View File

@@ -1,21 +1,15 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useNavigation } from '@react-navigation/native'
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
import Tracks from '../../Tracks/component'
import { useTracksInfiniteQueryContext } from '../../../providers/Library'
import { useLibrarySortAndFilterContext } from '../../../providers/Library/sorting-filtering'
export default function TracksTab(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
const tracksInfiniteQuery = useTracksInfiniteQueryContext()
const { isFavorites, isDownloaded } = useLibrarySortAndFilterContext()
return (
<Tracks
navigation={navigation}
tracks={tracksInfiniteQuery.data}
queue={isFavorites ? 'Favorite Tracks' : isDownloaded ? 'Downloaded Tracks' : 'Library'}
filterDownloaded={isDownloaded}

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 { useSettingsContext } from '../../providers/Settings'
import { useReducedHapticsContext } from '../../providers/Settings'
export default function LibraryTabBar(props: MaterialTopTabBarProps) {
const { isFavorites, setIsFavorites, isDownloaded, setIsDownloaded } =
useLibrarySortAndFilterContext()
const { reducedHaptics } = useSettingsContext()
const reducedHaptics = useReducedHapticsContext()
const insets = useSafeAreaInsets()

View File

@@ -3,7 +3,7 @@ import { useNowPlayingContext } from '../../../providers/Player'
import { getToken, useTheme, View, YStack, ZStack } from 'tamagui'
import { useColorScheme } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import { useSettingsContext } from '../../../providers/Settings'
import { useThemeSettingContext } from '../../../providers/Settings'
import { getPrimaryBlurhashFromDto } from '../../../utils/blurhash'
import { Blurhash } from 'react-native-blurhash'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
@@ -17,7 +17,7 @@ export default function BlurredBackground({
}): React.JSX.Element {
const nowPlaying = useNowPlayingContext()
const { theme: themeSetting } = useSettingsContext()
const themeSetting = useThemeSettingContext()
const theme = useTheme()

View File

@@ -3,13 +3,12 @@ import { XStack } from 'tamagui'
import Icon from '../../Global/components/icon'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
import { useNavigation } from '@react-navigation/native'
export default function Footer(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
export default function Footer({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
return (
<XStack justifyContent='flex-end' alignItems='center' marginHorizontal={'$5'} flex={1}>
<XStack alignItems='center' justifyContent='flex-start' flex={1}>

View File

@@ -5,18 +5,15 @@ import { getToken, useWindowDimensions, XStack, YStack, useTheme } from 'tamagui
import { Text } from '../../Global/helpers/text'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Icon from '../../Global/components/icon'
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
import React from 'react'
import { State } from 'react-native-track-player'
import ItemImage from '../../Global/components/image'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import { useNavigation } from '@react-navigation/native'
export default function PlayerHeader({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { api } = useJellifyContext()
export default function PlayerHeader(): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const nowPlaying = useNowPlayingContext()
const playbackState = usePlaybackStateContext()
@@ -57,7 +54,7 @@ export default function PlayerHeader({
small
name='dots-vertical'
onPress={() => {
navigation.navigate('Details', {
navigation.navigate('Context', {
item: nowPlaying!.item,
isNested: true,
})

View File

@@ -9,7 +9,7 @@ import { useNowPlayingContext, useSeekToContext } from '../../../providers/Playe
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
import { UPDATE_INTERVAL } from '../../../player/config'
import { ProgressMultiplier } from '../component.config'
import { useSettingsContext } from '../../../providers/Settings'
import { useReducedHapticsContext } from '../../../providers/Settings'
// Create a simple pan gesture
const scrubGesture = Gesture.Pan().runOnJS(true)
@@ -18,7 +18,7 @@ export default function Scrubber(): React.JSX.Element {
const useSeekTo = useSeekToContext()
const nowPlaying = useNowPlayingContext()
const { width } = useSafeAreaFrame()
const { reducedHaptics } = useSettingsContext()
const reducedHaptics = useReducedHapticsContext()
// Get progress from the track player with the specified update interval
const { position, duration } = useProgress(UPDATE_INTERVAL)

View File

@@ -4,28 +4,37 @@ import { TextTickerConfig } from '../component.config'
import { useNowPlayingContext } from '../../../providers/Player'
import { Text } from '../../Global/helpers/text'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../types'
import { RootStackParamList } from '../../../screens/types'
import React, { useMemo } from 'react'
import ItemImage from '../../Global/components/image'
import { useQuery } from '@tanstack/react-query'
import { fetchItem } from '../../../api/queries/item'
import { fetchItem, fetchItems } from '../../../api/queries/item'
import { useJellifyContext } from '../../../providers'
import FavoriteButton from '../../Global/components/favorite-button'
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
import Icon from '../../Global/components/icon'
import { useNavigation } from '@react-navigation/native'
import navigate from '../../../../navigation'
import { QueryKeys } from '../../../enums/query-keys'
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
export default function SongInfo({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { api } = useJellifyContext()
export default function SongInfo(): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const nowPlaying = useNowPlayingContext()
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const { data: album } = useQuery({
queryKey: ['album', nowPlaying!.item.AlbumId],
queryKey: [QueryKeys.Album, nowPlaying!.item.AlbumId],
queryFn: () => fetchItem(api, nowPlaying!.item.AlbumId!),
})
const { data: artists } = useQuery({
queryKey: [QueryKeys.TrackArtists, nowPlaying!.item.ArtistItems],
queryFn: () => fetchItems(api, user, library, [BaseItemKind.MusicArtist]),
select: (data) => data.data,
})
return useMemo(() => {
return (
<XStack flex={1}>
@@ -34,7 +43,8 @@ export default function SongInfo({
onPress={() => {
if (album) {
navigation.goBack() // Dismiss player modal
navigation.navigate('Tabs', {
navigate('Tabs', {
screen: 'Library',
params: {
screen: 'Album',
@@ -80,12 +90,12 @@ export default function SongInfo({
})
} else {
navigation.goBack() // Dismiss player modal
navigation.navigate('Tabs', {
navigate('Tabs', {
screen: 'Library',
params: {
screen: 'Artist',
params: {
artist: nowPlaying!.item.ArtistItems![0],
artist: nowPlaying!.item.ArtistItems[0],
},
},
})
@@ -99,7 +109,16 @@ export default function SongInfo({
</Animated.View>
</YStack>
<XStack justifyContent='flex-end' alignItems='center' flexShrink={1}>
<XStack gap={'$3'} justifyContent='flex-end' alignItems='center' flexShrink={1}>
<Icon
name='dots-horizontal-circle-outline'
onPress={() => {
navigation.navigate('Context', {
item: nowPlaying!.item,
isNested: true,
})
}}
/>
<FavoriteButton item={nowPlaying!.item} />
</XStack>
</XStack>

View File

@@ -1,4 +1,4 @@
import { StackParamList } from '../types'
import { RootStackParamList } from '../../screens/types'
import { useNowPlayingContext } from '../../providers/Player'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React, { useCallback, useState } from 'react'
@@ -27,7 +27,7 @@ import SongInfo from './components/song-info'
export default function PlayerScreen({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const [showToast, setShowToast] = useState(true)
@@ -54,7 +54,7 @@ export default function PlayerScreen({
<BlurredBackground width={width} height={height} />
<YStack fullscreen marginBottom={bottom}>
<PlayerHeader navigation={navigation} />
<PlayerHeader />
<XStack
justifyContent='center'
@@ -64,7 +64,7 @@ export default function PlayerScreen({
maxWidth={width / 1.1}
flex={2}
>
<SongInfo navigation={navigation} />
<SongInfo />
</XStack>
<XStack justifyContent='center' flex={1}>
@@ -74,7 +74,7 @@ export default function PlayerScreen({
<Controls />
<Footer navigation={navigation} />
<Footer />
</YStack>
</ZStack>
)}

View File

@@ -1,6 +1,6 @@
import Icon from '../Global/components/icon'
import Track from '../Global/components/track'
import { StackParamList } from '../types'
import { RootStackParamList } from '../../screens/types'
import { useNowPlayingContext } from '../../providers/Player'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import DraggableFlatList from 'react-native-draggable-flatlist'
@@ -20,7 +20,7 @@ import { useLayoutEffect } from 'react'
export default function Queue({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const nowPlaying = useNowPlayingContext()
@@ -84,7 +84,6 @@ export default function Queue({
>
<Track
queue={queueRef}
navigation={navigation}
track={queueItem.item}
index={getIndex() ?? 0}
showArtwork

View File

@@ -1,6 +1,6 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../types'
import { BaseStackParamList } from '../../../screens/types'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { getToken, getTokens, Separator, View, XStack, YStack } from 'tamagui'
import { AnimatedH5 } from '../../Global/helpers/text'
@@ -13,15 +13,16 @@ import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import { useJellifyContext } from '../../../providers'
import { ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { useNetworkContext } from '../../../../src/providers/Network'
import { useSettingsContext } from '../../../../src/providers/Settings'
import { ActivityIndicator } from 'react-native'
import { mapDtoToTrack } from '../../../utils/mappings'
import { useLoadQueueContext } from '../../../providers/Player/queue'
import { QueuingType } from '../../../enums/queuing-type'
import { useDownloadQualityContext, useStreamingQualityContext } from '../../../providers/Settings'
import navigate from '../../../../navigation'
export default function PlayliistTracklistHeader(
playlist: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>,
navigation: NativeStackNavigationProp<BaseStackParamList>,
editing: boolean,
playlistTracks: BaseItemDto[],
canEdit: boolean | undefined,
@@ -123,7 +124,6 @@ export default function PlayliistTracklistHeader(
<PlaylistHeaderControls
editing={editing}
setEditing={setEditing}
navigation={navigation}
playlist={playlist}
playlistTracks={playlistTracks}
canEdit={canEdit}
@@ -138,20 +138,19 @@ export default function PlayliistTracklistHeader(
function PlaylistHeaderControls({
editing,
setEditing,
navigation,
playlist,
playlistTracks,
canEdit,
}: {
editing: boolean
setEditing: (editing: boolean) => void
navigation: NativeStackNavigationProp<StackParamList>
playlist: BaseItemDto
playlistTracks: BaseItemDto[]
canEdit: boolean | undefined
}): React.JSX.Element {
const { useDownloadMultiple, pendingDownloads } = useNetworkContext()
const { downloadQuality, streamingQuality } = useSettingsContext()
const downloadQuality = useDownloadQualityContext()
const streamingQuality = useStreamingQualityContext()
const useLoadNewQueue = useLoadQueueContext()
const isDownloading = pendingDownloads.length != 0
const { sessionId, api } = useJellifyContext()
@@ -185,11 +184,19 @@ function PlaylistHeaderControls({
<Icon
color={'$danger'}
name='delete-sweep-outline' // otherwise use "delete-circle"
onPress={() => navigation.navigate('DeletePlaylist', { playlist })}
onPress={() => {
navigate('Tabs', {
screen: 'Library',
params: {
screen: 'DeletePlaylist',
params: { playlist },
},
})
}}
small
/>
) : (
<InstantMixButton item={playlist} navigation={navigation} />
<InstantMixButton item={playlist} />
)}
</YStack>

View File

@@ -8,6 +8,9 @@ import PlayliistTracklistHeader from './components/header'
import { usePlaylistContext } from '../../providers/Playlist'
import { useAnimatedScrollHandler } from 'react-native-reanimated'
import AnimatedDraggableFlatList from '../Global/components/animated-draggable-flat-list'
import { useNavigation } from '@react-navigation/native'
import { RootStackParamList } from '../../screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
export default function Playlist({
playlist,
@@ -25,6 +28,8 @@ export default function Playlist({
useRemoveFromPlaylist,
} = usePlaylistContext()
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>()
const scrollOffsetHandler = useAnimatedScrollHandler({
onScroll: (event) => {
'worklet'
@@ -77,7 +82,6 @@ export default function Playlist({
{editing && canEdit && <Icon name='drag' onPress={drag} />}
<Track
navigation={navigation}
track={track}
tracklist={playlistTracks ?? []}
index={getIndex() ?? 0}
@@ -86,9 +90,8 @@ export default function Playlist({
onLongPress={() => {
editing
? drag()
: navigation.navigate('Details', {
: rootNavigation.navigate('Context', {
item: track,
isNested: false,
})
}}
showRemove={editing}

View File

@@ -1,10 +1,10 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import { BaseStackParamList } from '../../screens/types'
export interface PlaylistProps {
playlist: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<BaseStackParamList>
canEdit?: boolean | undefined
}

View File

@@ -1,12 +1,24 @@
import { RefreshControl } from 'react-native-gesture-handler'
import { Separator } from 'tamagui'
import { PlaylistsProps } from '../types'
import { FlashList } from '@shopify/flash-list'
import ItemRow from '../Global/components/item-row'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { FetchNextPageOptions } from '@tanstack/react-query'
import { useNavigation } from '@react-navigation/native'
import { BaseStackParamList } from '@/src/screens/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
export interface PlaylistsProps {
canEdit?: boolean | undefined
playlists: BaseItemDto[] | undefined
refetch: () => void
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
}
export default function Playlists({
playlists,
navigation,
refetch,
fetchNextPage,
hasNextPage,
@@ -14,6 +26,7 @@ export default function Playlists({
isFetchingNextPage,
canEdit,
}: PlaylistsProps): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
return (
<FlashList
contentInsetAdjustmentBehavior='automatic'
@@ -28,7 +41,6 @@ export default function Playlists({
onPress={() => {
navigation.navigate('Playlist', { playlist, canEdit })
}}
navigation={navigation}
queueName={playlist.Name ?? 'Untitled Playlist'}
/>
)}

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react'
import Input from '../Global/helpers/input'
import ItemRow from '../Global/components/item-row'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import { RootStackParamList } from '../../screens/types'
import { QueryKeys } from '../../enums/query-keys'
import { fetchSearchResults } from '../../api/queries/search'
import { useQuery } from '@tanstack/react-query'
@@ -15,10 +15,11 @@ import { isEmpty } from 'lodash'
import HorizontalCardList from '../Global/components/horizontal-list'
import { ItemCard } from '../Global/components/item-card'
import { useJellifyContext } from '../../providers'
import SearchParamList from '../../screens/Search/types'
export default function Search({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<SearchParamList, 'Search'>
}): React.JSX.Element {
const { api, library, user } = useJellifyContext()
@@ -104,20 +105,14 @@ export default function Search({
ListEmptyComponent={() => {
return (
<YStack alignContent='center' justifyContent='flex-end' marginTop={'$4'}>
{fetchingResults ? (
<Spinner />
) : (
<Suggestions suggestions={suggestions} navigation={navigation} />
)}
{fetchingResults ? <Spinner /> : <Suggestions suggestions={suggestions} />}
</YStack>
)
}}
// We're displaying artists separately so we're going to filter them out here
data={items?.filter((result) => result.Type !== 'MusicArtist')}
refreshing={fetchingResults}
renderItem={({ item }) => (
<ItemRow item={item} queueName={searchString ?? 'Search'} navigation={navigation} />
)}
renderItem={({ item }) => <ItemRow item={item} queueName={searchString ?? 'Search'} />}
style={{
marginHorizontal: getToken('$2'),
marginTop: getToken('$4'),

View File

@@ -1,30 +1,31 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { FlatList, RefreshControl } from 'react-native'
import { StackParamList } from '../types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import ItemRow from '../Global/components/item-row'
import { H3, Text } from '../Global/helpers/text'
import { getToken, Separator, YStack } from 'tamagui'
import { Separator, YStack } from 'tamagui'
import { ItemCard } from '../Global/components/item-card'
import HorizontalCardList from '../Global/components/horizontal-list'
import { FlashList } from '@shopify/flash-list'
import SearchParamList from '../../screens/Search/types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
interface SuggestionsProps {
export default function Suggestions({
suggestions,
}: {
suggestions: BaseItemDto[] | undefined
navigation: NativeStackNavigationProp<StackParamList>
}
}): React.JSX.Element {
const navigation = useNavigation<NativeStackNavigationProp<SearchParamList>>()
export default function Suggestions(props: SuggestionsProps): React.JSX.Element {
return (
<FlashList
// Artists are displayed in the header, so we'll filter them out here
data={props.suggestions?.filter((suggestion) => suggestion.Type !== 'MusicArtist')}
data={suggestions?.filter((suggestion) => suggestion.Type !== 'MusicArtist')}
ListHeaderComponent={
<YStack>
<H3>Suggestions</H3>
<HorizontalCardList
data={props.suggestions?.filter(
data={suggestions?.filter(
(suggestion) => suggestion.Type === 'MusicArtist',
)}
renderItem={({ item: suggestedArtist }) => {
@@ -32,7 +33,7 @@ export default function Suggestions(props: SuggestionsProps): React.JSX.Element
<ItemCard
item={suggestedArtist}
onPress={() => {
props.navigation.push('Artist', {
navigation.push('Artist', {
artist: suggestedArtist,
})
}}
@@ -51,9 +52,7 @@ export default function Suggestions(props: SuggestionsProps): React.JSX.Element
</Text>
}
renderItem={({ item }) => {
return (
<ItemRow item={item} queueName={'Suggestions'} navigation={props.navigation} />
)
return <ItemRow item={item} queueName={'Suggestions'} />
}}
style={{
marginHorizontal: 2,

View File

@@ -9,13 +9,13 @@ 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 { useSettingsContext } from '../../providers/Settings'
import { useDevToolsContext } from '../../providers/Settings'
const SettingsTabsNavigator = createMaterialTopTabNavigator()
export default function Settings(): React.JSX.Element {
const theme = useTheme()
const { devTools } = useSettingsContext()
const devTools = useDevToolsContext()
return (
<SettingsTabsNavigator.Navigator

View File

@@ -11,11 +11,11 @@ import { FlatList, Linking } from 'react-native'
import { H6, ScrollView, Separator, XStack, YStack } from 'tamagui'
import Icon from '../../../Global/components/icon'
import { useEffect, useState } from 'react'
import { useSettingsContext } from '../../../../providers/Settings'
import { useSetDevToolsContext } from '../../../../providers/Settings'
export default function InfoTabIndex({ navigation }: InfoTabNativeStackNavigationProp) {
const { api } = useJellifyContext()
const { setDevTools } = useSettingsContext()
const setDevTools = useSetDevToolsContext()
const [versionNumberPresses, setVersionNumberPresses] = useState(0)
@@ -66,7 +66,7 @@ export default function InfoTabIndex({ navigation }: InfoTabNativeStackNavigatio
Linking.openURL('https://discord.gg/yf8fBatktn')
}
>
<Icon name='discord' small color='$borderColor' />
<Icon name='chat' small color='$borderColor' />
<Text>Join Discord</Text>
</XStack>
</XStack>

View File

@@ -3,10 +3,15 @@ import { RadioGroup, YStack } from 'tamagui'
import { RadioGroupItemWithLabel } from '../../Global/helpers/radio-group-item-with-label'
import { Text } from '../../Global/helpers/text'
import { getQualityLabel, getBandwidthEstimate } from '../utils/quality'
import { StreamingQuality, useSettingsContext } from '../../../providers/Settings'
import {
StreamingQuality,
useSetStreamingQualityContext,
useStreamingQualityContext,
} from '../../../providers/Settings'
export default function PlaybackTab(): React.JSX.Element {
const { streamingQuality, setStreamingQuality } = useSettingsContext()
const streamingQuality = useStreamingQualityContext()
const setStreamingQuality = useSetStreamingQualityContext()
return (
<SettingsListGroup

View File

@@ -1,26 +1,38 @@
import { RadioGroup, YStack } from 'tamagui'
import { Theme, useSettingsContext } from '../../../providers/Settings'
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 { Text } from '../../Global/helpers/text'
export default function PreferencesTab(): React.JSX.Element {
const { setSendMetrics, sendMetrics, setReducedHaptics, reducedHaptics, theme, setTheme } =
useSettingsContext()
const setSendMetrics = useSetSendMetricsContext()
const sendMetrics = useSendMetricsContext()
const setReducedHaptics = useSetReducedHapticsContext()
const reducedHaptics = useReducedHapticsContext()
const themeSetting = useThemeSettingContext()
const setThemeSetting = useSetThemeSettingContext()
return (
<SettingsListGroup
settingsList={[
{
title: 'Theme',
subTitle: `Current: ${theme}`,
subTitle: `Current: ${themeSetting}`,
iconName: 'theme-light-dark',
iconColor: `${theme === 'system' ? '$borderColor' : '$primary'}`,
iconColor: `${themeSetting === 'system' ? '$borderColor' : '$primary'}`,
children: (
<YStack gap='$2' paddingVertical='$2'>
<RadioGroup
value={theme}
onValueChange={(value) => setTheme(value as Theme)}
value={themeSetting}
onValueChange={(value) => setThemeSetting(value as Theme)}
>
<RadioGroupItemWithLabel size='$3' value='system' label='System' />
<RadioGroupItemWithLabel size='$3' value='light' label='Light' />

View File

@@ -1,14 +1,23 @@
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 } from '../../../providers/Settings'
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'
export default function StorageTab(): React.JSX.Element {
const { autoDownload, setAutoDownload, downloadQuality, setDownloadQuality } =
useSettingsContext()
const autoDownload = useAutoDownloadContext()
const setAutoDownload = useSetAutoDownloadContext()
const downloadQuality = useDownloadQualityContext()
const setDownloadQuality = useSetDownloadQualityContext()
const { downloadedTracks } = useNetworkContext()
return (

View File

@@ -1,10 +1,7 @@
import React, { useCallback, useEffect } from 'react'
import React, { useCallback } from 'react'
import Track from '../Global/components/track'
import { FlatList } from 'react-native'
import { getTokens, Separator } from 'tamagui'
import { StackParamList } from '../types'
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { Queue } from '../../player/types/queue-item'
import { useNetworkContext } from '../../providers/Network'
import { queryClient } from '../../constants/query-client'
@@ -16,7 +13,6 @@ export default function Tracks({
queue,
fetchNextPage,
hasNextPage,
navigation,
filterDownloaded,
filterFavorites,
}: {
@@ -24,7 +20,6 @@ export default function Tracks({
queue: Queue
fetchNextPage: () => void
hasNextPage: boolean
navigation: NativeStackNavigationProp<StackParamList>
filterDownloaded?: boolean | undefined
filterFavorites?: boolean | undefined
}): React.JSX.Element {
@@ -64,7 +59,6 @@ export default function Tracks({
data={tracksToDisplay()}
renderItem={({ index, item: track }) => (
<Track
navigation={navigation}
showArtwork
index={0}
track={track}

View File

@@ -7,7 +7,7 @@ import { JellifyUserDataProvider } from '../providers/UserData'
import { NetworkContextProvider } from '../providers/Network'
import { QueueProvider } from '../providers/Player/queue'
import { DisplayProvider } from '../providers/Display/display-provider'
import { SettingsProvider, useSettingsContext } from '../providers/Settings'
import { useSendMetricsContext, useThemeSettingContext } from '../providers/Settings'
import {
createTelemetryDeck,
TelemetryDeckProvider,
@@ -26,7 +26,7 @@ import { CarPlayProvider } from '../providers/CarPlay'
* @returns The {@link Jellify} component
*/
export default function Jellify(): React.JSX.Element {
const { theme } = useSettingsContext()
const theme = useThemeSettingContext()
const isDarkMode = useColorScheme() === 'dark'
@@ -44,7 +44,7 @@ export default function Jellify(): React.JSX.Element {
}
function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): React.JSX.Element {
const { sendMetrics } = useSettingsContext()
const sendMetrics = useSendMetricsContext()
/**
* Create the TelemetryDeck instance, which is used to send telemetry data to the server
@@ -68,7 +68,7 @@ function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): Rea
* @returns The {@link App} component
*/
function App(): React.JSX.Element {
const { sendMetrics } = useSettingsContext()
const sendMetrics = useSendMetricsContext()
const telemetrydeck = useTelemetryDeck()
const theme = useTheme()

View File

@@ -1,227 +0,0 @@
import { QueryKeys } from '../enums/query-keys'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { Queue } from '../player/types/queue-item'
import { MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
import {
InfiniteData,
InfiniteQueryObserverResult,
UseInfiniteQueryResult,
} from '@tanstack/react-query'
import { RefObject } from 'react'
export type StackParamList = {
Login: {
screen: keyof StackParamList
}
ServerAddress: undefined
ServerAuthentication: undefined
LibrarySelection: undefined
HomeScreen: undefined
Home: undefined
AddPlaylist: undefined
RecentArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
MostPlayedArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
RecentTracks: {
tracks: InfiniteData<BaseItemDto[], unknown> | undefined
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
}
MostPlayedTracks: {
tracks: InfiniteData<BaseItemDto[], unknown> | undefined
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
}
UserPlaylists: {
playlists: BaseItemDto[]
}
Tracks: {
tracks: InfiniteData<BaseItemDto[], unknown> | undefined
queue: Queue
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
}
Discover: undefined
RecentlyAdded: {
albums: BaseItemDto[] | undefined
navigation: NativeStackNavigationProp<StackParamList>
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
}
PublicPlaylists: {
playlists: BaseItemDto[] | undefined
navigation: NativeStackNavigationProp<StackParamList>
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
refetch: () => void
}
SuggestedArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
navigation: NativeStackNavigationProp<StackParamList>
}
LibraryScreen: undefined
Library: undefined
DeletePlaylist: {
playlist: BaseItemDto
}
Search: undefined
Settings: undefined
Account: undefined
Server: undefined
Playback: undefined
Labs: undefined
SignOut: undefined
Tabs: {
screen: keyof StackParamList
params: object
}
PlayerScreen: undefined
Player: undefined
Queue: undefined
MultipleArtists: {
artists: BaseItemDto[]
}
Artist: {
artist: BaseItemDto
}
ArtistAlbums: undefined
ArtistEps: undefined
ArtistFeaturedOn: undefined
SimilarArtists: {
artist: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
}
Album: {
album: BaseItemDto
}
Playlist: {
playlist: BaseItemDto
canEdit?: boolean | undefined
}
Details: {
item: BaseItemDto
isNested: boolean | undefined
}
Offline: undefined
InstantMix: {
item: BaseItemDto
mix: BaseItemDto[]
}
Context: {
item: BaseItemDto
}
}
export type LoginProps = NativeStackScreenProps<StackParamList, 'Login'>
export type ServerAddressProps = NativeStackScreenProps<StackParamList, 'ServerAddress'>
export type ServerAuthenticationProps = NativeStackScreenProps<
StackParamList,
'ServerAuthentication'
>
export type LibrarySelectionProps = NativeStackScreenProps<StackParamList, 'LibrarySelection'>
export type TabProps = NativeStackScreenProps<StackParamList, 'Tabs'>
export type PlayerProps = NativeStackScreenProps<StackParamList, 'Player'>
export type MultipleArtistsProps = NativeStackScreenProps<StackParamList, 'MultipleArtists'>
export type ProvidedHomeProps = NativeStackScreenProps<StackParamList, 'HomeScreen'>
export type AddPlaylistProps = NativeStackScreenProps<StackParamList, 'AddPlaylist'>
export type RecentArtistsProps = NativeStackScreenProps<StackParamList, 'RecentArtists'>
export type RecentTracksProps = NativeStackScreenProps<StackParamList, 'RecentTracks'>
export type MostPlayedArtistsProps = NativeStackScreenProps<StackParamList, 'MostPlayedArtists'>
export type MostPlayedTracksProps = NativeStackScreenProps<StackParamList, 'MostPlayedTracks'>
export type UserPlaylistsProps = NativeStackScreenProps<StackParamList, 'UserPlaylists'>
export type DiscoverProps = NativeStackScreenProps<StackParamList, 'Discover'>
export type RecentlyAddedProps = NativeStackScreenProps<StackParamList, 'RecentlyAdded'>
export type PublicPlaylistsProps = NativeStackScreenProps<StackParamList, 'PublicPlaylists'>
export type SuggestedArtistsProps = NativeStackScreenProps<StackParamList, 'SuggestedArtists'>
export type HomeArtistProps = NativeStackScreenProps<StackParamList, 'Artist'>
export type ArtistAlbumsProps = NativeStackScreenProps<StackParamList, 'ArtistAlbums'>
export type ArtistEpsProps = NativeStackScreenProps<StackParamList, 'ArtistEps'>
export type ArtistFeaturedOnProps = NativeStackScreenProps<StackParamList, 'ArtistFeaturedOn'>
export type AlbumProps = NativeStackScreenProps<StackParamList, 'Album'>
export type HomePlaylistProps = NativeStackScreenProps<StackParamList, 'Playlist'>
export type QueueProps = NativeStackScreenProps<StackParamList, 'Queue'>
export type LibraryProps = NativeStackScreenProps<StackParamList, 'LibraryScreen'>
export type TracksProps = NativeStackScreenProps<StackParamList, 'Tracks'>
export type ArtistsProps = {
navigation: NativeStackNavigationProp<StackParamList>
artistsInfiniteQuery: UseInfiniteQueryResult<
BaseItemDto[] | (string | number | BaseItemDto)[],
Error
>
showAlphabeticalSelector: boolean
artistPageParams?: RefObject<Set<string>>
}
export type AlbumsProps = {
albums: (string | number | BaseItemDto)[] | undefined
navigation: NativeStackNavigationProp<StackParamList>
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
showAlphabeticalSelector: boolean
}
export type GenresProps = {
genres: InfiniteData<BaseItemDto[], unknown> | undefined
navigation: NativeStackNavigationProp<StackParamList>
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
}
export type PlaylistsProps = {
canEdit?: boolean | undefined
playlists: BaseItemDto[] | undefined
navigation: NativeStackNavigationProp<StackParamList>
refetch: () => void
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
}
export type DeletePlaylistProps = NativeStackScreenProps<StackParamList, 'DeletePlaylist'>
export type DetailsProps = NativeStackScreenProps<StackParamList, 'Details'>
export type AccountDetailsProps = NativeStackScreenProps<StackParamList, 'Account'>
export type ServerDetailsProps = NativeStackScreenProps<StackParamList, 'Server'>
export type PlaybackDetailsProps = NativeStackScreenProps<StackParamList, 'Playback'>
export type LabsProps = NativeStackScreenProps<StackParamList, 'Labs'>
export type InstantMixProps = NativeStackScreenProps<StackParamList, 'InstantMix'>
export type ContextProps = NativeStackScreenProps<StackParamList, 'Context'>

View File

@@ -116,4 +116,6 @@ export enum QueryKeys {
* Query representing the fetching of suggested artists in an infinite query
*/
InfiniteSuggestedArtists = 'InfiniteSuggestedArtists',
Album = 'Album',
TrackArtists = 'TrackArtists',
}

View File

@@ -6,7 +6,6 @@ export const useUpdateOptions = async (isFavorite: boolean) => {
progressUpdateEventInterval: 1,
capabilities: CAPABILITIES,
notificationCapabilities: CAPABILITIES,
compactCapabilities: CAPABILITIES,
ratingType: RatingType.Heart,
likeOptions: {
isActive: isFavorite,

View File

@@ -15,7 +15,7 @@ import { useJellifyContext } from '..'
interface HomeContext {
refreshing: boolean
onRefresh: () => void
recentTracks: InfiniteData<BaseItemDto[], unknown> | undefined
recentTracks: BaseItemDto[] | undefined
fetchNextRecentTracks: () => void
hasNextRecentTracks: boolean
@@ -23,7 +23,7 @@ interface HomeContext {
fetchNextFrequentlyPlayed: () => void
hasNextFrequentlyPlayed: boolean
frequentlyPlayed: InfiniteData<BaseItemDto[], unknown> | undefined
frequentlyPlayed: BaseItemDto[] | undefined
isFetchingRecentTracks: boolean
isFetchingFrequentlyPlayed: boolean
@@ -49,6 +49,7 @@ const HomeContextInitializer = () => {
queryKey: [QueryKeys.RecentlyPlayed, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam),
initialPageParam: 0,
select: (data) => data.pages.flatMap((page) => page),
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for recent tracks')
return lastPage.length === QueryConfig.limits.recents ? lastPageParam + 1 : undefined
@@ -63,7 +64,7 @@ const HomeContextInitializer = () => {
console.debug('Getting next page for recent artists')
return lastPage.length > 0 ? lastPageParam + 1 : undefined
},
enabled: !!recentTracks && recentTracks.pages.length > 0 && !isPendingRecentTracks,
enabled: !!recentTracks && recentTracks.length > 0 && !isPendingRecentTracks,
})
const {
@@ -77,6 +78,7 @@ const HomeContextInitializer = () => {
} = useInfiniteQuery({
queryKey: [QueryKeys.FrequentlyPlayed, library?.musicLibraryId],
queryFn: ({ pageParam }) => fetchFrequentlyPlayed(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
console.debug('Getting next page for frequently played')
@@ -93,8 +95,7 @@ const HomeContextInitializer = () => {
console.debug('Getting next page for frequent artists')
return lastPage.length === 100 ? lastPageParam + 1 : undefined
},
enabled:
!!frequentlyPlayed && frequentlyPlayed.pages.length > 0 && !isStaleFrequentlyPlayed,
enabled: !!frequentlyPlayed && frequentlyPlayed.length > 0 && !isStaleFrequentlyPlayed,
})
const onRefresh = async () => {

View File

@@ -1,20 +1,13 @@
import React, { createContext, ReactNode, useContext, useEffect, useState, useMemo } from 'react'
import { JellifyDownload, JellifyDownloadProgress } from '../../types/JellifyDownload'
import {
useMutation,
UseMutationResult,
useQuery,
useQueryClient,
UseQueryResult,
} from '@tanstack/react-query'
import { useMutation, UseMutationResult, useQuery } from '@tanstack/react-query'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { mapDtoToTrack } from '../../utils/mappings'
import { deleteAudio, getAudioCache, saveAudio } from '../../components/Network/offlineModeUtils'
import { QueryKeys } from '../../enums/query-keys'
import { networkStatusTypes } from '../../components/Network/internetConnectionWatcher'
import DownloadProgress from '../../types/DownloadProgress'
import { useJellifyContext } from '..'
import { useSettingsContext } from '../Settings'
import { useDownloadQualityContext, useStreamingQualityContext } from '../Settings'
import { isUndefined } from 'lodash'
import RNFS from 'react-native-fs'
import { JellifyStorage } from './types'
@@ -39,7 +32,8 @@ interface NetworkContext {
const MAX_CONCURRENT_DOWNLOADS = 1
const NetworkContextInitializer = () => {
const { api, sessionId } = useJellifyContext()
const { downloadQuality, streamingQuality } = useSettingsContext()
const downloadQuality = useDownloadQualityContext()
const streamingQuality = useStreamingQualityContext()
const [downloadProgress, setDownloadProgress] = useState<JellifyDownloadProgress>({})
const [networkStatus, setNetworkStatus] = useState<networkStatusTypes | null>(null)

View File

@@ -31,7 +31,7 @@ import { PlaystateApi } from '@jellyfin/sdk/lib/generated-client/api/playstate-a
import { networkStatusTypes } from '../../components/Network/internetConnectionWatcher'
import { useJellifyContext } from '..'
import { isUndefined } from 'lodash'
import { useSettingsContext } from '../Settings'
import { useAutoDownloadContext } from '../Settings'
import {
getTracksToPreload,
shouldStartPrefetching,
@@ -448,7 +448,7 @@ const PlayerContextInitializer = () => {
const { state: playbackState } = usePlaybackState()
const { useDownload, useDownloadMultiple, downloadedTracks, networkStatus } =
useNetworkContext()
const { autoDownload } = useSettingsContext()
const autoDownload = useAutoDownloadContext()
const prefetchedTrackIds = useRef<Set<string>>(new Set())
/**

View File

@@ -45,9 +45,9 @@ export interface QueueMutation {
*/
export interface AddToQueueMutation {
/**
* The track to add to the queue.
* The tracks to add to the queue.
*/
track: BaseItemDto
tracks: BaseItemDto[]
/**
* The type of queuing to use, dictates the placement of the track in the queue,
* be it playing next, or playing in the queue later

View File

@@ -9,7 +9,7 @@ import JellifyTrack from '../../types/JellifyTrack'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { mapDtoToTrack } from '../../utils/mappings'
import { useNetworkContext } from '../Network'
import { useSettingsContext } from '../Settings'
import { useDownloadQualityContext, useStreamingQualityContext } from '../Settings'
import { QueuingType } from '../../enums/queuing-type'
import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-track-player'
import { findPlayQueueIndexStart } from './utils'
@@ -22,7 +22,7 @@ import { SKIP_TO_PREVIOUS_THRESHOLD } from '../../player/config'
import { isUndefined } from 'lodash'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '..'
import { networkStatusTypes } from '@/src/components/Network/internetConnectionWatcher'
import { networkStatusTypes } from '../../components/Network/internetConnectionWatcher'
import { ensureUpcomingTracksInQueue } from '../../player/helpers/gapless'
/**
@@ -166,7 +166,8 @@ const QueueContextInitailizer = () => {
//#region Context
const { api, sessionId } = useJellifyContext()
const { downloadedTracks, networkStatus } = useNetworkContext()
const { downloadQuality, streamingQuality } = useSettingsContext()
const downloadQuality = useDownloadQualityContext()
const streamingQuality = useStreamingQualityContext()
//#endregion Context
@@ -359,36 +360,38 @@ const QueueContextInitailizer = () => {
*
* @param item The track to play next
*/
const playNextInQueue = async (item: BaseItemDto) => {
const playNextInQueue = async (items: BaseItemDto[]) => {
console.debug(`Playing item next in queue`)
const playNextTrack = mapDtoToTrack(
api!,
sessionId,
item,
downloadedTracks ?? [],
QueuingType.PlayingNext,
downloadQuality,
streamingQuality,
const tracksToPlayNext = items.map((item) =>
mapDtoToTrack(
api!,
sessionId,
item,
downloadedTracks ?? [],
QueuingType.PlayingNext,
downloadQuality,
streamingQuality,
),
)
// Update app state first to prevent race conditions
const newQueue = [
...playQueue.slice(0, currentIndex + 1),
playNextTrack,
...tracksToPlayNext,
...playQueue.slice(currentIndex + 1),
]
setPlayQueue(newQueue)
// Then update RNTP
await TrackPlayer.add([playNextTrack], currentIndex + 1)
await TrackPlayer.add(tracksToPlayNext, currentIndex + 1)
const nowPlaying = playQueue[currentIndex]
// Add to the state unshuffled queue, using the currently playing track as the index
setUnshuffledQueue([
...unshuffledQueue.slice(0, unshuffledQueue.indexOf(nowPlaying) + 1),
playNextTrack,
...tracksToPlayNext,
...unshuffledQueue.slice(unshuffledQueue.indexOf(nowPlaying) + 1),
])
@@ -492,10 +495,10 @@ const QueueContextInitailizer = () => {
//#region Hooks
const useAddToQueue = useMutation({
mutationFn: ({ track, queuingType }: AddToQueueMutation) => {
mutationFn: ({ tracks, queuingType }: AddToQueueMutation) => {
return queuingType === QueuingType.PlayingNext
? playNextInQueue(track)
: playInQueue([track])
? playNextInQueue(tracks)
: playInQueue(tracks)
},
onSuccess: (data, { queuingType }) => {
trigger('notificationSuccess')

View File

@@ -1,7 +1,8 @@
import { Platform } from 'react-native'
import { storage } from '../../constants/storage'
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
import { createContext, useContext, useEffect, useState, useMemo } from 'react'
import { useEffect, useState, useMemo } from 'react'
import { createContext, useContextSelector } from 'use-context-selector'
export type DownloadQuality = 'original' | 'high' | 'medium' | 'low'
export type StreamingQuality = 'original' | 'high' | 'medium' | 'low'
@@ -156,4 +157,37 @@ export const SettingsProvider = ({ children }: { children: React.ReactNode }) =>
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>
}
export const useSettingsContext = () => useContext(SettingsContext)
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)

View File

@@ -1,21 +1,13 @@
import { RouteProp } from '@react-navigation/native'
import { Album } from '../../components/Album'
import { StackParamList } from '../../components/types'
import { AlbumProps } from '../types'
import { AlbumProvider } from '../../providers/Album'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
export default function AlbumScreen({
navigation,
route,
}: {
navigation: NativeStackNavigationProp<StackParamList, 'Album'>
route: RouteProp<StackParamList, 'Album'>
}): React.JSX.Element {
export default function AlbumScreen({ route }: AlbumProps): React.JSX.Element {
const { album } = route.params
return (
<AlbumProvider album={album}>
<Album route={route} navigation={navigation} />
<Album />
</AlbumProvider>
)
}

View File

@@ -1,6 +1,6 @@
import { RouteProp } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../components/types'
import { BaseStackParamList } from '../types'
import { ArtistProvider } from '../../providers/Artist'
import ArtistNavigation from '../../components/Artist'
@@ -8,14 +8,14 @@ export function ArtistScreen({
route,
navigation,
}: {
route: RouteProp<StackParamList, 'Artist'>
navigation: NativeStackNavigationProp<StackParamList>
route: RouteProp<BaseStackParamList, 'Artist'>
navigation: NativeStackNavigationProp<BaseStackParamList, 'Artist'>
}): React.JSX.Element {
const { artist } = route.params
return (
<ArtistProvider artist={artist}>
<ArtistNavigation navigation={navigation} />
<ArtistNavigation />
</ArtistProvider>
)
}

View File

@@ -1,6 +1,12 @@
import ItemContext from '../../components/Context'
import { ContextProps } from '../../components/types'
import { ContextProps } from '../types'
export default function ItemContextScreen({ route }: ContextProps): React.JSX.Element {
return <ItemContext item={route.params.item} />
return (
<ItemContext
item={route.params.item}
isNested={route.params.isNested}
navigation={route.params.navigation}
/>
)
}

View File

@@ -1,5 +1,5 @@
import { MultipleArtistsProps } from '../../components/types'
import MultipleArtists from '../../components/Context/components/multiple-artists'
import { MultipleArtistsProps } from '../Player/types'
export default function MultipleArtistsSheet(props: MultipleArtistsProps): React.JSX.Element {
return <MultipleArtists {...props} />

View File

@@ -1,21 +0,0 @@
import ItemDetail from '../../components/Detail/component'
import { StackParamList } from '../../components/types'
import { RouteProp } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import React from 'react'
export default function DetailsScreen({
route,
navigation,
}: {
route: RouteProp<StackParamList, 'Details'>
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
return (
<ItemDetail
item={route.params.item}
navigation={navigation}
isNested={route.params.isNested}
/>
)
}

View File

@@ -1,13 +1,14 @@
import { RouteProp } from '@react-navigation/native'
import Albums from '../../components/Albums/component'
import { RecentlyAddedProps } from '../../components/types'
import DiscoverStackParamList, { RecentlyAddedProps } from './types'
export default function RecentlyAdded({
route,
navigation,
}: RecentlyAddedProps): React.JSX.Element {
}: {
route: RouteProp<DiscoverStackParamList, 'RecentlyAdded'>
}): React.JSX.Element {
return (
<Albums
navigation={navigation}
albums={route.params.albums}
fetchNextPage={route.params.fetchNextPage}
hasNextPage={route.params.hasNextPage}

View File

@@ -1,10 +1,9 @@
import Artists from '../../components/Artists/component'
import { SuggestedArtistsProps } from '../../components/types'
import { SuggestedArtistsProps } from './types'
export default function SuggestedArtists({ navigation, route }: SuggestedArtistsProps) {
return (
<Artists
navigation={navigation}
artistsInfiniteQuery={route.params.artistsInfiniteQuery}
showAlphabeticalSelector={false}
/>

View File

@@ -1,18 +1,16 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { StackParamList } from '../../components/types'
import Index from '../../components/Discover/component'
import DetailsScreen from '../Detail'
import AlbumScreen from '../Album'
import { ArtistScreen } from '../Artist'
import { DiscoverProvider } from '../../providers/Discover'
import InstantMix from '../../components/InstantMix/component'
import { useTheme } from 'tamagui'
import RecentlyAdded from './albums'
import PublicPlaylists from './playlists'
import { PlaylistScreen } from '../Playlist'
import SuggestedArtists from './artists'
import DiscoverStackParamList from './types'
export const DiscoverStack = createNativeStackNavigator<StackParamList>()
export const DiscoverStack = createNativeStackNavigator<DiscoverStackParamList>()
export function Discover(): React.JSX.Element {
const theme = useTheme()
@@ -90,26 +88,6 @@ export function Discover(): React.JSX.Element {
title: 'Suggested Artists',
}}
/>
<DiscoverStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
title: route.params.item.Name
? `${route.params.item.Name} Mix`
: 'Instant Mix',
})}
/>
<DiscoverStack.Group screenOptions={{ presentation: 'modal' }}>
<DiscoverStack.Screen
name='Details'
component={DetailsScreen}
options={{
headerShown: false,
}}
/>
</DiscoverStack.Group>
</DiscoverStack.Navigator>
</DiscoverProvider>
)

View File

@@ -1,5 +1,5 @@
import Playlists from '../../components/Playlists/component'
import { PublicPlaylistsProps } from '../../components/types'
import { PublicPlaylistsProps } from './types'
export default function PublicPlaylists({
navigation,
@@ -7,7 +7,6 @@ export default function PublicPlaylists({
}: PublicPlaylistsProps): React.JSX.Element {
return (
<Playlists
navigation={navigation}
playlists={route.params.playlists}
fetchNextPage={route.params.fetchNextPage}
hasNextPage={route.params.hasNextPage}

38
src/screens/Discover/types.d.ts vendored Normal file
View File

@@ -0,0 +1,38 @@
import { BaseStackParamList } from '../types'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { UseInfiniteQueryResult } from '@tanstack/react-query'
type DiscoverStackParamList = BaseStackParamList & {
Discover: undefined
RecentlyAdded: {
albums: BaseItemDto[] | undefined
navigation: NativeStackNavigationProp<RootStackParamList>
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
}
PublicPlaylists: {
playlists: BaseItemDto[] | undefined
navigation: NativeStackNavigationProp<RootStackParamList>
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
isFetchingNextPage: boolean
refetch: () => void
}
SuggestedArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
navigation: NativeStackNavigationProp<RootStackParamList>
}
}
export default DiscoverStackParamList
export type RecentlyAddedProps = NativeStackScreenProps<DiscoverStackParamList, 'RecentlyAdded'>
export type PublicPlaylistsProps = NativeStackScreenProps<DiscoverStackParamList, 'PublicPlaylists'>
export type SuggestedArtistsProps = NativeStackScreenProps<
DiscoverStackParamList,
'SuggestedArtists'
>

View File

@@ -1,6 +1,6 @@
import React from 'react'
import Artists from '../../components/Artists/component'
import { MostPlayedArtistsProps, RecentArtistsProps } from '../../components/types'
import { MostPlayedArtistsProps, RecentArtistsProps } from './types'
import { useHomeContext } from '../../providers/Home'
export default function HomeArtistsScreen({
@@ -12,7 +12,6 @@ export default function HomeArtistsScreen({
if (route.name === 'MostPlayedArtists') {
return (
<Artists
navigation={navigation}
artistsInfiniteQuery={frequentArtistsInfiniteQuery}
showAlphabeticalSelector={false}
/>
@@ -21,7 +20,6 @@ export default function HomeArtistsScreen({
return (
<Artists
navigation={navigation}
artistsInfiniteQuery={recentArtistsInfiniteQuery}
showAlphabeticalSelector={false}
/>

View File

@@ -1,18 +1,17 @@
import _ from 'lodash'
import { HomeProvider } from '../../providers/Home'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { StackParamList } from '../../components/types'
import { PlaylistScreen } from '../Playlist'
import { ProvidedHome } from '../../components/Home'
import DetailsScreen from '../Detail'
import { ArtistScreen } from '../Artist'
import InstantMix from '../../components/InstantMix/component'
import { useTheme } from 'tamagui'
import HomeArtistsScreen from './artists'
import HomeTracksScreen from './tracks'
import AlbumScreen from '../Album'
import HomeStackParamList from './types'
import { HomeTabProps } from '../Tabs/types'
const HomeStack = createNativeStackNavigator<StackParamList>()
const HomeStack = createNativeStackNavigator<HomeStackParamList>()
/**
* The main screen for the home tab.
@@ -23,13 +22,10 @@ export default function Home(): React.JSX.Element {
return (
<HomeProvider>
<HomeStack.Navigator
initialRouteName='HomeScreen'
screenOptions={{ headerShown: true }}
>
<HomeStack.Navigator initialRouteName='Home' screenOptions={{ headerShown: true }}>
<HomeStack.Group>
<HomeStack.Screen
name='HomeScreen'
name='Home'
component={ProvidedHome}
options={{
title: 'Home',
@@ -94,26 +90,6 @@ export default function Home(): React.JSX.Element {
},
})}
/>
<HomeStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
title: route.params.item.Name
? `${route.params.item.Name} Mix`
: 'Instant Mix',
})}
/>
</HomeStack.Group>
<HomeStack.Group screenOptions={{ presentation: 'modal' }}>
<HomeStack.Screen
name='Details'
component={DetailsScreen}
options={{
headerShown: false,
}}
/>
</HomeStack.Group>
</HomeStack.Navigator>
</HomeProvider>

View File

@@ -1,6 +1,6 @@
import Tracks from '../../components/Tracks/component'
import { MostPlayedTracksProps, RecentTracksProps } from '../../components/types'
import { useHomeContext } from '../../providers/Home'
import { MostPlayedTracksProps, RecentTracksProps } from './types'
export default function HomeTracksScreen({
navigation,
@@ -18,7 +18,6 @@ export default function HomeTracksScreen({
if (route.name === 'MostPlayedTracks') {
return (
<Tracks
navigation={navigation}
tracks={frequentlyPlayed}
fetchNextPage={fetchNextFrequentlyPlayed}
hasNextPage={hasNextFrequentlyPlayed}
@@ -29,7 +28,6 @@ export default function HomeTracksScreen({
return (
<Tracks
navigation={navigation}
tracks={recentTracks}
fetchNextPage={fetchNextRecentTracks}
hasNextPage={hasNextRecentTracks}

32
src/screens/Home/types.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
import { BaseStackParamList } from '../types'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { UseInfiniteQueryResult } from '@tanstack/react-query'
type HomeStackParamList = BaseStackParamList & {
RecentArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
MostPlayedArtists: {
artistsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
RecentTracks: {
tracks: BaseItemDto[] | undefined
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
}
MostPlayedTracks: {
tracks: BaseItemDto[] | undefined
fetchNextPage: () => void
hasNextPage: boolean
isPending: boolean
}
}
export default HomeStackParamList
export type RecentArtistsProps = NativeStackScreenProps<HomeStackParamList, 'RecentArtists'>
export type RecentTracksProps = NativeStackScreenProps<HomeStackParamList, 'RecentTracks'>
export type MostPlayedArtistsProps = NativeStackScreenProps<HomeStackParamList, 'MostPlayedArtists'>
export type MostPlayedTracksProps = NativeStackScreenProps<HomeStackParamList, 'MostPlayedTracks'>

View File

@@ -4,7 +4,7 @@ import React, { useState } from 'react'
import { View, XStack } from 'tamagui'
import Button from '../../components/Global/helpers/button'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../../components/types'
import { RootStackParamList } from '../types'
import { useMutation } from '@tanstack/react-query'
import { createPlaylist } from '../../api/mutations/playlists'
import { trigger } from 'react-native-haptic-feedback'
@@ -13,13 +13,14 @@ import { QueryKeys } from '../../enums/query-keys'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '../../providers'
import Icon from '../../components/Global/components/icon'
import LibraryStackParamList from './types'
export default function AddPlaylist({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList, 'AddPlaylist'>
navigation: NativeStackNavigationProp<LibraryStackParamList, 'AddPlaylist'>
}): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const { api, user } = useJellifyContext()
const [name, setName] = useState<string>('')
const useAddPlaylist = useMutation({

View File

@@ -1,5 +1,4 @@
import { View, XStack } from 'tamagui'
import { DeletePlaylistProps } from '../../components/types'
import Button from '../../components/Global/helpers/button'
import { H5, Text } from '../../components/Global/helpers/text'
import { useMutation } from '@tanstack/react-query'
@@ -10,12 +9,13 @@ import { queryClient } from '../../constants/query-client'
import { QueryKeys } from '../../enums/query-keys'
import { useJellifyContext } from '../../providers'
import Icon from '../../components/Global/components/icon'
import { LibraryDeletePlaylistProps } from './types'
// import * as Burnt from 'burnt'
export default function DeletePlaylist({
navigation,
route,
}: DeletePlaylistProps): React.JSX.Element {
}: LibraryDeletePlaylistProps): React.JSX.Element {
const { api, user, library } = useJellifyContext()
const useDeletePlaylist = useMutation({
mutationFn: (playlist: BaseItemDto) => deletePlaylist(api, playlist.Id!),

View File

@@ -1,19 +1,17 @@
import React from 'react'
import { StackParamList } from '../../components/types'
import Library from '../../components/Library/component'
import { PlaylistScreen } from '../Playlist'
import DetailsScreen from '../Detail'
import AddPlaylist from './add-playlist'
import DeletePlaylist from './delete-playlist'
import { ArtistScreen } from '../Artist'
import InstantMix from '../../components/InstantMix/component'
import { useTheme } from 'tamagui'
import { LibraryProvider } from '../../providers/Library'
import { LibrarySortAndFilterProvider } from '../../providers/Library/sorting-filtering'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import AlbumScreen from '../Album'
import LibraryStackParamList from './types'
const Stack = createNativeStackNavigator<StackParamList>()
const Stack = createNativeStackNavigator<LibraryStackParamList>()
export default function LibraryStack(): React.JSX.Element {
const theme = useTheme()
@@ -21,9 +19,9 @@ export default function LibraryStack(): React.JSX.Element {
return (
<LibrarySortAndFilterProvider>
<LibraryProvider>
<Stack.Navigator initialRouteName='LibraryScreen'>
<Stack.Navigator initialRouteName='Library'>
<Stack.Screen
name='LibraryScreen'
name='Library'
component={Library}
options={{
title: 'Library',
@@ -70,26 +68,6 @@ export default function LibraryStack(): React.JSX.Element {
})}
/>
<Stack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
title: route.params.item.Name
? `${route.params.item.Name} Mix`
: 'Instant Mix',
})}
/>
<Stack.Group screenOptions={{ presentation: 'modal' }}>
<Stack.Screen
name='Details'
component={DetailsScreen}
options={{
headerShown: false,
}}
/>
</Stack.Group>
<Stack.Group
screenOptions={{
presentation: 'formSheet',

22
src/screens/Library/types.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { BaseStackParamList } from '../types'
type LibraryStackParamList = BaseStackParamList & {
AddPlaylist: undefined
DeletePlaylist: {
playlist: BaseItemDto
}
}
export default LibraryStackParamList
export type LibraryArtistProps = NativeStackScreenProps<LibraryStackParamList, 'Artist'>
export type LibraryAlbumProps = NativeStackScreenProps<LibraryStackParamList, 'Album'>
export type LibraryAddPlaylistProps = NativeStackScreenProps<LibraryStackParamList, 'AddPlaylist'>
export type LibraryDeletePlaylistProps = NativeStackScreenProps<
LibraryStackParamList,
'DeletePlaylist'
>

View File

@@ -9,10 +9,10 @@ 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 '../../components/types'
import { RootStackParamList } from '../types'
import Toast from 'react-native-toast-message'
import { useJellifyContext } from '../../providers'
import { useSettingsContext } from '../../providers/Settings'
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'
@@ -22,7 +22,7 @@ import { sleepify } from '../../utils/sleep'
export default function ServerAddress({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const [serverAddressContainsProtocol, setServerAddressContainsProtocol] =
useState<boolean>(false)
@@ -33,7 +33,8 @@ export default function ServerAddress({
const { server, setServer, signOut } = useJellifyContext()
const { setSendMetrics, sendMetrics } = useSettingsContext()
const sendMetrics = useSendMetricsContext()
const setSendMetrics = useSetSendMetricsContext()
useEffect(() => {
setServerAddressContainsProtocol(

View File

@@ -7,7 +7,7 @@ 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 '../../components/types'
import { RootStackParamList } from '../types'
import Input from '../../components/Global/helpers/input'
import Icon from '../../components/Global/components/icon'
import { useJellifyContext } from '../../providers'
@@ -18,7 +18,7 @@ import { IS_MAESTRO_BUILD } from '../../configs/config'
export default function ServerAuthentication({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
navigation: NativeStackNavigationProp<RootStackParamList>
}): React.JSX.Element {
const { api } = useJellifyContext()
const [username, setUsername] = useState<string | undefined>(undefined)

Some files were not shown because too many files have changed in this diff Show More