mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-21 05:20:06 -06:00
A-Z Selector for Artists, Prep for the same for Albums, Style Fixes, Verbiage Adjustments (#373)
Lots of overall improvements to styling, color usage, and wording Adds more iconography, adjust settings tab to hide "Labs", enabled via 5 taps on "Made with <3" message Adds flashlist, for faster lists. Lots of work on the library to add an alphabetical selector for skipping to a particular spot in the list quickly. Will be iterated on as we build out more ways to sort and view your library! adds additional dns fixes for connecting to a local domain
This commit is contained in:
30
README.md
30
README.md
@@ -17,12 +17,13 @@
|
||||
## 📄 Contents
|
||||
|
||||
- [Info](#ℹ️-info)
|
||||
- [Download](#%EF%B8%8F-download)
|
||||
- [Downloading](#️-downloading)
|
||||
- [Screenshots](#-screenshots)
|
||||
- [Features](#-features)
|
||||
- [Built with](#-built-with-good-stuff)
|
||||
- [Support](#-support-the-project)
|
||||
- [Running Locally](#️running-locally)
|
||||
- [Contributing](#-contributing)
|
||||
- [Special Thanks](#-special-thanks-to)
|
||||
|
||||
|
||||
@@ -41,16 +42,28 @@ Showcasing the artwork of your library, it has a user interface congruent to wha
|
||||
|
||||
This app was designed with me and my dad in mind. I wanted us to have a sleek, one stop shop for live recordings of bands we like (read: the Grateful Dead). The UI was designed so that we'd find it instantly familiar and useful. CarPlay / Android Auto support was also a must for us, as we both use CarPlay religiously.
|
||||
|
||||
## ⬇️ Download
|
||||
## ⬇️ Downloading
|
||||
|
||||
### Android
|
||||
|
||||
Head to [releases](https://github.com/Jellify-Music/App/releases) to download the required APK directly.
|
||||
Head to [releases](https://github.com/Jellify-Music/App/releases) to download the required .APK directly.
|
||||
|
||||
Also there is [obtanium](https://github.com/ImranR98/Obtainium) to which you can add Jellify as a repo to use the above releases as a repository.
|
||||
|
||||
For Obtanium, click "Add App", put "https://github.com/Jellify-Music/App" as the source URL, and on the next screen toggle "prereleases". You'll now be easily able to keep your local copy in sync with new releases.
|
||||
|
||||
### iOS
|
||||
|
||||
#### The TestFlight Way
|
||||
|
||||
Join the [TestFlight](https://testflight.apple.com/join/etVSc7ZQ) and install the latest version from there
|
||||
|
||||
#### The Sideloading Way
|
||||
|
||||
Head to [releases](https://github.com/Jellify-Music/App/releases) to download the required .IPA directly.
|
||||
|
||||
Install via [Altstore](https://altstore.io) or your favorite sideloading utility
|
||||
|
||||
|
||||
## 📱 Screenshots
|
||||
|
||||
@@ -284,6 +297,17 @@ This allows me to prioritize specific features, acquire additional hardware for
|
||||
- [ANDROID_HOME not being set](https://stackoverflow.com/questions/26356359/error-android-home-is-not-set-and-android-command-not-in-your-path-you-must/54888107#54888107)
|
||||
- [Android Auto app not showing up](https://www.reddit.com/r/AndroidAuto/s/LGYHoSPdXm)
|
||||
|
||||
## 👩💻 Contributing
|
||||
|
||||
We are open to any developer that wants to lend their hand at _Jellify_ development! Here's the best way to get started
|
||||
|
||||
- Fork this repository
|
||||
- Follow the instructions for [Running Locally](#️running-locally)
|
||||
- Hack, hack, hack
|
||||
- ???
|
||||
- Submit a Pull Request to sync the main repository with your fork!
|
||||
- Profit 🎉
|
||||
|
||||
## 🙏 Special Thanks To
|
||||
|
||||
- The [Jellyfin Team](https://jellyfin.org/) for making this possible with their software, SDKs, and unequivocal helpfulness.
|
||||
|
||||
@@ -1903,6 +1903,30 @@ PODS:
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
- SDWebImageWebPCoder (~> 0.8.4)
|
||||
- RNFlashList (1.8.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-hermes
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.25.0):
|
||||
@@ -2275,6 +2299,7 @@ DEPENDENCIES:
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
- RNDnsLookup (from `../node_modules/react-native-dns-lookup`)
|
||||
- RNFastImage (from `../node_modules/react-native-fast-image`)
|
||||
- "RNFlashList (from `../node_modules/@shopify/flash-list`)"
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
|
||||
@@ -2462,6 +2487,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-dns-lookup"
|
||||
RNFastImage:
|
||||
:path: "../node_modules/react-native-fast-image"
|
||||
RNFlashList:
|
||||
:path: "../node_modules/@shopify/flash-list"
|
||||
RNFS:
|
||||
:path: "../node_modules/react-native-fs"
|
||||
RNGestureHandler:
|
||||
@@ -2564,6 +2591,7 @@ SPEC CHECKSUMS:
|
||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
||||
RNDnsLookup: db4a89381b80ec1a5153088518d2c4f8e51f2521
|
||||
RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
|
||||
RNFlashList: 5001dd06f0003a497de3e2035653c54cf8b48e96
|
||||
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
||||
RNGestureHandler: ebef699ea17e7c0006c1074e1e423ead60ce0121
|
||||
RNReactNativeHapticFeedback: 851adf794e1fcdc0664d80820fa3272ee8a6a538
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"@react-navigation/native-stack": "^7.3.13",
|
||||
"@react-navigation/stack": "^7.3.2",
|
||||
"@sentry/react-native": "^6.13.1",
|
||||
"@shopify/flash-list": "^1.8.0",
|
||||
"@tamagui/config": "^1.126.12",
|
||||
"@tanstack/query-sync-storage-persister": "^5.76.0",
|
||||
"@tanstack/react-query": "^5.76.0",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import QueryConfig from './query.config'
|
||||
import {
|
||||
BaseItemDto,
|
||||
BaseItemKind,
|
||||
@@ -13,11 +11,11 @@ import { fetchItems } from './item'
|
||||
export function fetchAlbums(
|
||||
api: Api | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
page: number,
|
||||
page: string,
|
||||
isFavorite: boolean = false,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
): Promise<BaseItemDto[]> {
|
||||
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
||||
console.debug('Fetching albums', page)
|
||||
|
||||
return fetchItems(api, library, [BaseItemKind.MusicAlbum], page, sortBy, sortOrder, isFavorite)
|
||||
|
||||
@@ -12,11 +12,11 @@ import { fetchItems } from './item'
|
||||
export function fetchArtists(
|
||||
api: Api | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
page: number,
|
||||
page: string | number,
|
||||
isFavorite: boolean,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
): Promise<BaseItemDto[]> {
|
||||
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
||||
console.debug('Fetching artists', page)
|
||||
return fetchItems(api, library, [BaseItemKind.MusicArtist], page, sortBy, sortOrder, isFavorite)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { SectionList } from 'react-native'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
import { JellifyLibrary } from '../../types/JellifyLibrary'
|
||||
import QueryConfig from './query.config'
|
||||
import { alphabet } from '../../providers/Library'
|
||||
|
||||
/**
|
||||
* Fetches a single Jellyfin item by it's ID
|
||||
@@ -51,12 +52,12 @@ export async function fetchItems(
|
||||
api: Api | undefined,
|
||||
library: JellifyLibrary | undefined,
|
||||
types: BaseItemKind[],
|
||||
page: number = 0,
|
||||
page: string | number = 0,
|
||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||
isFavorite: boolean,
|
||||
parentId?: string | undefined,
|
||||
): Promise<BaseItemDto[]> {
|
||||
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
||||
console.debug('Fetching items', page)
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isUndefined(api)) return reject('Client not initialized')
|
||||
@@ -69,12 +70,14 @@ export async function fetchItems(
|
||||
sortBy,
|
||||
recursive: true,
|
||||
sortOrder,
|
||||
startIndex: page * QueryConfig.limits.library,
|
||||
startIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
|
||||
limit: QueryConfig.limits.library,
|
||||
nameStartsWith: typeof page === 'string' && page !== alphabet[0] ? page : undefined,
|
||||
nameLessThan: typeof page === 'string' && page === alphabet[0] ? 'A' : undefined,
|
||||
isFavorite,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
resolve(data.Items ?? [])
|
||||
resolve({ title: page, data: data.Items ?? [] })
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
|
||||
@@ -11,7 +11,7 @@ const QueryConfig = {
|
||||
* The number of items to fetch for the library, set to 30
|
||||
* This is used for the artists, albums, and tracks tabs in the library
|
||||
*/
|
||||
library: 30,
|
||||
library: 75,
|
||||
|
||||
/**
|
||||
* The number of items to fetch for the instant mix, set to 50
|
||||
|
||||
@@ -1,43 +1,90 @@
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { FlatList } from 'react-native'
|
||||
import { ActivityIndicator, FlatList, RefreshControl } from 'react-native'
|
||||
import { AlbumsProps } from '../types'
|
||||
import { useDisplayContext } from '../../providers/Display/display-provider'
|
||||
import { getTokens } from 'tamagui'
|
||||
import { getToken, getTokens, XStack, YStack } from 'tamagui'
|
||||
import Item from '../Global/components/item'
|
||||
import React from 'react'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
|
||||
export default function Albums({
|
||||
albums,
|
||||
navigation,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isPending,
|
||||
isFetchingNextPage,
|
||||
showAlphabeticalSelector,
|
||||
}: AlbumsProps): React.JSX.Element {
|
||||
const { numberOfColumns } = useDisplayContext()
|
||||
|
||||
const MemoizedItem = React.memo(Item)
|
||||
|
||||
const itemHeight = getToken('$6')
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
marginVertical: getTokens().size.$1.val,
|
||||
}}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
numColumns={numberOfColumns}
|
||||
data={albums?.pages.flatMap((page) => page) ?? []}
|
||||
renderItem={({ index, item: album }) => (
|
||||
<ItemCard
|
||||
item={album}
|
||||
caption={album.Name ?? 'Untitled Album'}
|
||||
subCaption={album.ProductionYear?.toString() ?? ''}
|
||||
squared
|
||||
onPress={() => {
|
||||
navigation.navigate('Album', { album })
|
||||
}}
|
||||
size={'$11'}
|
||||
/>
|
||||
)}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage) fetchNextPage()
|
||||
}}
|
||||
onEndReachedThreshold={0.25}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
<XStack flex={1}>
|
||||
<FlashList
|
||||
contentContainerStyle={{
|
||||
paddingTop: getToken('$1'),
|
||||
}}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={albums ?? []}
|
||||
renderItem={({ index, item: album }) =>
|
||||
typeof album === 'string' ? (
|
||||
<XStack
|
||||
padding={'$2'}
|
||||
backgroundColor={'$background'}
|
||||
borderRadius={'$5'}
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
margin={'$2'}
|
||||
>
|
||||
<Text>{album.toUpperCase()}</Text>
|
||||
</XStack>
|
||||
) : typeof album === 'number' ? null : typeof album === 'object' ? (
|
||||
<MemoizedItem
|
||||
item={album}
|
||||
queueName={album.Name ?? 'Unknown Album'}
|
||||
navigation={navigation}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
isPending ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<YStack justifyContent='center'>
|
||||
<Text>No albums</Text>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage) fetchNextPage()
|
||||
}}
|
||||
ListFooterComponent={isPending ? <ActivityIndicator /> : null}
|
||||
refreshControl={<RefreshControl refreshing={isPending} />}
|
||||
stickyHeaderIndices={
|
||||
showAlphabeticalSelector
|
||||
? albums
|
||||
?.map((album, index, albums) =>
|
||||
typeof album === 'string' ? index : 0,
|
||||
)
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
: []
|
||||
}
|
||||
keyExtractor={(item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: typeof item === 'number'
|
||||
? item.toString()
|
||||
: item.Id!
|
||||
}
|
||||
estimatedItemSize={itemHeight}
|
||||
onEndReachedThreshold={0.25}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import { getTokens, YStack } from 'tamagui'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { getToken, getTokens, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { ActivityIndicator, FlatList } from 'react-native'
|
||||
import { useDisplayContext } from '../../providers/Display/display-provider'
|
||||
import { StackParamList } from '../types'
|
||||
import { ActivityIndicator, RefreshControl } from 'react-native'
|
||||
import { ArtistsProps } from '../types'
|
||||
import Item from '../Global/components/item'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { alphabet } from '../../providers/Library'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { useLibraryContext } from '../../providers/Library'
|
||||
import { sleepify } from '../../helpers/sleep'
|
||||
|
||||
export default function Artists({
|
||||
artists,
|
||||
@@ -13,47 +18,160 @@ export default function Artists({
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isPending,
|
||||
isFetchingNextPage,
|
||||
showAlphabeticalSelector,
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
const { numberOfColumns } = useDisplayContext()
|
||||
const { width, height } = useSafeAreaFrame()
|
||||
|
||||
const { artistPageParams } = useLibraryContext()
|
||||
|
||||
const memoizedAlphabet = useMemo(() => alphabet, [])
|
||||
|
||||
const sectionListRef = useRef<FlashList<string | number | BaseItemDto>>(null)
|
||||
|
||||
const itemHeight = getToken('$6')
|
||||
|
||||
const MemoizedItem = React.memo(Item)
|
||||
|
||||
const artistsRef = useRef<(string | number | BaseItemDto)[]>(artists ?? [])
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const alphabeticalSelectorCallback = useCallback(async (letter: string) => {
|
||||
do {
|
||||
await sleepify(100)
|
||||
fetchNextPage()
|
||||
console.debug(
|
||||
`Alphabetical Selector Callback: ${letter}, ${artistPageParams.current.join(', ')}`,
|
||||
)
|
||||
} while (
|
||||
artistsRef.current?.indexOf(letter) === -1 ||
|
||||
!artistPageParams.current.includes(letter)
|
||||
)
|
||||
|
||||
sleepify(250).then(() => {
|
||||
sectionListRef.current?.scrollToIndex({
|
||||
index:
|
||||
(artistsRef.current?.indexOf(letter) ?? 0) > -1
|
||||
? artistsRef.current!.indexOf(letter)
|
||||
: 0,
|
||||
viewPosition: 0.2,
|
||||
animated: true,
|
||||
})
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.debug(hasNextPage)
|
||||
}, [hasNextPage])
|
||||
console.debug(`Fetching Artist Component: ${artistPageParams.current.join(', ')}`)
|
||||
}, [artistPageParams])
|
||||
|
||||
useEffect(() => {
|
||||
artistsRef.current = artists ?? []
|
||||
}, [artists])
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
alignItems: 'center',
|
||||
marginVertical: getTokens().size.$1.val,
|
||||
}}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
numColumns={numberOfColumns}
|
||||
data={artists?.pages.flatMap((page) => page) ?? []}
|
||||
renderItem={({ index, item: artist }) => (
|
||||
<ItemCard
|
||||
item={artist}
|
||||
caption={artist.Name ?? 'Unknown Artist'}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', { artist })
|
||||
}}
|
||||
size={'$11'}
|
||||
/>
|
||||
<XStack flex={1}>
|
||||
<FlashList
|
||||
ref={sectionListRef}
|
||||
style={{
|
||||
width: getToken('$10'),
|
||||
marginRight: getToken('$4'),
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingTop: getToken('$1'),
|
||||
}}
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
keyExtractor={(item) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
: typeof item === 'number'
|
||||
? item.toString()
|
||||
: item.Id!
|
||||
}
|
||||
estimatedItemSize={itemHeight}
|
||||
data={artists}
|
||||
refreshControl={<RefreshControl refreshing={isPending} />}
|
||||
renderItem={({ index, item: artist }) =>
|
||||
typeof artist === 'string' ? (
|
||||
<XStack
|
||||
padding={'$2'}
|
||||
backgroundColor={'$background'}
|
||||
borderRadius={'$5'}
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
margin={'$2'}
|
||||
>
|
||||
<Text>{artist.toUpperCase()}</Text>
|
||||
</XStack>
|
||||
) : typeof artist === 'number' ? null : typeof artist === 'object' ? (
|
||||
<MemoizedItem
|
||||
item={artist}
|
||||
queueName={artist.Name ?? 'Unknown Artist'}
|
||||
navigation={navigation}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
isPending || isFetchingNextPage ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<YStack justifyContent='center'>
|
||||
<Text>No artists</Text>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
ListFooterComponent={isPending ? <ActivityIndicator /> : null}
|
||||
stickyHeaderIndices={
|
||||
showAlphabeticalSelector
|
||||
? artists
|
||||
?.map((artist, index, artists) =>
|
||||
typeof artist === 'string' ? index : 0,
|
||||
)
|
||||
.filter((value, index, indices) => indices.indexOf(value) === index)
|
||||
: []
|
||||
}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage) fetchNextPage()
|
||||
}}
|
||||
onEndReachedThreshold={0.8}
|
||||
removeClippedSubviews={false}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && (
|
||||
<YStack
|
||||
maxWidth={'$4'}
|
||||
margin={'$2'}
|
||||
minWidth={'$2'}
|
||||
width={width / 8}
|
||||
height={height - getTokens().size.$15.val}
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
borderRadius={'$5'}
|
||||
gap={0}
|
||||
flex={1}
|
||||
>
|
||||
{memoizedAlphabet.map((letter) => (
|
||||
<Text
|
||||
display='flex'
|
||||
paddingHorizontal={'$3'}
|
||||
flex={1}
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
key={letter}
|
||||
bold
|
||||
fontSize={'$6'}
|
||||
onPressOut={() => {
|
||||
trigger('impactLight')
|
||||
alphabeticalSelectorCallback(letter)
|
||||
}}
|
||||
>
|
||||
{letter.toUpperCase()}
|
||||
</Text>
|
||||
))}
|
||||
</YStack>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
isPending ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<YStack justifyContent='center'>
|
||||
<Text>No artists</Text>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
onEndReached={() => {
|
||||
if (hasNextPage) fetchNextPage()
|
||||
}}
|
||||
onEndReachedThreshold={0.25}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ export default function ArtistsScreen({
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isPending,
|
||||
isFetchingNextPage,
|
||||
showAlphabeticalSelector,
|
||||
}: ArtistsProps): React.JSX.Element {
|
||||
return (
|
||||
<Artists
|
||||
@@ -16,6 +18,8 @@ export default function ArtistsScreen({
|
||||
fetchNextPage={fetchNextPage}
|
||||
hasNextPage={hasNextPage}
|
||||
isPending={isPending}
|
||||
isFetchingNextPage={isFetchingNextPage}
|
||||
showAlphabeticalSelector={showAlphabeticalSelector}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,13 @@ export default function RecentlyAdded({
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { recentlyAdded, fetchNextRecentlyAdded, hasNextRecentlyAdded, isPendingRecentlyAdded } =
|
||||
useDiscoverContext()
|
||||
const {
|
||||
recentlyAdded,
|
||||
fetchNextRecentlyAdded,
|
||||
hasNextRecentlyAdded,
|
||||
isPendingRecentlyAdded,
|
||||
isFetchingNextRecentlyAdded,
|
||||
} = useDiscoverContext()
|
||||
|
||||
return (
|
||||
<View>
|
||||
@@ -26,6 +31,7 @@ export default function RecentlyAdded({
|
||||
fetchNextPage: fetchNextRecentlyAdded,
|
||||
hasNextPage: hasNextRecentlyAdded,
|
||||
isPending: isPendingRecentlyAdded,
|
||||
isFetchingNextPage: isFetchingNextRecentlyAdded,
|
||||
})
|
||||
}}
|
||||
>
|
||||
@@ -34,11 +40,7 @@ export default function RecentlyAdded({
|
||||
</XStack>
|
||||
|
||||
<HorizontalCardList
|
||||
data={
|
||||
(recentlyAdded?.pages[0].length ?? 0 > 10)
|
||||
? recentlyAdded!.pages[0].slice(0, 10)
|
||||
: recentlyAdded?.pages[0]
|
||||
}
|
||||
data={recentlyAdded?.slice(0, 10) ?? []}
|
||||
renderItem={({ item }) => (
|
||||
<ItemCard
|
||||
caption={item.Name}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import ItemImage from './image'
|
||||
import FavoriteIcon from './favorite-icon'
|
||||
|
||||
export default function Item({
|
||||
item,
|
||||
@@ -26,13 +27,13 @@ export default function Item({
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
return (
|
||||
<View flex={1}>
|
||||
<View>
|
||||
<Separator />
|
||||
|
||||
<XStack
|
||||
alignContent='center'
|
||||
flex={1}
|
||||
minHeight={width / 9}
|
||||
minHeight={'$7'}
|
||||
width={'100%'}
|
||||
onPress={() => {
|
||||
switch (item.Type) {
|
||||
case 'MusicArtist': {
|
||||
@@ -73,18 +74,13 @@ export default function Item({
|
||||
})
|
||||
}}
|
||||
paddingVertical={'$2'}
|
||||
marginHorizontal={'$1'}
|
||||
paddingRight={'$2'}
|
||||
>
|
||||
<YStack flex={1}>
|
||||
<YStack marginHorizontal={'$3'} justifyContent='center'>
|
||||
<ItemImage item={item} height={'$12'} width={'$12'} />
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
marginLeft={'$1'}
|
||||
alignContent='center'
|
||||
justifyContent='flex-start'
|
||||
flex={3}
|
||||
>
|
||||
<YStack alignContent='center' justifyContent='center' flex={4}>
|
||||
<Text bold lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{item.Name ?? ''}
|
||||
</Text>
|
||||
@@ -92,26 +88,26 @@ export default function Item({
|
||||
<Text
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
color={'$amethyst'}
|
||||
color={'$borderColor'}
|
||||
bold
|
||||
>
|
||||
{item.AlbumArtist ?? 'Untitled Artist'}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{item.Type === 'MusicAlbum' && <RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>}
|
||||
</YStack>
|
||||
|
||||
<XStack justifyContent='space-between' alignItems='center' flex={2}>
|
||||
{item.UserData?.IsFavorite ? (
|
||||
<Icon small color={'$primary'} name='heart' />
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
<XStack
|
||||
justifyContent='flex-end'
|
||||
alignItems='center'
|
||||
flex={item.Type === 'Audio' ? 2 : 1}
|
||||
>
|
||||
<FavoriteIcon item={item} />
|
||||
{/* Runtime ticks for Songs */}
|
||||
{item.Type === 'Audio' ? (
|
||||
<RunTimeTicks>{item.RunTimeTicks}</RunTimeTicks>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
|
||||
<Icon
|
||||
@@ -123,9 +119,7 @@ export default function Item({
|
||||
})
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
) : null}
|
||||
</XStack>
|
||||
</XStack>
|
||||
</View>
|
||||
|
||||
@@ -39,11 +39,7 @@ export default function FrequentArtists({
|
||||
</XStack>
|
||||
|
||||
<HorizontalCardList
|
||||
data={
|
||||
(frequentArtists?.pages.flatMap((page) => page).length ?? 0 > 10)
|
||||
? frequentArtists?.pages.flatMap((page) => page).slice(0, 10)
|
||||
: frequentArtists?.pages.flatMap((page) => page)
|
||||
}
|
||||
data={frequentArtists?.slice(0, 10) ?? []}
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemCard
|
||||
item={artist}
|
||||
|
||||
@@ -34,11 +34,7 @@ export default function RecentArtists({
|
||||
</XStack>
|
||||
|
||||
<HorizontalCardList
|
||||
data={
|
||||
(recentArtists?.pages.flatMap((page) => page).length ?? 0 > 10)
|
||||
? recentArtists?.pages.flatMap((page) => page).slice(0, 10)
|
||||
: recentArtists?.pages.flatMap((page) => page)
|
||||
}
|
||||
data={recentArtists?.slice(0, 10) ?? []}
|
||||
renderItem={({ item: recentArtist }) => (
|
||||
<ItemCard
|
||||
item={recentArtist}
|
||||
|
||||
@@ -5,7 +5,13 @@ import { useLibraryContext } from '../../../providers/Library'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
export default function AlbumsTab(): React.JSX.Element {
|
||||
const { albums, fetchNextAlbumsPage, hasNextAlbumsPage, isPendingAlbums } = useLibraryContext()
|
||||
const {
|
||||
albums,
|
||||
fetchNextAlbumsPage,
|
||||
hasNextAlbumsPage,
|
||||
isPendingAlbums,
|
||||
isFetchingNextAlbumsPage,
|
||||
} = useLibraryContext()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
|
||||
|
||||
@@ -16,6 +22,8 @@ export default function AlbumsTab(): React.JSX.Element {
|
||||
fetchNextPage={fetchNextAlbumsPage}
|
||||
hasNextPage={hasNextAlbumsPage}
|
||||
isPending={isPendingAlbums}
|
||||
isFetchingNextPage={isFetchingNextAlbumsPage}
|
||||
showAlphabeticalSelector={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,13 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../types'
|
||||
|
||||
export default function ArtistsTab(): React.JSX.Element {
|
||||
const { artists, fetchNextArtistsPage, hasNextArtistsPage, isPendingArtists } =
|
||||
useLibraryContext()
|
||||
const {
|
||||
artists,
|
||||
isPendingArtists,
|
||||
fetchNextArtistsPage,
|
||||
hasNextArtistsPage,
|
||||
isFetchingNextArtistsPage,
|
||||
} = useLibraryContext()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
|
||||
|
||||
@@ -17,6 +22,8 @@ export default function ArtistsTab(): React.JSX.Element {
|
||||
navigation={navigation}
|
||||
fetchNextPage={fetchNextArtistsPage}
|
||||
hasNextPage={hasNextArtistsPage}
|
||||
isFetchingNextPage={isFetchingNextArtistsPage}
|
||||
showAlphabeticalSelector={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MaterialTopTabBar, MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Separator, XStack, YStack } from 'tamagui'
|
||||
import { Separator, Spacer, XStack, YStack } from 'tamagui'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { useLibrarySortAndFilterContext } from '../../providers/Library/sorting-filtering'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
@@ -25,19 +25,19 @@ export default function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
<YStack>
|
||||
<MaterialTopTabBar {...props} />
|
||||
|
||||
<XStack
|
||||
paddingHorizontal={'$4'}
|
||||
paddingVertical={'$3'}
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$purpleGray'}
|
||||
marginTop={'$2'}
|
||||
marginHorizontal={'$2'}
|
||||
borderRadius={'$4'}
|
||||
backgroundColor={'$background'}
|
||||
alignItems={'center'}
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
{props.state.routes[props.state.index].name === 'Artists' ? null : (
|
||||
<XStack
|
||||
paddingHorizontal={'$4'}
|
||||
paddingVertical={'$2'}
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
marginTop={'$2'}
|
||||
marginHorizontal={'$2'}
|
||||
borderRadius={'$4'}
|
||||
backgroundColor={'$background'}
|
||||
alignItems={'center'}
|
||||
justifyContent='flex-end'
|
||||
>
|
||||
{props.state.routes[props.state.index].name === 'Playlists' ? (
|
||||
<XStack
|
||||
flex={1}
|
||||
@@ -66,48 +66,26 @@ export default function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
</Text>
|
||||
</XStack>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{props.state.routes[props.state.index].name === 'Tracks' && (
|
||||
<XStack
|
||||
flex={1}
|
||||
onPress={() => setIsDownloaded(!isDownloaded)}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Icon
|
||||
name={isDownloaded ? 'download' : 'download-outline'}
|
||||
color={isDownloaded ? '$success' : '$borderColor'}
|
||||
/>
|
||||
{props.state.routes[props.state.index].name === 'Tracks' && (
|
||||
<XStack
|
||||
flex={1}
|
||||
onPress={() => setIsDownloaded(!isDownloaded)}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Icon
|
||||
name={isDownloaded ? 'download' : 'download-outline'}
|
||||
color={isDownloaded ? '$success' : '$borderColor'}
|
||||
/>
|
||||
|
||||
<Text color={isDownloaded ? '$success' : '$borderColor'}>
|
||||
{isDownloaded ? 'Downloaded' : 'All'}
|
||||
</Text>
|
||||
</XStack>
|
||||
)}
|
||||
|
||||
<Separator vertical />
|
||||
|
||||
<XStack
|
||||
flex={1}
|
||||
onPress={() => setSortDescending(!sortDescending)}
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
sortDescending
|
||||
? 'sort-alphabetical-descending'
|
||||
: 'sort-alphabetical-ascending'
|
||||
}
|
||||
color={sortDescending ? '$success' : '$borderColor'}
|
||||
/>
|
||||
|
||||
<Text color={sortDescending ? '$success' : '$borderColor'}>
|
||||
{sortDescending ? 'Descending' : 'Ascending'}
|
||||
</Text>
|
||||
<Text color={isDownloaded ? '$success' : '$borderColor'}>
|
||||
{isDownloaded ? 'Downloaded' : 'All'}
|
||||
</Text>
|
||||
</XStack>
|
||||
)}
|
||||
</XStack>
|
||||
</XStack>
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ export default function ServerAddress({
|
||||
const api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`)
|
||||
|
||||
const connectViaHostnamePromise = () =>
|
||||
new Promise<PublicSystemInfo>((resolve, reject) => {
|
||||
new Promise<{
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'hostname'
|
||||
}>((resolve, reject) => {
|
||||
getSystemApi(api)
|
||||
.getPublicSystemInfo()
|
||||
.then((response) => {
|
||||
@@ -55,7 +58,10 @@ export default function ServerAddress({
|
||||
'Jellyfin instance did not respond to our hostname request',
|
||||
),
|
||||
)
|
||||
return resolve(response.data)
|
||||
return resolve({
|
||||
publicSystemInfoResponse: response.data,
|
||||
connectionType: 'hostname',
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('An error occurred getting public system info', error)
|
||||
@@ -69,7 +75,10 @@ export default function ServerAddress({
|
||||
`${useHttps ? https : http}${ipAddress[0]}:${serverAddress.split(':')[1]}`,
|
||||
)
|
||||
const connectViaLocalNetworkPromise = () =>
|
||||
new Promise<PublicSystemInfo>((resolve, reject) => {
|
||||
new Promise<{
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'ipAddress'
|
||||
}>((resolve, reject) => {
|
||||
getSystemApi(ipAddressApi)
|
||||
.getPublicSystemInfo()
|
||||
.then((response) => {
|
||||
@@ -79,7 +88,10 @@ export default function ServerAddress({
|
||||
'Jellyfin instance did not respond to our IP Address request',
|
||||
),
|
||||
)
|
||||
return resolve(response.data)
|
||||
return resolve({
|
||||
publicSystemInfoResponse: response.data,
|
||||
connectionType: 'ipAddress',
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('An error occurred getting public system info', error)
|
||||
@@ -89,14 +101,24 @@ export default function ServerAddress({
|
||||
|
||||
return connectViaHostnamePromise().catch(() => connectViaLocalNetworkPromise())
|
||||
},
|
||||
onSuccess: (publicSystemInfoResponse) => {
|
||||
onSuccess: ({
|
||||
publicSystemInfoResponse,
|
||||
connectionType,
|
||||
}: {
|
||||
publicSystemInfoResponse: PublicSystemInfo
|
||||
connectionType: 'hostname' | 'ipAddress'
|
||||
}) => {
|
||||
if (!publicSystemInfoResponse.Version)
|
||||
throw new Error('Jellyfin instance did not respond')
|
||||
|
||||
console.debug(`Connected to Jellyfin via ${connectionType}`, publicSystemInfoResponse)
|
||||
console.log(`Connected to Jellyfin ${publicSystemInfoResponse.Version!}`)
|
||||
|
||||
const server: JellifyServer = {
|
||||
url: `${useHttps ? https : http}${serverAddress!}`,
|
||||
url:
|
||||
connectionType === 'hostname'
|
||||
? `${useHttps ? https : http}${serverAddress!}`
|
||||
: publicSystemInfoResponse.LocalAddress!,
|
||||
address: serverAddress!,
|
||||
name: publicSystemInfoResponse.ServerName!,
|
||||
version: publicSystemInfoResponse.Version!,
|
||||
|
||||
@@ -2,8 +2,8 @@ import React, { useState } from 'react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import _ from 'lodash'
|
||||
import { JellyfinCredentials } from '../../../api/types/jellyfin-credentials'
|
||||
import { getToken, Spacer, Spinner, XStack, YStack } from 'tamagui'
|
||||
import { H2 } from '../../Global/helpers/text'
|
||||
import { getToken, H6, Spacer, Spinner, XStack, YStack } from 'tamagui'
|
||||
import { H2, H5, Text } from '../../Global/helpers/text'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { JellifyUser } from '../../../types/JellifyUser'
|
||||
@@ -69,6 +69,9 @@ export default function ServerAuthentication({
|
||||
<H2 marginHorizontal={'$2'} textAlign='center'>
|
||||
{`Sign in to ${server?.name ?? 'Jellyfin'}`}
|
||||
</H2>
|
||||
<H6 marginHorizontal={'$2'} textAlign='center'>
|
||||
{server?.version ?? 'Unknown Jellyfin version'}
|
||||
</H6>
|
||||
</YStack>
|
||||
<YStack marginHorizontal={'$4'}>
|
||||
<Input
|
||||
@@ -103,7 +106,7 @@ export default function ServerAuthentication({
|
||||
if (navigation.canGoBack()) navigation.goBack()
|
||||
else
|
||||
navigation.navigate('ServerAddress', undefined, {
|
||||
pop: false,
|
||||
pop: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
@@ -115,6 +118,7 @@ export default function ServerAuthentication({
|
||||
<Button
|
||||
marginVertical={0}
|
||||
disabled={_.isEmpty(username) || useApiMutation.isPending}
|
||||
icon={() => <Icon name='chevron-right' small />}
|
||||
onPress={() => {
|
||||
if (!_.isUndefined(username)) {
|
||||
console.log(`Signing in...`)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { fetchUserViews } from '../../../api/queries/libraries'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import Icon from '../../Global/components/icon'
|
||||
|
||||
export default function ServerLibrary({
|
||||
navigation,
|
||||
@@ -83,6 +84,7 @@ export default function ServerLibrary({
|
||||
|
||||
<Button
|
||||
disabled={!libraryId}
|
||||
icon={() => <Icon name='guitar-electric' small />}
|
||||
onPress={() => {
|
||||
setLibrary({
|
||||
musicLibraryId: libraryId!,
|
||||
@@ -105,10 +107,11 @@ export default function ServerLibrary({
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={() => <Icon name='chevron-left' small />}
|
||||
onPress={() => {
|
||||
setUser(undefined)
|
||||
navigation.navigate('ServerAuthentication', undefined, {
|
||||
pop: false,
|
||||
pop: true,
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -9,17 +9,19 @@ 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'
|
||||
const SettingsTabsNavigator = createMaterialTopTabNavigator()
|
||||
|
||||
export default function Settings(): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const { devTools } = useSettingsContext()
|
||||
|
||||
return (
|
||||
<SettingsTabsNavigator.Navigator
|
||||
screenOptions={{
|
||||
tabBarScrollEnabled: true,
|
||||
tabBarGap: getToken('$size.0'),
|
||||
tabBarScrollEnabled: true,
|
||||
tabBarItemStyle: {
|
||||
width: getToken('$size.8'),
|
||||
},
|
||||
@@ -69,16 +71,6 @@ export default function Settings(): React.JSX.Element {
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Labs'
|
||||
component={LabsTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon name='flask' color={focused ? '$primary' : '$borderColor'} small />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Account'
|
||||
component={AccountTab}
|
||||
@@ -106,6 +98,21 @@ export default function Settings(): React.JSX.Element {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{devTools && (
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Labs'
|
||||
component={LabsTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon
|
||||
name='flask'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SettingsTabsNavigator.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
import { https } from '../../Login/utils/constants'
|
||||
|
||||
export default function AccountTab(): React.JSX.Element {
|
||||
const { user, library, server } = useJellifyContext()
|
||||
@@ -33,11 +34,11 @@ export default function AccountTab(): React.JSX.Element {
|
||||
children: <Text>{library?.musicLibraryName ?? 'Unknown Library'}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Jellyfin Server',
|
||||
title: server?.name ?? 'Untitled Server',
|
||||
subTitle: server?.version ?? 'Unknown Jellyfin Version',
|
||||
iconName: 'server',
|
||||
iconColor: '$borderColor',
|
||||
children: <Text>{server?.name ?? 'Unknown Server'}</Text>,
|
||||
iconName: server?.url.includes(https) ? 'lock' : 'lock-open',
|
||||
iconColor: server?.url.includes(https) ? '$success' : '$borderColor',
|
||||
children: <Text>{server?.address ?? 'Unknown Server'}</Text>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -10,15 +10,26 @@ import fetchPatrons from '../../../../api/queries/patrons'
|
||||
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'
|
||||
export default function InfoTabIndex({ navigation }: InfoTabStackNavigationProp) {
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
const { setDevTools } = useSettingsContext()
|
||||
|
||||
const [versionNumberPresses, setVersionNumberPresses] = useState(0)
|
||||
|
||||
const { data: patrons } = useQuery({
|
||||
queryKey: [QueryKeys.Patrons],
|
||||
queryFn: () => fetchPatrons(api),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (versionNumberPresses > 5) {
|
||||
setDevTools(true)
|
||||
}
|
||||
}, [versionNumberPresses])
|
||||
|
||||
return (
|
||||
<ScrollView contentInsetAdjustmentBehavior='automatic'>
|
||||
<SettingsListGroup
|
||||
@@ -30,7 +41,13 @@ export default function InfoTabIndex({ navigation }: InfoTabStackNavigationProp)
|
||||
iconColor: '$borderColor',
|
||||
children: (
|
||||
<YStack gap={'$2'}>
|
||||
<Text>Made with 💜 by Violet Caulfield</Text>
|
||||
<Text
|
||||
onPress={() =>
|
||||
setVersionNumberPresses(versionNumberPresses + 1)
|
||||
}
|
||||
>
|
||||
Made with 💜 by Violet Caulfield
|
||||
</Text>
|
||||
|
||||
<Separator marginBottom={'$2'} />
|
||||
<XStack gap={'$3'}>
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import { ListItem, View, YGroup } from 'tamagui'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import { storage } from '../../../constants/storage'
|
||||
|
||||
export default function LabsTab(): React.JSX.Element {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return (
|
||||
<SettingsListGroup
|
||||
borderColor={'$danger'}
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Nothing to see here...(yet)',
|
||||
subTitle: 'Come back later to enable experimental features',
|
||||
title: 'Clear Artists Cache',
|
||||
subTitle: 'Invalidates the artists in the library',
|
||||
iconName: 'test-tube-off',
|
||||
iconColor: '$danger',
|
||||
children: (
|
||||
<Button
|
||||
onPress={() => {
|
||||
storage.delete(QueryKeys.AllArtistsAlphabetical)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QueryKeys.AllArtistsAlphabetical],
|
||||
})
|
||||
}}
|
||||
>
|
||||
Clear Cache
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Button from '../../Global/helpers/button'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { SettingsStackParamList } from '../../../screens/Settings/types'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
|
||||
import Icon from '../../Global/components/icon'
|
||||
export default function SignOut({
|
||||
navigation,
|
||||
}: {
|
||||
@@ -12,6 +12,7 @@ export default function SignOut({
|
||||
return (
|
||||
<Button
|
||||
color={'$danger'}
|
||||
icon={() => <Icon name='hand-peace' small color={'$danger'} />}
|
||||
borderColor={'$danger'}
|
||||
marginHorizontal={'$6'}
|
||||
onPress={() => {
|
||||
|
||||
25
src/components/types.d.ts
vendored
25
src/components/types.d.ts
vendored
@@ -16,13 +16,13 @@ export type StackParamList = {
|
||||
Home: undefined
|
||||
AddPlaylist: undefined
|
||||
RecentArtists: {
|
||||
artists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
artists: BaseItemDto[] | undefined
|
||||
fetchNextPage: () => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
}
|
||||
MostPlayedArtists: {
|
||||
artists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
artists: BaseItemDto[] | undefined
|
||||
fetchNextPage: () => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
@@ -53,11 +53,12 @@ export type StackParamList = {
|
||||
|
||||
Discover: undefined
|
||||
RecentlyAdded: {
|
||||
albums: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
albums: BaseItemDto[] | undefined
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
fetchNextPage: () => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
isFetchingNextPage: boolean
|
||||
}
|
||||
|
||||
Library: undefined
|
||||
@@ -148,32 +149,38 @@ export type LibraryProps = NativeStackScreenProps<StackParamList, 'Library'>
|
||||
export type TracksProps = NativeStackScreenProps<StackParamList, 'Tracks'>
|
||||
|
||||
export type ArtistsProps = {
|
||||
artists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
artists: (string | number | BaseItemDto)[] | undefined
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
fetchNextPage: () => void
|
||||
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
isFetchingNextPage: boolean
|
||||
showAlphabeticalSelector: boolean
|
||||
}
|
||||
export type AlbumsProps = {
|
||||
albums: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
albums: (string | number | BaseItemDto)[] | undefined
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
fetchNextPage: () => void
|
||||
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: () => void
|
||||
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
isFetchingNextPage: boolean
|
||||
}
|
||||
export type PlaylistsProps = {
|
||||
playlists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
fetchNextPage: () => void
|
||||
fetchNextPage: (options?: FetchNextPageOptions | undefined) => void
|
||||
hasNextPage: boolean
|
||||
isPending: boolean
|
||||
isFetchingNextPage: boolean
|
||||
}
|
||||
|
||||
export type DeletePlaylistProps = NativeStackScreenProps<StackParamList, 'DeletePlaylist'>
|
||||
|
||||
@@ -15,4 +15,6 @@ export enum MMKVStorageKeys {
|
||||
SendMetrics = 'SEND_METRICS',
|
||||
AutoDownload = 'AutoDownload',
|
||||
LibraryIsDownloaded = 'LibraryIsDownloaded',
|
||||
DevTools = 'DevTools',
|
||||
LibraryArtistPageParam = 'LibraryArtistPageParam',
|
||||
}
|
||||
|
||||
@@ -80,4 +80,5 @@ export enum QueryKeys {
|
||||
AllAlbums = 'AllAlbums',
|
||||
StorageInUse = 'StorageInUse',
|
||||
Patrons = 'Patrons',
|
||||
AllArtistsAlphabetical = 'AllArtistsAlphabetical',
|
||||
}
|
||||
|
||||
@@ -1 +1,10 @@
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
/**
|
||||
* Sleep for a given number of milliseconds
|
||||
*
|
||||
* Name inspired by the album Sleepify by Vulfpeck
|
||||
* @see https://en.wikipedia.org/wiki/Sleepify
|
||||
*
|
||||
* @param ms The number of milliseconds to sleep
|
||||
* @returns A promise that resolves after the given number of milliseconds
|
||||
*/
|
||||
export const sleepify = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useJellifyContext } from '..'
|
||||
interface DiscoverContext {
|
||||
refreshing: boolean
|
||||
refresh: () => void
|
||||
recentlyAdded: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
recentlyAdded: BaseItemDto[] | undefined
|
||||
recentlyPlayed: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
fetchNextRecentlyAdded: () => void
|
||||
fetchNextRecentlyPlayed: () => void
|
||||
@@ -15,6 +15,8 @@ interface DiscoverContext {
|
||||
hasNextRecentlyPlayed: boolean
|
||||
isPendingRecentlyAdded: boolean
|
||||
isPendingRecentlyPlayed: boolean
|
||||
isFetchingNextRecentlyAdded: boolean
|
||||
isFetchingNextRecentlyPlayed: boolean
|
||||
}
|
||||
|
||||
const DiscoverContextInitializer = () => {
|
||||
@@ -27,9 +29,11 @@ const DiscoverContextInitializer = () => {
|
||||
fetchNextPage: fetchNextRecentlyAdded,
|
||||
hasNextPage: hasNextRecentlyAdded,
|
||||
isPending: isPendingRecentlyAdded,
|
||||
isFetchingNextPage: isFetchingNextRecentlyAdded,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyAdded],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined),
|
||||
initialPageParam: 0,
|
||||
})
|
||||
@@ -40,6 +44,7 @@ const DiscoverContextInitializer = () => {
|
||||
fetchNextPage: fetchNextRecentlyPlayed,
|
||||
hasNextPage: hasNextRecentlyPlayed,
|
||||
isPending: isPendingRecentlyPlayed,
|
||||
isFetchingNextPage: isFetchingNextRecentlyPlayed,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayed],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam),
|
||||
@@ -65,6 +70,8 @@ const DiscoverContextInitializer = () => {
|
||||
hasNextRecentlyPlayed,
|
||||
isPendingRecentlyAdded,
|
||||
isPendingRecentlyPlayed,
|
||||
isFetchingNextRecentlyAdded,
|
||||
isFetchingNextRecentlyPlayed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +86,8 @@ const DiscoverContext = createContext<DiscoverContext>({
|
||||
hasNextRecentlyPlayed: false,
|
||||
isPendingRecentlyAdded: false,
|
||||
isPendingRecentlyPlayed: false,
|
||||
isFetchingNextRecentlyAdded: false,
|
||||
isFetchingNextRecentlyPlayed: false,
|
||||
})
|
||||
|
||||
export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
||||
|
||||
@@ -14,7 +14,7 @@ const DisplayContextInitializer = () => {
|
||||
const { width } = useSafeAreaFrame()
|
||||
|
||||
const [numberOfColumns, setNumberOfColumns] = useState<number>(
|
||||
Math.floor(width / getTokens().size.$11.val),
|
||||
Math.floor(width / getTokens().size.$12.val),
|
||||
)
|
||||
|
||||
const [display, setDisplay] = useState<'grid' | 'list'>('grid')
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useJellifyContext } from '..'
|
||||
interface HomeContext {
|
||||
refreshing: boolean
|
||||
onRefresh: () => void
|
||||
recentArtists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
recentArtists: BaseItemDto[] | undefined
|
||||
recentTracks: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
|
||||
fetchNextRecentTracks: () => void
|
||||
@@ -25,7 +25,7 @@ interface HomeContext {
|
||||
fetchNextFrequentlyPlayed: () => void
|
||||
hasNextFrequentlyPlayed: boolean
|
||||
|
||||
frequentArtists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
frequentArtists: BaseItemDto[] | undefined
|
||||
frequentlyPlayed: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
|
||||
isFetchingRecentTracks: boolean
|
||||
@@ -65,12 +65,13 @@ const HomeContextInitializer = () => {
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.RecentlyPlayedArtists],
|
||||
queryFn: ({ pageParam }) => fetchRecentlyPlayedArtists(pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug('Getting next page for recent artists')
|
||||
return lastPage.length > 0 ? lastPageParam + 1 : undefined
|
||||
},
|
||||
enabled: !isErrorRecentTracks && recentTracks && recentTracks.pages.length > 0,
|
||||
enabled: !!recentTracks && recentTracks.pages.length > 0,
|
||||
})
|
||||
|
||||
const {
|
||||
@@ -100,6 +101,7 @@ const HomeContextInitializer = () => {
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.FrequentArtists],
|
||||
queryFn: ({ pageParam }) => fetchFrequentlyPlayedArtists(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug('Getting next page for frequent artists')
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { BaseItemDto, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
FetchNextPageOptions,
|
||||
FetchPreviousPageOptions,
|
||||
InfiniteData,
|
||||
InfiniteQueryObserverResult,
|
||||
QueryObserverResult,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { useJellifyContext } from '..'
|
||||
import { fetchArtists } from '../../api/queries/artist'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { createContext, RefObject, useContext, useRef, useState } from 'react'
|
||||
import { useDisplayContext } from '../Display/display-provider'
|
||||
import QueryConfig from '../../api/queries/query.config'
|
||||
import { fetchTracks } from '../../api/queries/tracks'
|
||||
import { fetchAlbums } from '../../api/queries/album'
|
||||
import { useLibrarySortAndFilterContext } from './sorting-filtering'
|
||||
|
||||
export const alphabet = '#abcdefghijklmnopqrstuvwxyz'.split('')
|
||||
|
||||
interface LibraryContext {
|
||||
artists: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
albums: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
artists: (string | number | BaseItemDto)[] | undefined
|
||||
albums: (string | number | BaseItemDto)[] | undefined
|
||||
tracks: InfiniteData<BaseItemDto[], unknown> | undefined
|
||||
// genres: BaseItemDto[] | undefined
|
||||
// playlists: BaseItemDto[] | undefined
|
||||
@@ -23,18 +32,28 @@ interface LibraryContext {
|
||||
// refetchGenres: () => void
|
||||
// refetchPlaylists: () => void
|
||||
|
||||
fetchNextArtistsPage: () => void
|
||||
fetchNextArtistsPage: (
|
||||
options?: FetchNextPageOptions,
|
||||
) => Promise<InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>>
|
||||
hasNextArtistsPage: boolean
|
||||
|
||||
fetchNextTracksPage: () => void
|
||||
fetchNextTracksPage: (options?: FetchNextPageOptions | undefined) => void
|
||||
hasNextTracksPage: boolean
|
||||
|
||||
fetchNextAlbumsPage: () => void
|
||||
fetchNextAlbumsPage: (
|
||||
options?: FetchNextPageOptions | undefined,
|
||||
) => Promise<InfiniteQueryObserverResult<(string | number | BaseItemDto)[], Error>>
|
||||
hasNextAlbumsPage: boolean
|
||||
|
||||
isPendingArtists: boolean
|
||||
isPendingTracks: boolean
|
||||
isPendingAlbums: boolean
|
||||
|
||||
artistPageParams: RefObject<string[]>
|
||||
albumPageParams: RefObject<string[]>
|
||||
isFetchingNextArtistsPage: boolean
|
||||
isFetchingNextTracksPage: boolean
|
||||
isFetchingNextAlbumsPage: boolean
|
||||
}
|
||||
|
||||
const LibraryContextInitializer = () => {
|
||||
@@ -44,27 +63,53 @@ const LibraryContextInitializer = () => {
|
||||
|
||||
const { sortDescending, isFavorites } = useLibrarySortAndFilterContext()
|
||||
|
||||
const artistPageParams = useRef<string[]>([])
|
||||
|
||||
const albumPageParams = useRef<string[]>([])
|
||||
|
||||
const {
|
||||
data: artists,
|
||||
isPending: isPendingArtists,
|
||||
refetch: refetchArtists,
|
||||
fetchNextPage: fetchNextArtistsPage,
|
||||
hasNextPage: hasNextArtistsPage,
|
||||
isFetchingNextPage: isFetchingNextArtistsPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.AllArtists, isFavorites, sortDescending],
|
||||
queryKey: [QueryKeys.AllArtistsAlphabetical, isFavorites, sortDescending],
|
||||
queryFn: ({ pageParam }) =>
|
||||
fetchArtists(
|
||||
api,
|
||||
library,
|
||||
pageParam,
|
||||
isFavorites,
|
||||
false,
|
||||
[ItemSortBy.SortName],
|
||||
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
|
||||
),
|
||||
initialPageParam: 0,
|
||||
select: (data) => data.pages.flatMap((page) => [page.title, ...page.data]),
|
||||
initialPageParam: alphabet[0],
|
||||
maxPages: alphabet.length,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug(`Artists last page length: ${lastPage.length}`)
|
||||
return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined
|
||||
console.debug(`Fetching next Artists page, last page: ${lastPage.title}`)
|
||||
console.debug(`fetching artist params: ${allPageParams.join(', ')}`)
|
||||
if (lastPageParam !== alphabet[alphabet.length - 1]) {
|
||||
artistPageParams.current = [
|
||||
...allPageParams,
|
||||
alphabet[alphabet.indexOf(lastPageParam) + 1],
|
||||
]
|
||||
return alphabet[alphabet.indexOf(lastPageParam) + 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
|
||||
console.debug(`Artists first page: ${firstPage.title}`)
|
||||
artistPageParams.current = allPageParams
|
||||
if (firstPageParam !== alphabet[0]) {
|
||||
artistPageParams.current = allPageParams
|
||||
return alphabet[alphabet.indexOf(firstPageParam) - 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
|
||||
@@ -73,6 +118,8 @@ const LibraryContextInitializer = () => {
|
||||
isPending: isPendingTracks,
|
||||
refetch: refetchTracks,
|
||||
fetchNextPage: fetchNextTracksPage,
|
||||
isFetchingNextPage: isFetchingNextTracksPage,
|
||||
isError: isFetchingTracksError,
|
||||
hasNextPage: hasNextTracksPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.AllTracks, isFavorites, sortDescending],
|
||||
@@ -99,6 +146,7 @@ const LibraryContextInitializer = () => {
|
||||
isPending: isPendingAlbums,
|
||||
refetch: refetchAlbums,
|
||||
fetchNextPage: fetchNextAlbumsPage,
|
||||
isFetchingNextPage: isFetchingNextAlbumsPage,
|
||||
hasNextPage: hasNextAlbumsPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: [QueryKeys.AllAlbums, isFavorites, sortDescending],
|
||||
@@ -111,10 +159,30 @@ const LibraryContextInitializer = () => {
|
||||
[ItemSortBy.SortName],
|
||||
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
|
||||
),
|
||||
initialPageParam: 0,
|
||||
initialPageParam: alphabet[0],
|
||||
select: (data) => data.pages.flatMap((page) => [page.title, ...page.data]),
|
||||
maxPages: alphabet.length,
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug(`Albums last page length: ${lastPage.length}`)
|
||||
return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined
|
||||
console.debug(`Albums last page length: ${lastPage.data.length}`)
|
||||
if (lastPageParam !== alphabet[alphabet.length - 1]) {
|
||||
albumPageParams.current = [
|
||||
...allPageParams,
|
||||
alphabet[alphabet.indexOf(lastPageParam) + 1],
|
||||
]
|
||||
return alphabet[alphabet.indexOf(lastPageParam) + 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
|
||||
console.debug(`Albums first page: ${firstPage.title}`)
|
||||
albumPageParams.current = allPageParams
|
||||
if (firstPageParam !== alphabet[0]) {
|
||||
albumPageParams.current = allPageParams
|
||||
return alphabet[alphabet.indexOf(firstPageParam) - 1]
|
||||
}
|
||||
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
|
||||
@@ -134,13 +202,59 @@ const LibraryContextInitializer = () => {
|
||||
isPendingArtists,
|
||||
isPendingTracks,
|
||||
isPendingAlbums,
|
||||
artistPageParams,
|
||||
albumPageParams,
|
||||
isFetchingNextArtistsPage,
|
||||
isFetchingNextTracksPage,
|
||||
isFetchingNextAlbumsPage,
|
||||
}
|
||||
}
|
||||
|
||||
const LibraryContext = createContext<LibraryContext>({
|
||||
artists: undefined,
|
||||
refetchArtists: () => {},
|
||||
fetchNextArtistsPage: () => {},
|
||||
fetchNextArtistsPage: async () => {
|
||||
return {
|
||||
data: [],
|
||||
status: 'success',
|
||||
fetchStatus: 'idle',
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isStale: false,
|
||||
error: null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
refetch: async () => Promise.resolve({} as any),
|
||||
remove: () => {},
|
||||
dataUpdatedAt: 0,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
isFetched: true,
|
||||
isFetchingNextPage: false,
|
||||
isFetchingPreviousPage: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fetchNextPage: async () => Promise.resolve({} as any),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fetchPreviousPage: async () => Promise.resolve({} as any),
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
isPending: false,
|
||||
isLoadingError: false,
|
||||
isRefetchError: false,
|
||||
isPlaceholderData: false,
|
||||
isFetchNextPageError: false,
|
||||
isFetchPreviousPageError: false,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
isFetchedAfterMount: true,
|
||||
isInitialLoading: false,
|
||||
isPaused: false,
|
||||
isRefetching: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
promise: Promise.resolve({} as any),
|
||||
}
|
||||
},
|
||||
hasNextArtistsPage: false,
|
||||
tracks: undefined,
|
||||
refetchTracks: () => {},
|
||||
@@ -148,11 +262,57 @@ const LibraryContext = createContext<LibraryContext>({
|
||||
hasNextTracksPage: false,
|
||||
albums: undefined,
|
||||
refetchAlbums: () => {},
|
||||
fetchNextAlbumsPage: () => {},
|
||||
fetchNextAlbumsPage: async () => {
|
||||
return {
|
||||
data: [],
|
||||
status: 'success',
|
||||
fetchStatus: 'idle',
|
||||
isFetching: false,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isStale: false,
|
||||
error: null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
refetch: async () => Promise.resolve({} as any),
|
||||
remove: () => {},
|
||||
dataUpdatedAt: 0,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
isFetched: true,
|
||||
isFetchingNextPage: false,
|
||||
isFetchingPreviousPage: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fetchNextPage: async () => Promise.resolve({} as any),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fetchPreviousPage: async () => Promise.resolve({} as any),
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
isPending: false,
|
||||
isLoadingError: false,
|
||||
isRefetchError: false,
|
||||
isPlaceholderData: false,
|
||||
isFetchNextPageError: false,
|
||||
isFetchPreviousPageError: false,
|
||||
failureReason: null,
|
||||
errorUpdateCount: 0,
|
||||
isFetchedAfterMount: true,
|
||||
isInitialLoading: false,
|
||||
isPaused: false,
|
||||
isRefetching: false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
promise: Promise.resolve({} as any),
|
||||
}
|
||||
},
|
||||
hasNextAlbumsPage: false,
|
||||
isPendingArtists: false,
|
||||
isPendingTracks: false,
|
||||
isPendingAlbums: false,
|
||||
artistPageParams: { current: [] },
|
||||
albumPageParams: { current: [] },
|
||||
isFetchingNextArtistsPage: false,
|
||||
isFetchingNextTracksPage: false,
|
||||
isFetchingNextAlbumsPage: false,
|
||||
})
|
||||
|
||||
export const LibraryProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -162,3 +322,4 @@ export const LibraryProvider = ({ children }: { children: React.ReactNode }) =>
|
||||
}
|
||||
|
||||
export const useLibraryContext = () => useContext(LibraryContext)
|
||||
export { useLibrarySortAndFilterContext }
|
||||
|
||||
@@ -8,6 +8,8 @@ interface SettingsContext {
|
||||
setSendMetrics: React.Dispatch<React.SetStateAction<boolean>>
|
||||
autoDownload: boolean
|
||||
setAutoDownload: React.Dispatch<React.SetStateAction<boolean>>
|
||||
devTools: boolean
|
||||
setDevTools: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,12 +28,16 @@ const SettingsContextInitializer = () => {
|
||||
|
||||
const autoDownloadInit = storage.getBoolean(MMKVStorageKeys.AutoDownload)
|
||||
|
||||
const devToolsInit = storage.getBoolean(MMKVStorageKeys.DevTools)
|
||||
|
||||
const [sendMetrics, setSendMetrics] = useState(sendMetricsInit ?? false)
|
||||
|
||||
const [autoDownload, setAutoDownload] = useState(
|
||||
autoDownloadInit ?? ['ios', 'android'].includes(Platform.OS),
|
||||
)
|
||||
|
||||
const [devTools, setDevTools] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.SendMetrics, sendMetrics)
|
||||
}, [sendMetrics])
|
||||
@@ -40,11 +46,17 @@ const SettingsContextInitializer = () => {
|
||||
storage.set(MMKVStorageKeys.AutoDownload, autoDownload)
|
||||
}, [autoDownload])
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.DevTools, devTools)
|
||||
}, [devTools])
|
||||
|
||||
return {
|
||||
sendMetrics,
|
||||
setSendMetrics,
|
||||
autoDownload,
|
||||
setAutoDownload,
|
||||
devTools,
|
||||
setDevTools,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +65,8 @@ export const SettingsContext = createContext<SettingsContext>({
|
||||
setSendMetrics: () => {},
|
||||
autoDownload: false,
|
||||
setAutoDownload: () => {},
|
||||
devTools: false,
|
||||
setDevTools: () => {},
|
||||
})
|
||||
|
||||
export const SettingsProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
@@ -12,6 +12,8 @@ export default function RecentlyAdded({
|
||||
fetchNextPage={route.params.fetchNextPage}
|
||||
hasNextPage={route.params.hasNextPage}
|
||||
isPending={route.params.isPending}
|
||||
isFetchingNextPage={route.params.isFetchingNextPage}
|
||||
showAlphabeticalSelector={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export default function HomeArtistsScreen({
|
||||
fetchNextPage={fetchNextFrequentArtists}
|
||||
hasNextPage={hasNextFrequentArtists}
|
||||
isPending={isFetchingFrequentArtists}
|
||||
isFetchingNextPage={isFetchingFrequentArtists}
|
||||
showAlphabeticalSelector={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +39,8 @@ export default function HomeArtistsScreen({
|
||||
fetchNextPage={fetchNextRecentArtists}
|
||||
hasNextPage={hasNextRecentArtists}
|
||||
isPending={isFetchingRecentArtists}
|
||||
isFetchingNextPage={isFetchingRecentArtists}
|
||||
showAlphabeticalSelector={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ export default function SettingsScreen(): React.JSX.Element {
|
||||
/* https://www.reddit.com/r/reactnative/comments/1dgktbn/comment/lxd23sj/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button */
|
||||
presentation: 'formSheet',
|
||||
sheetInitialDetentIndex: 0,
|
||||
sheetAllowedDetents: [0.2],
|
||||
sheetAllowedDetents: [0.25],
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</SettingsStack.Navigator>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { Button, Spacer, View, XStack, YStack } from 'tamagui'
|
||||
import { Spacer, View, XStack, YStack } from 'tamagui'
|
||||
import { SignOutModalProps } from './types'
|
||||
import { H5, Text } from '../../components/Global/helpers/text'
|
||||
export default function SignOutModal({ navigation }: SignOutModalProps): React.JSX.Element {
|
||||
return (
|
||||
<YStack marginHorizontal={'$6'}>
|
||||
<H5>Sign out?</H5>
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import Icon from '../../components/Global/components/icon'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
|
||||
<Spacer />
|
||||
export default function SignOutModal({ navigation }: SignOutModalProps): React.JSX.Element {
|
||||
const { server } = useJellifyContext()
|
||||
|
||||
return (
|
||||
<YStack margin={'$6'}>
|
||||
<H5>{`Sign out of ${server?.name ?? 'Jellyfin'}?`}</H5>
|
||||
<XStack gap={'$2'}>
|
||||
<Button
|
||||
icon={() => <Icon name='chevron-left' small color={'$borderColor'} />}
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
flex={1}
|
||||
@@ -23,6 +28,7 @@ export default function SignOutModal({ navigation }: SignOutModalProps): React.J
|
||||
</Button>
|
||||
<Button
|
||||
flex={1}
|
||||
icon={() => <Icon name='logout' small color={'$danger'} />}
|
||||
color={'$danger'}
|
||||
borderColor={'$danger'}
|
||||
onPress={() => {
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@@ -2182,6 +2182,14 @@
|
||||
dependencies:
|
||||
"@sentry/core" "8.54.0"
|
||||
|
||||
"@shopify/flash-list@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.8.0.tgz#d271d3ca49acf9f53812594f0c4d24a994eb95e5"
|
||||
integrity sha512-APZ48kceCCJobUimmI2594io+HujELK60HFKgzIyIdHGX5ySR5YfvsPy3PKtPwHHDtIMFNaq3U/BY3qZocOhCA==
|
||||
dependencies:
|
||||
recyclerlistview "4.2.3"
|
||||
tslib "2.8.1"
|
||||
|
||||
"@sideway/address@^4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5"
|
||||
@@ -6970,7 +6978,7 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
lodash.debounce@4.0.8, lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||
@@ -7971,7 +7979,7 @@ prompts@^2.0.1, prompts@^2.4.2:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@@ -8388,6 +8396,15 @@ recast@^0.23.3:
|
||||
tiny-invariant "^1.3.3"
|
||||
tslib "^2.0.1"
|
||||
|
||||
recyclerlistview@4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.3.tgz#14032e7ad2f24396e24d5b3060c6ba76b567f000"
|
||||
integrity sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==
|
||||
dependencies:
|
||||
lodash.debounce "4.0.8"
|
||||
prop-types "15.8.1"
|
||||
ts-object-utils "0.0.5"
|
||||
|
||||
redent@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
|
||||
@@ -9331,6 +9348,11 @@ ts-api-utils@^1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
|
||||
integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==
|
||||
|
||||
ts-object-utils@0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077"
|
||||
integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==
|
||||
|
||||
tsconfig-paths@^3.15.0:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4"
|
||||
@@ -9341,16 +9363,16 @@ tsconfig-paths@^3.15.0:
|
||||
minimist "^1.2.6"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
tslib@2.8.1, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tslib@^1.8.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
|
||||
|
||||
Reference in New Issue
Block a user