diff --git a/README.md b/README.md index f9b086a8..359a1936 100644 --- a/README.md +++ b/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. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 78f0b981..a0323aed 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/package.json b/package.json index 0c8e6580..88d81176 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/api/queries/album.ts b/src/api/queries/album.ts index 181dc161..51b27843 100644 --- a/src/api/queries/album.ts +++ b/src/api/queries/album.ts @@ -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 { +): Promise<{ title: string | number; data: BaseItemDto[] }> { console.debug('Fetching albums', page) return fetchItems(api, library, [BaseItemKind.MusicAlbum], page, sortBy, sortOrder, isFavorite) diff --git a/src/api/queries/artist.ts b/src/api/queries/artist.ts index 9434e9a8..43f4fba1 100644 --- a/src/api/queries/artist.ts +++ b/src/api/queries/artist.ts @@ -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 { +): Promise<{ title: string | number; data: BaseItemDto[] }> { console.debug('Fetching artists', page) return fetchItems(api, library, [BaseItemKind.MusicArtist], page, sortBy, sortOrder, isFavorite) } diff --git a/src/api/queries/item.ts b/src/api/queries/item.ts index c0fa99ad..3622c554 100644 --- a/src/api/queries/item.ts +++ b/src/api/queries/item.ts @@ -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 { +): 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) diff --git a/src/api/queries/query.config.ts b/src/api/queries/query.config.ts index 51c01422..e1dade21 100644 --- a/src/api/queries/query.config.ts +++ b/src/api/queries/query.config.ts @@ -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 diff --git a/src/components/Albums/component.tsx b/src/components/Albums/component.tsx index c79802ed..dbcd748e 100644 --- a/src/components/Albums/component.tsx +++ b/src/components/Albums/component.tsx @@ -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 ( - page) ?? []} - renderItem={({ index, item: album }) => ( - { - navigation.navigate('Album', { album }) - }} - size={'$11'} - /> - )} - onEndReached={() => { - if (hasNextPage) fetchNextPage() - }} - onEndReachedThreshold={0.25} - removeClippedSubviews - /> + + + typeof album === 'string' ? ( + + {album.toUpperCase()} + + ) : typeof album === 'number' ? null : typeof album === 'object' ? ( + + ) : null + } + ListEmptyComponent={ + isPending ? ( + + ) : ( + + No albums + + ) + } + onEndReached={() => { + if (hasNextPage) fetchNextPage() + }} + ListFooterComponent={isPending ? : null} + refreshControl={} + 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 + /> + ) } diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx index 837191a8..14d46941 100644 --- a/src/components/Artists/component.tsx +++ b/src/components/Artists/component.tsx @@ -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>(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 ( - page) ?? []} - renderItem={({ index, item: artist }) => ( - { - navigation.navigate('Artist', { artist }) - }} - size={'$11'} - /> + + + typeof item === 'string' + ? item + : typeof item === 'number' + ? item.toString() + : item.Id! + } + estimatedItemSize={itemHeight} + data={artists} + refreshControl={} + renderItem={({ index, item: artist }) => + typeof artist === 'string' ? ( + + {artist.toUpperCase()} + + ) : typeof artist === 'number' ? null : typeof artist === 'object' ? ( + + ) : null + } + ListEmptyComponent={ + isPending || isFetchingNextPage ? ( + + ) : ( + + No artists + + ) + } + ListFooterComponent={isPending ? : 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 && ( + + {memoizedAlphabet.map((letter) => ( + { + trigger('impactLight') + alphabeticalSelectorCallback(letter) + }} + > + {letter.toUpperCase()} + + ))} + )} - ListEmptyComponent={ - isPending ? ( - - ) : ( - - No artists - - ) - } - onEndReached={() => { - if (hasNextPage) fetchNextPage() - }} - onEndReachedThreshold={0.25} - removeClippedSubviews - /> + ) } diff --git a/src/components/Artists/screen.tsx b/src/components/Artists/screen.tsx index e3502071..3ae54aed 100644 --- a/src/components/Artists/screen.tsx +++ b/src/components/Artists/screen.tsx @@ -8,6 +8,8 @@ export default function ArtistsScreen({ fetchNextPage, hasNextPage, isPending, + isFetchingNextPage, + showAlphabeticalSelector, }: ArtistsProps): React.JSX.Element { return ( ) } diff --git a/src/components/Discover/helpers/just-added.tsx b/src/components/Discover/helpers/just-added.tsx index aaf3449a..19038885 100644 --- a/src/components/Discover/helpers/just-added.tsx +++ b/src/components/Discover/helpers/just-added.tsx @@ -12,8 +12,13 @@ export default function RecentlyAdded({ }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { recentlyAdded, fetchNextRecentlyAdded, hasNextRecentlyAdded, isPendingRecentlyAdded } = - useDiscoverContext() + const { + recentlyAdded, + fetchNextRecentlyAdded, + hasNextRecentlyAdded, + isPendingRecentlyAdded, + isFetchingNextRecentlyAdded, + } = useDiscoverContext() return ( @@ -26,6 +31,7 @@ export default function RecentlyAdded({ fetchNextPage: fetchNextRecentlyAdded, hasNextPage: hasNextRecentlyAdded, isPending: isPendingRecentlyAdded, + isFetchingNextPage: isFetchingNextRecentlyAdded, }) }} > @@ -34,11 +40,7 @@ export default function RecentlyAdded({ 10) - ? recentlyAdded!.pages[0].slice(0, 10) - : recentlyAdded?.pages[0] - } + data={recentlyAdded?.slice(0, 10) ?? []} renderItem={({ item }) => ( + { switch (item.Type) { case 'MusicArtist': { @@ -73,18 +74,13 @@ export default function Item({ }) }} paddingVertical={'$2'} - marginHorizontal={'$1'} + paddingRight={'$2'} > - + - + {item.Name ?? ''} @@ -92,26 +88,26 @@ export default function Item({ {item.AlbumArtist ?? 'Untitled Artist'} )} + + {item.Type === 'MusicAlbum' && {item.RunTimeTicks}} - - {item.UserData?.IsFavorite ? ( - - ) : ( - - )} + + {/* Runtime ticks for Songs */} {item.Type === 'Audio' ? ( {item.RunTimeTicks} - ) : ( - - )} + ) : null} {item.Type === 'Audio' || item.Type === 'MusicAlbum' ? ( - ) : ( - - )} + ) : null} diff --git a/src/components/Home/helpers/frequent-artists.tsx b/src/components/Home/helpers/frequent-artists.tsx index 4ef2b08a..67546c8b 100644 --- a/src/components/Home/helpers/frequent-artists.tsx +++ b/src/components/Home/helpers/frequent-artists.tsx @@ -39,11 +39,7 @@ export default function FrequentArtists({ 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 }) => ( 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 }) => ( >() @@ -16,6 +22,8 @@ export default function AlbumsTab(): React.JSX.Element { fetchNextPage={fetchNextAlbumsPage} hasNextPage={hasNextAlbumsPage} isPending={isPendingAlbums} + isFetchingNextPage={isFetchingNextAlbumsPage} + showAlphabeticalSelector={true} /> ) } diff --git a/src/components/Library/components/artists-tab.tsx b/src/components/Library/components/artists-tab.tsx index 05e879b9..539b94b7 100644 --- a/src/components/Library/components/artists-tab.tsx +++ b/src/components/Library/components/artists-tab.tsx @@ -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>() @@ -17,6 +22,8 @@ export default function ArtistsTab(): React.JSX.Element { navigation={navigation} fetchNextPage={fetchNextArtistsPage} hasNextPage={hasNextArtistsPage} + isFetchingNextPage={isFetchingNextArtistsPage} + showAlphabeticalSelector={true} /> ) } diff --git a/src/components/Library/tab-bar.tsx b/src/components/Library/tab-bar.tsx index 9773c138..b6b97fba 100644 --- a/src/components/Library/tab-bar.tsx +++ b/src/components/Library/tab-bar.tsx @@ -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) { - - + {props.state.routes[props.state.index].name === 'Artists' ? null : ( + {props.state.routes[props.state.index].name === 'Playlists' ? ( )} - - {props.state.routes[props.state.index].name === 'Tracks' && ( - setIsDownloaded(!isDownloaded)} - alignItems={'center'} - justifyContent={'center'} - > - + {props.state.routes[props.state.index].name === 'Tracks' && ( + setIsDownloaded(!isDownloaded)} + alignItems={'center'} + justifyContent={'center'} + > + - - {isDownloaded ? 'Downloaded' : 'All'} - - - )} - - - - setSortDescending(!sortDescending)} - alignItems={'center'} - justifyContent={'center'} - > - - - - {sortDescending ? 'Descending' : 'Ascending'} - + + {isDownloaded ? 'Downloaded' : 'All'} + + + )} - + )} ) } diff --git a/src/components/Login/screens/server-address.tsx b/src/components/Login/screens/server-address.tsx index 7b88d11f..5a26872b 100644 --- a/src/components/Login/screens/server-address.tsx +++ b/src/components/Login/screens/server-address.tsx @@ -45,7 +45,10 @@ export default function ServerAddress({ const api = jellyfin.createApi(`${useHttps ? https : http}${serverAddress}`) const connectViaHostnamePromise = () => - new Promise((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((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!, diff --git a/src/components/Login/screens/server-authentication.tsx b/src/components/Login/screens/server-authentication.tsx index 2ee705cc..64a515f0 100644 --- a/src/components/Login/screens/server-authentication.tsx +++ b/src/components/Login/screens/server-authentication.tsx @@ -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({

{`Sign in to ${server?.name ?? 'Jellyfin'}`}

+
+ {server?.version ?? 'Unknown Jellyfin version'} +
@@ -115,6 +118,7 @@ export default function ServerAuthentication({ + ), }, ]} /> diff --git a/src/components/Settings/components/sign-out-button.tsx b/src/components/Settings/components/sign-out-button.tsx index 9f5f6e41..40db5605 100644 --- a/src/components/Settings/components/sign-out-button.tsx +++ b/src/components/Settings/components/sign-out-button.tsx @@ -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 (