diff --git a/.github/workflows/publish-ota-update.yml b/.github/workflows/publish-ota-update.yml
index 46e31454..68dfe2a2 100644
--- a/.github/workflows/publish-ota-update.yml
+++ b/.github/workflows/publish-ota-update.yml
@@ -1,8 +1,7 @@
name: Publish Over-the-Air Update
on:
- push:
- branches:
- - 'main'
+ workflow_dispatch:
+
jobs:
publish-ota-update:
runs-on: macos-15
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 357ca6cb..3614c397 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1722,6 +1722,35 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
+ - react-native-blurhash (2.1.1):
+ - boost
+ - DoubleConversion
+ - fast_float
+ - fmt
+ - glog
+ - hermes-engine
+ - RCT-Folly
+ - RCT-Folly/Fabric
+ - RCTRequired
+ - RCTTypeSafety
+ - React-Core
+ - React-debug
+ - React-Fabric
+ - React-featureflags
+ - React-graphics
+ - React-hermes
+ - React-ImageManager
+ - React-jsi
+ - React-NativeModulesApple
+ - React-RCTFabric
+ - React-renderercss
+ - React-rendererdebug
+ - React-utils
+ - ReactCodegen
+ - ReactCommon/turbomodule/bridging
+ - ReactCommon/turbomodule/core
+ - SocketRocket
+ - Yoga
- react-native-carplay (2.4.1-beta.0):
- React
- react-native-config (1.5.5):
@@ -2689,7 +2718,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNScreens (4.11.1):
+ - RNScreens (4.12.0):
- boost
- DoubleConversion
- fast_float
@@ -2717,10 +2746,10 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - RNScreens/common (= 4.11.1)
+ - RNScreens/common (= 4.12.0)
- SocketRocket
- Yoga
- - RNScreens/common (4.11.1):
+ - RNScreens/common (4.12.0):
- boost
- DoubleConversion
- fast_float
@@ -2867,6 +2896,7 @@ DEPENDENCIES:
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-background-actions (from `../node_modules/react-native-background-actions`)
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
+ - react-native-blurhash (from `../node_modules/react-native-blurhash`)
- react-native-carplay (from `../node_modules/react-native-carplay`)
- react-native-config (from `../node_modules/react-native-config`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
@@ -3023,6 +3053,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-background-actions"
react-native-blob-util:
:path: "../node_modules/react-native-blob-util"
+ react-native-blurhash:
+ :path: "../node_modules/react-native-blurhash"
react-native-carplay:
:path: "../node_modules/react-native-carplay"
react-native-config:
@@ -3176,6 +3208,7 @@ SPEC CHECKSUMS:
React-microtasksnativemodule: d9499269ad1f484ae71319bac1d9231447f2094e
react-native-background-actions: 48e6bad9e2a47e3b04858634c5a05ea11062f680
react-native-blob-util: a9a07801b63e97d1bbdcf4eba3b98ff16c249bd5
+ react-native-blurhash: 7aa21188beaed1eef0d9261e98befa56449eb65a
react-native-carplay: 8f388f6f73e5e0f73ed154ad8794371343ee20c0
react-native-config: 644074ab88db883fcfaa584f03520ec29589d7df
react-native-mmkv: 7fb4729ad5cb787a4394e6c4bd48e4b8ec30f25c
@@ -3225,7 +3258,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 5e1a1605659c22098719fc2e8aee453fe728f52e
RNReactNativeHapticFeedback: 8eb91a6f48567d02ec8026e515102e18c41030cf
RNReanimated: bc1ddb7a5352648bcf0d592256069833bf935a46
- RNScreens: ee2abe7e0c548eed14e92742e81ed991165c56aa
+ RNScreens: ab490a252dd536fca261c72ab7e5538e656dcb2b
RNSentry: 2b690575f638f588e51b6817e5f77c8ab62de2cf
RNVectorIcons: ef9b4b0b786053ebdd63ee2972f48de9633ba166
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
diff --git a/package.json b/package.json
index 6a8e524c..5d8dc98d 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
"react-native": "0.80.1",
"react-native-background-actions": "^4.0.1",
"react-native-blob-util": "^0.22.2",
+ "react-native-blurhash": "^2.1.1",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-config": "^1.5.5",
"react-native-device-info": "^14.0.4",
@@ -78,7 +79,7 @@
"react-native-pager-view": "^6.8.1",
"react-native-reanimated": "^3.18.0",
"react-native-safe-area-context": "^5.5.2",
- "react-native-screens": "^4.11.1",
+ "react-native-screens": "^4.12.0",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-toast-message": "^2.3.3",
diff --git a/src/api/queries/artist.ts b/src/api/queries/artist.ts
index 52f5c186..f41ccfd0 100644
--- a/src/api/queries/artist.ts
+++ b/src/api/queries/artist.ts
@@ -6,9 +6,10 @@ import {
ItemSortBy,
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
-import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
-import { fetchItems } from './item'
+import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { JellifyUser } from '../../types/JellifyUser'
+import QueryConfig from './query.config'
+import { alphabet } from '../../providers/Library'
export function fetchArtists(
api: Api | undefined,
user: JellifyUser | undefined,
@@ -19,16 +20,32 @@ export function fetchArtists(
sortOrder: SortOrder[] = [SortOrder.Ascending],
): Promise<{ title: string | number; data: BaseItemDto[] }> {
console.debug('Fetching artists', page)
- return fetchItems(
- api,
- user,
- library,
- [BaseItemKind.MusicArtist],
- page,
- sortBy,
- sortOrder,
- isFavorite,
- )
+
+ return new Promise((resolve, reject) => {
+ if (!api) return reject('No API instance provided')
+
+ getArtistsApi(api)
+ .getAlbumArtists({
+ parentId: library?.musicLibraryId,
+ userId: user?.id,
+ enableUserData: true,
+ sortBy: sortBy,
+ sortOrder: sortOrder,
+ startIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
+ limit: QueryConfig.limits.library,
+ isFavorite: isFavorite,
+ nameStartsWith: typeof page === 'string' && page !== alphabet[0] ? page : undefined,
+ nameLessThan: typeof page === 'string' && page === alphabet[0] ? 'A' : undefined,
+ })
+ .then((response) => {
+ return response.data.Items
+ ? resolve({ title: page, data: response.data.Items })
+ : resolve({ title: page, data: [] })
+ })
+ .catch((error) => {
+ reject(error)
+ })
+ })
}
/**
diff --git a/src/components/Artists/component.tsx b/src/components/Artists/component.tsx
index a2ef678c..135f0beb 100644
--- a/src/components/Artists/component.tsx
+++ b/src/components/Artists/component.tsx
@@ -66,6 +66,7 @@ export default function Artists({
useEffect(() => {
artistsRef.current = artists ?? []
+ console.debug(`artists: ${JSON.stringify(artists)}`)
}, [artists])
return (
diff --git a/src/components/Global/components/image.tsx b/src/components/Global/components/image.tsx
index 8aa648ab..4aa02afa 100644
--- a/src/components/Global/components/image.tsx
+++ b/src/components/Global/components/image.tsx
@@ -27,11 +27,12 @@ export default function ItemImage({
const theme = useTheme()
const imageUrl =
- (item.AlbumId && getImageApi(api!).getItemImageUrlById(item.AlbumId)) ||
- (item.Id && getImageApi(api!).getItemImageUrlById(item.Id)) ||
- ''
+ api &&
+ ((item.AlbumId && getImageApi(api).getItemImageUrlById(item.AlbumId)) ||
+ (item.Id && getImageApi(api).getItemImageUrlById(item.Id)) ||
+ '')
- return (
+ return api && imageUrl ? (
+ ) : (
+ <>>
)
}
diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx
index de36b10c..2020b243 100644
--- a/src/components/Global/components/item-row.tsx
+++ b/src/components/Global/components/item-row.tsx
@@ -119,28 +119,25 @@ export default function ItemRow({
{item.Name ?? ''}
{(item.Type === 'Audio' || item.Type === 'MusicAlbum') && (
-
+
{item.AlbumArtist ?? 'Untitled Artist'}
)}
-
- {item.Type === 'MusicAlbum' && {item.RunTimeTicks}}
{/* Runtime ticks for Songs */}
- {item.Type === 'Audio' ? (
+ {['Audio', 'MusicAlbum'].includes(item.Type ?? '') ? (
{item.RunTimeTicks}
+ ) : ['Playlist'].includes(item.Type ?? '') ? (
+ {`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Track' : 'Tracks'}`}
) : null}
{item.Type === 'Audio' || item.Type === 'MusicAlbum' ? (
diff --git a/src/components/Player/components/blurred-background.tsx b/src/components/Player/components/blurred-background.tsx
index 020b340e..3a4cfb6d 100644
--- a/src/components/Player/components/blurred-background.tsx
+++ b/src/components/Player/components/blurred-background.tsx
@@ -1,11 +1,11 @@
import React from 'react'
import { usePlayerContext } from '../../../providers/Player'
-import { BlurView } from 'blur-react-native'
-import ItemImage from '../../Global/components/image'
import { getToken, useTheme, View, YStack, ZStack } from 'tamagui'
import { useColorScheme } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
-
+import { getPrimaryBlurhashFromDto } from '../../../utils/blurhash'
+import { BlurView } from 'blur-react-native'
+import ItemImage from '../../Global/components/image'
export default function BlurredBackground({
width,
height,
@@ -18,24 +18,8 @@ export default function BlurredBackground({
const isDarkMode = useColorScheme() === 'dark'
return (
-
-
+
+
diff --git a/src/components/Player/components/header.tsx b/src/components/Player/components/header.tsx
index e5153586..a47e7878 100644
--- a/src/components/Player/components/header.tsx
+++ b/src/components/Player/components/header.tsx
@@ -1,16 +1,13 @@
import { useJellifyContext } from '../../../providers'
import { usePlayerContext } from '../../../providers/Player'
import { useQueueContext } from '../../../providers/Player/queue'
-import { getToken, useWindowDimensions, XStack, YStack, Spacer, useTheme, View } from 'tamagui'
-import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
-import FastImage from 'react-native-fast-image'
+import { getToken, useWindowDimensions, XStack, YStack, useTheme } from 'tamagui'
import { Text } from '../../Global/helpers/text'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import Icon from '../../Global/components/icon'
import { StackParamList } from '../../types'
import React from 'react'
import { State } from 'react-native-track-player'
-import { Platform } from 'react-native'
import ItemImage from '../../Global/components/image'
export default function PlayerHeader({
@@ -35,7 +32,7 @@ export default function PlayerHeader({
{
navigation.goBack()
}}
diff --git a/src/components/Playlists/component.tsx b/src/components/Playlists/component.tsx
index 6b03786e..e1578f40 100644
--- a/src/components/Playlists/component.tsx
+++ b/src/components/Playlists/component.tsx
@@ -1,7 +1,7 @@
import { FlatList, RefreshControl } from 'react-native-gesture-handler'
import { ItemCard } from '../Global/components/item-card'
import Icon from '../Global/components/icon'
-import { getToken, getTokens } from 'tamagui'
+import { getToken, getTokens, Separator } from 'tamagui'
import { fetchFavoritePlaylists } from '../../api/queries/favorites'
import { QueryKeys } from '../../enums/query-keys'
import { useQuery } from '@tanstack/react-query'
@@ -10,6 +10,8 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { StackParamList } from '../types'
import { useDisplayContext } from '../../providers/Display/display-provider'
import { useLayoutEffect } from 'react'
+import { FlashList } from '@shopify/flash-list'
+import ItemRow from '../Global/components/item-row'
export default function Playlists({
navigation,
@@ -42,31 +44,22 @@ export default function Playlists({
queryFn: () => fetchFavoritePlaylists(api, user, library),
})
- const { numberOfColumns } = useDisplayContext()
-
return (
- }
+ ItemSeparatorComponent={() => }
renderItem={({ index, item: playlist }) => (
- {
navigation.navigate('Playlist', { playlist })
}}
- size={'$11'}
- squared
+ navigation={navigation}
+ queueName={playlist.Name ?? 'Untitled Playlist'}
/>
)}
- removeClippedSubviews
/>
)
}
diff --git a/src/screens/index.tsx b/src/screens/index.tsx
index 03367002..e95fa8fa 100644
--- a/src/screens/index.tsx
+++ b/src/screens/index.tsx
@@ -37,7 +37,6 @@ export default function Root(): React.JSX.Element {
options={{
headerShown: false,
presentation: 'modal',
- sheetAllowedDetents: Platform.OS === 'ios' ? 'fitToContents' : [1.0],
}}
/>
- )
- },
}}
tabBar={(props) => (
<>
{nowPlaying && (
/* Hide miniplayer if the queue is empty */
- <>
-
- >
+
)}
diff --git a/yarn.lock b/yarn.lock
index 42e5be20..b77052da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8376,6 +8376,11 @@ react-native-blob-util@^0.22.2:
base-64 "0.1.0"
glob "^10.3.10"
+react-native-blurhash@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/react-native-blurhash/-/react-native-blurhash-2.1.1.tgz#c8079881422664691b387e43b020b846dfd196e6"
+ integrity sha512-b1aA5Tn31pPbqmaWnhJv7zSuN6o9M1t4yHciPunfP89LDkH2dvDIynvkE00Hen4Vmt6SnyXViSYH34MyvTvRiA==
+
react-native-carplay@^2.4.1-beta.0:
version "2.4.1-beta.0"
resolved "https://registry.yarnpkg.com/react-native-carplay/-/react-native-carplay-2.4.1-beta.0.tgz#985c44e4eae622e7d2cf17c5b996a0182dbf5a52"
@@ -8496,10 +8501,10 @@ react-native-safe-area-context@^5.5.2:
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz#a3c0e99385a45e31ef24d24358aa622cc9e1a069"
integrity sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw==
-react-native-screens@^4.11.1:
- version "4.11.1"
- resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.11.1.tgz#7d0f3d313d8ddc1e55437c5e038f15f8805dc991"
- integrity sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw==
+react-native-screens@^4.12.0:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.12.0.tgz#9ee3da954dbbfcaa6fd69a21e736a2265b73395b"
+ integrity sha512-T2KL6RcDSYDRZswh9glRe600Hvaeq240U21eaqv0uxCNmJz05UeFc4YGQgbFPI8XsakPKx3HjNonItxElFy+QA==
dependencies:
react-freeze "^1.0.0"
react-native-is-edge-to-edge "^1.1.7"