Merge branch 'main' of github.com:anultravioletaurora/Jellify

This commit is contained in:
Violet Caulfield
2025-05-01 17:45:25 -05:00
210 changed files with 1877 additions and 4537 deletions

View File

@@ -16,8 +16,8 @@ jobs:
- name: 💬 Echo package.json version to Github ENV
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
- name: 🧵 Run yarn install
run: yarn install
- name: 🤖 Run yarn init-android
run: yarn init-android
- name: 🚀 Run fastlane build
run: yarn fastlane:android:build

View File

@@ -16,13 +16,10 @@ jobs:
- name: 💬 Echo package.json version to Github ENV
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
- name: 🍎 Run yarn init-ios:new-arch
run: yarn init-ios:new-arch
- name: 🍫 Install CocoaPods
run: yarn pod:install
- name: 🚀 Run fastlane build
run: yarn fastlane:ios:build
env:

View File

@@ -16,24 +16,23 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20
- name: 🍎 Run yarn init-ios:new-arch
run: yarn init-ios:new-arch
- name: Version Up
run: yarn react-native bump-version --type patch
- name: 💬 Echo package.json version to Github ENV
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
- name: 🤫 Output App Store Connect API Key JSON to Fastlane
run: echo -e '${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}' > appstore_connect_api_key.json
working-directory: ./ios/fastlane
- name: 🚀 Run Android fastlane build
run: yarn fastlane:android:build
- name: 🚀 Run iOS fastlane build and publish to TestFlight
run: yarn fastlane:ios:beta
env:

View File

@@ -22,7 +22,10 @@ jobs:
- name: 🍎 Run yarn init-ios:new-arch
run: yarn init-ios:new-arch
- name: 🧪 Run npm test
- name: 🔍 Run yarn tsc
run: yarn tsc
- name: 🧪 Run yarn test
run: yarn test
- name: 🦋 Check Styling

17
App.tsx
View File

@@ -2,21 +2,23 @@ import './gesture-handler'
import React, { useState } from 'react'
import 'react-native-url-polyfill/auto'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import Jellify from './components/jellify'
import Jellify from './src/components/jellify'
import { TamaguiProvider, Theme } from 'tamagui'
import { useColorScheme } from 'react-native'
import jellifyConfig from './tamagui.config'
import { clientPersister } from './constants/storage'
import { queryClient } from './constants/query-client'
import { clientPersister } from './src/constants/storage'
import { queryClient } from './src/constants/query-client'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import TrackPlayer, { IOSCategory, IOSCategoryOptions } from 'react-native-track-player'
import { CAPABILITIES } from './player/constants'
import { CAPABILITIES } from './src/player/constants'
import { createWorkletRuntime } from 'react-native-reanimated'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { NavigationContainer } from '@react-navigation/native'
import { JellifyDarkTheme, JellifyLightTheme } from './components/theme'
import { requestStoragePermission } from './helpers/permisson-helpers'
import ErrorBoundary from './components/ErrorBoundary'
import { JellifyDarkTheme, JellifyLightTheme } from './src/components/theme'
import { requestStoragePermission } from './src/helpers/permisson-helpers'
import ErrorBoundary from './src/components/ErrorBoundary'
import Toast from 'react-native-toast-message'
import JellifyToastConfig from './src/constants/toast.config'
export const backgroundRuntime = createWorkletRuntime('background')
@@ -84,6 +86,7 @@ export default function App(): React.JSX.Element {
</TamaguiProvider>
</GestureHandlerRootView>
</PersistQueryClientProvider>
<Toast config={JellifyToastConfig(isDarkMode)} />
</NavigationContainer>
</ErrorBoundary>
</SafeAreaProvider>

View File

@@ -1,304 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
activesupport (7.2.2.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1044.0)
aws-sdk-core (3.217.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.97.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.179.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
claide (1.1.0)
cocoapods (1.15.2)
addressable (~> 2.8)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.15.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 2.1, < 3.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
cocoapods-search (>= 1.0.0, < 2.0)
cocoapods-trunk (>= 1.6.0, < 2.0)
cocoapods-try (>= 1.1.0, < 2.0)
colored2 (~> 3.1)
escape (~> 0.0.4)
fourflusher (>= 2.3.0, < 3.0)
gh_inspector (~> 1.0)
molinillo (~> 0.8.0)
nap (~> 1.0)
ruby-macho (>= 2.3.0, < 3.0)
xcodeproj (>= 1.23.0, < 2.0)
cocoapods-core (1.15.2)
activesupport (>= 5.0, < 8)
addressable (~> 2.8)
algoliasearch (~> 1.0)
concurrent-ruby (~> 1.1)
fuzzy_match (~> 2.0.4)
nap (~> 1.0)
netrc (~> 0.11)
public_suffix (~> 4.0)
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (2.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
concurrent-ruby (1.3.3)
connection_pool (2.5.0)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
drb (2.2.1)
emoji_regex (3.2.3)
escape (0.0.4)
ethon (0.16.0)
ffi (>= 1.15.0)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.226.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
ffi (1.17.1)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.8.3)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
jmespath (1.6.2)
json (2.9.1)
jwt (2.10.1)
base64
logger (1.6.5)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.4)
molinillo (0.8.0)
multi_json (1.15.0)
multipart-post (2.4.1)
nanaimo (0.3.0)
nap (1.1.0)
naturally (2.2.1)
netrc (0.11.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (4.0.7)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.0)
rouge (3.28.0)
ruby-macho (2.5.1)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
securerandom (0.4.1)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
typhoeus (1.4.1)
ethon (>= 0.9.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.25.1)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
concurrent-ruby (< 1.3.4)
fastlane
xcodeproj (< 1.26.0)
RUBY VERSION
ruby 3.1.4p223
BUNDLED WITH
2.3.26

View File

@@ -2,14 +2,15 @@
![Jellify App Icon](assets/icon_dark_60pt_3x.png)
[![publish-beta](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml/badge.svg?branch=main)](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml)
[![Latest Version](https://img.shields.io/github/package-json/version/anultravioletaurora/jellify?label=Latest%20Version&color=indigo)](https://github.com/anultravioletaurora/Jellify/releases)
[![publish-beta](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml/badge.svg?branch=main)](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml) [![Sponsors](https://img.shields.io/github/sponsors/anultravioletaurora?label=Project%20Sponsors&color=magenta)](https://github.com/sponsors/anultravioletaurora)
## 🔗 Quick Links
[Discord Server](https://discord.gg/yf8fBatktn)
[TestFlight](https://testflight.apple.com/join/etVSc7ZQ)
[![Discord Server](https://dcbadge.limes.pink/api/server/https://discord.gg/yf8fBatktn)](https://discord.gg/yf8fBatktn)
## About
> **jellify** (verb) - _to make gelatinous_ <br>
@@ -114,26 +115,24 @@ Playlist
## 🏗 Built with good stuff
[![Made with React](https://img.shields.io/badge/React-18-blue?logo=react&logoColor=white)](https://reactjs.org) [![Made with TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript&logoColor=white)](https://typescriptlang.org) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![Made with React](https://img.shields.io/badge/React-19-blue?logo=react)](https://reactjs.org) [![React Native](https://img.shields.io/badge/React-Native-079?logo=react)](https://reactnative.dev) [![Made with TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript&logoColor=white)](https://typescriptlang.org) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![GitHub License](https://img.shields.io/github/license/anultravioletaurora/jellify?color=indigo)](https://github.com/anultravioletaurora/jellify/blob/main/LICENSE)
### 🎨 Frontend
[Tamagui](https://tamagui.dev/)\
[Burnt](https://github.com/nandorojo/burnt)\
[React Navigation](https://reactnavigation.org/)\
[React Native CarPlay](https://github.com/birkir/react-native-carplay)\
[React Native Draggable Flatlist](https://github.com/computerjazz/react-native-draggable-flatlist)\
[React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/)\
[React Native Toast Message](https://github.com/calintamas/react-native-toast-message)\
[React Native Vector Icons](https://github.com/oblador/react-native-vector-icons)
- Specifically using [Material Community Icons](https://oblador.github.io/react-native-vector-icons/#MaterialCommunityIcons)
### 🎛️ Backend
[Expo SDK](https://expo.dev/)\
[Jellyfin SDK](https://typescript-sdk.jellyfin.org/)\
[Tanstack Query](https://tanstack.com/query/latest/docs/framework/react/react-native)\
[React Native Boost](https://github.com/kuatsu/react-native-boost)\
[React Native File Access](https://github.com/alpha0010/react-native-file-access)\
[React Native MMKV](https://github.com/mrousavy/react-native-mmkv)\
[React Native Track Player](https://github.com/doublesymmetry/react-native-track-player)\

View File

@@ -96,8 +96,8 @@ android {
applicationId "com.jellify"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 40
versionName "0.11.15"
versionCode 46
versionName "0.11.21"
}
signingConfigs {
debug {

View File

@@ -1,5 +1,5 @@
package com.jellify
import expo.modules.ReactActivityDelegateWrapper
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
@@ -19,5 +19,6 @@ class MainActivity : ReactActivity() {
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled))
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}

View File

@@ -1,7 +1,4 @@
package com.jellify
import android.content.res.Configuration
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
import android.app.Application
import com.facebook.react.PackageList
@@ -18,7 +15,7 @@ import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) {
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
@@ -31,10 +28,10 @@ class MainApplication : Application(), ReactApplication {
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
})
}
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
@@ -43,11 +40,5 @@ class MainApplication : Application(), ReactApplication {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}
}

View File

@@ -1,21 +1,6 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex ->
def command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
}
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'Jellify'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle")
useExpoModules()
includeBuild('../node_modules/@react-native/gradle-plugin')

View File

@@ -1,28 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../enums/query-keys'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { BaseItemKind, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
import Client from '../client'
export const useArtistFeaturedOnAlbums = (artistId: string) =>
useQuery({
queryKey: [QueryKeys.ArtistFeaturedAlbums, artistId],
queryFn: ({ queryKey }) => {
return getItemsApi(Client.api!)
.getItems({
includeItemTypes: [BaseItemKind.MusicAlbum],
recursive: true,
excludeItemIds: [queryKey[1] as string],
sortBy: [
ItemSortBy.PremiereDate,
ItemSortBy.ProductionYear,
ItemSortBy.SortName,
],
sortOrder: [SortOrder.Descending],
contributingArtistIds: [queryKey[1] as string],
})
.then((response) => {
return response.data.Items ? response.data.Items! : []
})
},
})

View File

@@ -1,28 +0,0 @@
import { Dirs, FileSystem } from 'react-native-file-access'
import Client from '../../../api/client'
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api'
export async function downloadTrack(itemId: string): Promise<void> {
// Make sure downloads folder exists, create if it doesn't
if (!(await FileSystem.exists(`${Dirs.DocumentDir}/downloads`)))
await FileSystem.mkdir(`${Dirs.DocumentDir}/downloads`)
getLibraryApi(Client.api!)
.getDownload(
{
itemId,
},
{
responseType: 'blob',
},
)
.then(async (response) => {
if (response.status < 300) {
await FileSystem.writeFile(getTrackFilePath(itemId), response.data)
}
})
}
export function getTrackFilePath(itemId: string) {
return `${Dirs.DocumentDir}/downloads/${itemId}`
}

View File

@@ -1,20 +0,0 @@
import Client from '../../../api/client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { isEmpty } from 'lodash'
export async function fetchItem(itemId: string): Promise<BaseItemDto> {
return new Promise((resolve, reject) => {
if (isEmpty(itemId)) reject('No item ID proviced')
getItemsApi(Client.api!)
.getItems({
ids: [itemId],
})
.then((response) => {
if (response.data.Items && response.data.TotalRecordCount == 1)
resolve(response.data.Items[0])
else reject(`${response.data.TotalRecordCount} items returned for ID`)
})
})
}

View File

@@ -1,7 +1,4 @@
{
"name": "Jellify",
"displayName": "Jellify",
"expo": {
"newArchEnabled": true
}
"displayName": "Jellify"
}

View File

@@ -1,146 +0,0 @@
import { HomeAlbumProps } from '../types'
import { YStack, XStack, Separator, getToken } from 'tamagui'
import { BaseItemDto, ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models'
import { H5, Text } from '../Global/helpers/text'
import { FlatList, SectionList } from 'react-native'
import { RunTimeTicks } from '../Global/helpers/time-codes'
import Track from '../Global/components/track'
import FavoriteButton from '../Global/components/favorite-button'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../enums/query-keys'
import { getImageApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../api/client'
import { ItemCard } from '../Global/components/item-card'
import { Image } from 'expo-image'
import { groupBy, isEqual } from 'lodash'
export function AlbumScreen({ route, navigation }: HomeAlbumProps): React.JSX.Element {
const { album } = route.params
navigation.setOptions({
headerRight: () => {
return <FavoriteButton item={album} />
},
})
const { data: discs } = useQuery({
queryKey: [QueryKeys.ItemTracks, album.Id!],
queryFn: () => {
let sortBy: ItemSortBy[] = []
sortBy = [ItemSortBy.ParentIndexNumber, ItemSortBy.IndexNumber, ItemSortBy.SortName]
return new Promise<{ title: string; data: BaseItemDto[] }[]>((resolve, reject) => {
getItemsApi(Client.api!)
.getItems({
parentId: album.Id!,
sortBy,
})
.then(({ data }) => {
const discs = data.Items
? Object.keys(
groupBy(data.Items, (track) => track.ParentIndexNumber),
).map((discNumber) => {
console.debug(discNumber)
return {
title: discNumber,
data: data.Items!.filter((track: BaseItemDto) =>
track.ParentIndexNumber
? isEqual(
discNumber,
(track.ParentIndexNumber ?? 0).toString(),
)
: track,
),
}
})
: [{ title: '1', data: [] }]
resolve(discs)
})
.catch((error) => {
reject(error)
})
})
},
})
return (
<SectionList
contentInsetAdjustmentBehavior='automatic'
sections={discs ? discs : [{ title: '1', data: [] }]}
keyExtractor={(item, index) => item.Id! + index}
ItemSeparatorComponent={() => <Separator />}
renderSectionHeader={({ section }) => {
return discs && discs.length >= 2 ? (
<Text
paddingVertical={'$2'}
paddingLeft={'$4.5'}
backgroundColor={'$background'}
bold
>{`Disc ${section.title}`}</Text>
) : null
}}
ListHeaderComponent={
<YStack marginTop={'$2'} minHeight={getToken('$20') + getToken('$15')}>
<Image
source={getImageApi(Client.api!).getItemImageUrlById(album.Id!)}
style={{
borderRadius: getToken('$5'),
width: getToken('$20') + getToken('$15'),
height: getToken('$20') + getToken('$15'),
alignSelf: 'center',
}}
/>
<H5 textAlign='center'>{album.Name ?? 'Untitled Album'}</H5>
<XStack justifyContent='space-evenly'>
<Text>{album.ProductionYear?.toString() ?? ''}</Text>
</XStack>
<FlatList
contentContainerStyle={{
marginLeft: 2,
}}
style={{
alignSelf: 'center',
}}
horizontal
keyExtractor={(item) => item.Id!}
data={album.ArtistItems}
renderItem={({ index, item: artist }) => (
<ItemCard
size={'$8'}
item={artist}
caption={artist.Name ?? 'Unknown Artist'}
onPress={() => {
navigation.navigate('Artist', {
artist,
})
}}
/>
)}
/>
</YStack>
}
renderItem={({ item: track, index }) => (
<Track
track={track}
tracklist={discs?.flatMap((disc) => disc.data)}
index={discs?.flatMap((disc) => disc.data).indexOf(track) ?? index}
navigation={navigation}
queue={album}
/>
)}
ListFooterComponent={
<XStack marginRight={'$2'} justifyContent='flex-end'>
<Text color={'$purpleGray'} paddingRight={'$1'}>
Total Runtime:
</Text>
<RunTimeTicks>{album.RunTimeTicks}</RunTimeTicks>
</XStack>
}
/>
)
}

View File

@@ -1,108 +0,0 @@
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { Blurhash } from 'react-native-blurhash'
import { Square, View } from 'tamagui'
import { isEmpty } from 'lodash'
import { Image } from 'react-native'
import { QueryKeys } from '../../../enums/query-keys'
import { useQuery } from '@tanstack/react-query'
import { fetchItemImage } from '../../../api/queries/functions/images'
interface BlurhashLoadingProps {
item: BaseItemDto
width: number
height?: number
type?: ImageType
borderRadius?: number | undefined
}
/**
* @deprecated
*
* Please use the `Image` component from
* the `expo-image` module instead, as that is more performant
*
* A React component that will render a Blurhash
* string as an image while loading the full image
* from the server
*
* Image Query is stale after 30 minutes and collected
* after an hour to keep the cache size down and the
* app performant
*
* TODO: Keep images in offline mode
*
* @param param0
* @returns
*/
export default function BlurhashedImage({
item,
width,
height,
type,
borderRadius,
}: BlurhashLoadingProps): React.JSX.Element {
const { data: image, isSuccess } = useQuery({
queryKey: [
QueryKeys.ItemImage,
item.AlbumId ? item.AlbumId : item.Id!,
type ?? ImageType.Primary,
Math.ceil(width / 100) * 100, // Images are fetched at a higher, generic resolution
Math.ceil(height ?? width / 100) * 100, // So these keys need to match
],
queryFn: () =>
fetchItemImage(
item.AlbumId ? item.AlbumId : item.Id!,
type ?? ImageType.Primary,
width,
height ?? width,
),
staleTime: 1000 * 60 * 30, // 30 minutes
gcTime: 1000 * 60 * 60, // 1 hour
refetchOnMount: false,
refetchOnWindowFocus: false,
})
const blurhash =
!isEmpty(item.ImageBlurHashes) &&
!isEmpty(type ? item.ImageBlurHashes[type] : item.ImageBlurHashes.Primary)
? Object.values(type ? item.ImageBlurHashes[type]! : item.ImageBlurHashes.Primary!)[0]
: undefined
return (
<View
minHeight={height ?? width}
minWidth={width}
borderRadius={borderRadius ? borderRadius : 25}
>
{isSuccess ? (
<Image
source={{
uri: image ?? undefined,
}}
style={{
height: height ?? width,
width,
borderRadius: borderRadius ? borderRadius : 25,
resizeMode: 'contain',
}}
/>
) : blurhash ? (
<Blurhash
blurhash={blurhash!}
style={{
height: height ?? width,
width: width,
borderRadius: borderRadius ? borderRadius : 25,
}}
/>
) : (
<Square
backgroundColor='$amethyst'
width={width}
height={height ?? width}
borderRadius={borderRadius ? borderRadius : 25}
/>
)}
</View>
)
}

View File

@@ -1,35 +0,0 @@
import Client from '../../../api/client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import { Image } from 'expo-image'
import { getToken, getTokenValue, SizeTokens } from 'tamagui'
interface ImageProps {
item: BaseItemDto
circular?: boolean | undefined
width?: SizeTokens | undefined
height?: SizeTokens | undefined
}
export default function ItemImage({
item,
circular,
width,
height,
}: ImageProps): React.JSX.Element {
return (
<Image
source={getImageApi(Client.api!).getItemImageUrlById(item.Id!)}
style={{
borderRadius: circular
? width
? width
: getTokenValue('$12') + getToken('$5')
: getTokenValue('$2'),
width: width ? width : getToken('$12') + getToken('$5'),
height: height ? height : getToken('$12') + getToken('$5'),
alignSelf: 'center',
}}
/>
)
}

View File

@@ -1,7 +0,0 @@
import { ToastViewport } from '@tamagui/toast'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
export default function SafeToastViewport(): React.JSX.Element {
const { left, top, right } = useSafeAreaInsets()
return <ToastViewport flexDirection='column-reverse' top={top} left={left} right={right} />
}

View File

@@ -1,28 +0,0 @@
import { Toast as TamaguiToast, useToastState } from '@tamagui/toast'
import { YStack } from 'tamagui'
export default function Toast(): React.JSX.Element | null {
const currentToast = useToastState()
if (!currentToast || currentToast.isHandledNatively) return null
return (
<TamaguiToast
key={currentToast.id}
duration={currentToast.duration}
enterStyle={{ opacity: 0, scale: 0.5, y: -25 }}
exitStyle={{ opacity: 0, scale: 1, y: -20 }}
y={0}
opacity={1}
scale={1}
animation='200ms'
viewportName={currentToast.viewportName}
>
<YStack>
<TamaguiToast.Title>{currentToast.title}</TamaguiToast.Title>
{!!currentToast.message && (
<TamaguiToast.Description>{currentToast.message}</TamaguiToast.Description>
)}
</YStack>
</TamaguiToast>
)
}

View File

@@ -1,46 +0,0 @@
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { ItemCard } from '../../Global/components/item-card'
import { H2 } from '../../../components/Global/helpers/text'
import { StackParamList } from '../../../components/types'
import React from 'react'
import { FlatList } from 'react-native'
import { View, XStack } from 'tamagui'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserPlaylists } from '../../../api/queries/functions/playlists'
export default function Playlists({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { data: playlists } = useQuery({
queryKey: [QueryKeys.UserPlaylists],
queryFn: () => fetchUserPlaylists(),
})
return (
<View>
<XStack alignContent='center' marginHorizontal={'$2'}>
<H2 textAlign='left'>Your Playlists</H2>
</XStack>
<FlatList
horizontal
data={playlists}
renderItem={({ item: playlist }) => (
<ItemCard
item={playlist}
size={'$11'}
squared
caption={playlist.Name ?? 'Untitled Playlist'}
onPress={() => {
navigation.navigate('Playlist', {
playlist,
})
}}
/>
)}
/>
</View>
)
}

View File

@@ -2,15 +2,14 @@ import 'react-native-gesture-handler'
import { AppRegistry } from 'react-native'
import App from './App'
import { name as appName } from './app.json'
import { PlaybackService } from './player/service'
import { PlaybackService } from './src/player/service'
import TrackPlayer from 'react-native-track-player'
import Client from './api/client'
import Client from './src/api/client'
// Initialize API client instance
/* eslint-disable @typescript-eslint/no-unused-expressions */
Client.instance
Client.instance
console.debug('Created Jellify client')
AppRegistry.registerComponent(appName, () => App)

View File

@@ -1,3 +1,2 @@
#import "RNCarPlay.h"
#import <Expo/Expo.h>
#import <RCTAppDelegate.h>
#import <RCTAppDelegate.h>

View File

@@ -10,7 +10,6 @@
00E356F31AD99517003FC87E /* JellifyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* JellifyTests.m */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
217EBE16A3E8C5FBF476C905 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */; };
66BC9C5D1B536CD0799EEC89 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82DE980BB8253E3C5F2207CE /* ExpoModulesProvider.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
92C580068317633958E4B0F9 /* libPods-Jellify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4912CEDF8E675A9B3515C88E /* libPods-Jellify.a */; };
CF620D0C2CF2BB210045E433 /* Aileron-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */; };
@@ -82,7 +81,6 @@
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = Jellify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
4912CEDF8E675A9B3515C88E /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Jellify/LaunchScreen.storyboard; sourceTree = "<group>"; };
82DE980BB8253E3C5F2207CE /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Jellify/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
A4403789D3D6FBE6706E62B4 /* Pods-Jellify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.release.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.release.xcconfig"; sourceTree = "<group>"; };
ACD0D4797EFB0AA1C5B6FC7D /* Pods-Jellify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.debug.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.debug.xcconfig"; sourceTree = "<group>"; };
CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-Italic.otf"; path = "../assets/fonts/Aileron-Italic.otf"; sourceTree = "<group>"; };
@@ -216,22 +214,6 @@
name = Frameworks;
sourceTree = "<group>";
};
56E1F4734630BBAE3738C238 /* ExpoModulesProviders */ = {
isa = PBXGroup;
children = (
7466A2DFBB2DB18C07F6CEC6 /* Jellify */,
);
name = ExpoModulesProviders;
sourceTree = "<group>";
};
7466A2DFBB2DB18C07F6CEC6 /* Jellify */ = {
isa = PBXGroup;
children = (
82DE980BB8253E3C5F2207CE /* ExpoModulesProvider.swift */,
);
name = Jellify;
sourceTree = "<group>";
};
832341AE1AAA6A7D00B99B32 /* Libraries */ = {
isa = PBXGroup;
children = (
@@ -265,7 +247,6 @@
83CBBA001A601CBA00E9B192 /* Products */,
2D16E6871FA4F8E400B85C8A /* Frameworks */,
BBD78D7AC51CEA395F1C20DB /* Pods */,
56E1F4734630BBAE3738C238 /* ExpoModulesProviders */,
);
indentWidth = 2;
sourceTree = "<group>";
@@ -376,7 +357,6 @@
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Jellify" */;
buildPhases = (
B44EEE05F9243658C0ACC188 /* [CP] Check Pods Manifest.lock */,
41245AA25E9CF87045F16E79 /* [Expo] Configure project */,
13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */,
@@ -545,25 +525,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
41245AA25E9CF87045F16E79 /* [Expo] Configure project */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[Expo] Configure project";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Jellify/expo-configure-project.sh\"\n";
};
B44EEE05F9243658C0ACC188 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -604,7 +565,6 @@
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */,
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */,
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */,
66BC9C5D1B536CD0799EEC89 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -683,7 +643,7 @@
CODE_SIGN_IDENTITY = "Apple Development: Jack Caulfield (66Z9J9NX2X)";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development: Jack Caulfield (66Z9J9NX2X)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 152;
CURRENT_PROJECT_VERSION = 158;
DEVELOPMENT_TEAM = WAH9CZ8BPG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
ENABLE_BITCODE = NO;
@@ -694,7 +654,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.11.15;
MARKETING_VERSION = 0.11.21;
NEW_SETTING = "";
OTHER_LDFLAGS = (
"$(inherited)",
@@ -725,7 +685,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 152;
CURRENT_PROJECT_VERSION = 158;
DEVELOPMENT_TEAM = WAH9CZ8BPG;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -735,7 +695,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.11.15;
MARKETING_VERSION = 0.11.21;
NEW_SETTING = "";
OTHER_LDFLAGS = (
"$(inherited)",

View File

@@ -1,68 +0,0 @@
// ios/AppDelegate.swift
import UIKit
import CarPlay
import React
#if DEBUG
#if FB_SONARKIT_ENABLED
import FlipperKit
#endif
#endif
@main
class AppDelegate: UIResponder, UIApplicationDelegate, RCTBridgeDelegate {
var window: UIWindow?
var bridge: RCTBridge?;
var rootView: RCTRootView?;
static var shared: AppDelegate { return UIApplication.shared.delegate as! AppDelegate }
func sourceURL(for bridge: RCTBridge!) -> URL! {
#if DEBUG
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index");
#else
return Bundle.main.url(forResource:"main", withExtension:"jsbundle")
#endif
}
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
initializeFlipper(with: application)
self.bridge = RCTBridge.init(delegate: self, launchOptions: launchOptions)
self.rootView = RCTRootView.init(bridge: self.bridge!, moduleName: "Jellify", initialProperties: nil)
super.application(application, didFinishLaunchingWithOptions: launchOptions)
return true
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
if (connectingSceneSession.role == UISceneSession.Role.carTemplateApplication) {
let scene = UISceneConfiguration(name: "CarPlay", sessionRole: connectingSceneSession.role)
scene.delegateClass = CarSceneDelegate.self
return scene
} else {
let scene = UISceneConfiguration(name: "Phone", sessionRole: connectingSceneSession.role)
scene.delegateClass = PhoneSceneDelegate.self
return scene
}
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
override func bundleURL() -> URL? {
return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index");
}
private func initializeFlipper(with application: UIApplication) {
#if DEBUG
#if FB_SONARKIT_ENABLED
let client = FlipperClient.shared()
let layoutDescriptorMapper = SKDescriptorMapper(defaults: ())
client?.add(FlipperKitLayoutPlugin(rootNode: application, with: layoutDescriptorMapper!))
client?.add(FKUserDefaultsPlugin(suiteName: nil))
client?.add(FlipperKitReactPlugin())
client?.add(FlipperKitNetworkPlugin(networkAdapter: SKIOSNetworkAdapter()))
client?.start()
#endif
#endif
}
}

View File

@@ -1,14 +0,0 @@
// ios/CarScene.swift
import Foundation
import CarPlay
class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController) {
RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow);
}
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
RNCarPlay.disconnect()
}
}

View File

@@ -4,6 +4,14 @@
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
@@ -17,9 +25,9 @@
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>0A2A.1</string>
<string>3B52.1</string>
<string>C617.1</string>
</array>
</dict>
<dict>
@@ -27,16 +35,8 @@
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
<string>85F4.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
<string>E174.1</string>
</array>
</dict>
</array>

View File

@@ -1,4 +1,3 @@
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
# Resolve react_native_pods.rb with node to allow for hoisting
require Pod::Executable.execute_command('node', ['-p',
'require.resolve(
@@ -6,7 +5,6 @@ require Pod::Executable.execute_command('node', ['-p',
{paths: [process.argv[1]]},
)', __dir__]).strip
puts "RCT_NEW_ARCH_ENABLED = " + ENV['RCT_NEW_ARCH_ENABLED'];
platform :ios, min_ios_version_supported
prepare_react_native_project!
@@ -18,24 +16,7 @@ if linkage != nil
end
target 'Jellify' do
use_expo_modules!
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'ios'
]
end
config = use_native_modules!(config_command)
config = use_native_modules!
use_react_native!(
:path => config[:reactNativePath],

View File

@@ -1,78 +1,6 @@
PODS:
- boost (1.84.0)
- Burnt (0.13.0):
- ExpoModulesCore
- SPAlert (~> 4.2)
- SPIndicator (~> 1.6)
- DoubleConversion (1.1.6)
- EXConstants (17.1.3):
- ExpoModulesCore
- Expo (53.0.1):
- DoubleConversion
- ExpoModulesCore
- 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-RCTAppDelegate
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactAppDependencyProvider
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- ExpoAsset (11.1.3):
- ExpoModulesCore
- ExpoFileSystem (18.1.7):
- ExpoModulesCore
- ExpoFont (13.2.2):
- ExpoModulesCore
- ExpoImage (2.0.7):
- ExpoModulesCore
- libavif/libdav1d
- SDWebImage (~> 5.19.1)
- SDWebImageAVIFCoder (~> 0.11.0)
- SDWebImageSVGCoder (~> 1.7.0)
- ExpoKeepAwake (14.1.3):
- ExpoModulesCore
- ExpoModulesCore (2.3.11):
- 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-jsinspector
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- fast_float (6.1.4)
- FBLazyVector (0.79.1)
- fmt (11.0.2)
@@ -80,11 +8,18 @@ PODS:
- hermes-engine (0.79.1):
- hermes-engine/Pre-built (= 0.79.1)
- hermes-engine/Pre-built (0.79.1)
- libavif/core (0.11.1)
- libavif/libdav1d (0.11.1):
- libavif/core
- libdav1d (>= 0.6.0)
- libdav1d (1.2.0)
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- RCT-Folly (2024.11.18.00):
- boost
- DoubleConversion
@@ -1411,7 +1346,7 @@ PODS:
- ReactCommon/turbomodule/core
- react-native-background-actions (4.0.1):
- React-Core
- react-native-blurhash (2.1.1):
- react-native-blur (4.4.1):
- DoubleConversion
- glog
- hermes-engine
@@ -1913,6 +1848,10 @@ PODS:
- Yoga
- RNDeviceInfo (14.0.4):
- React-Core
- RNFastImage (8.6.3):
- React-Core
- SDWebImage (~> 5.11.1)
- SDWebImageWebPCoder (~> 0.8.4)
- RNFS (2.20.0):
- React-Core
- RNGestureHandler (2.25.0):
@@ -2162,32 +2101,19 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- SDWebImage (5.19.7):
- SDWebImage/Core (= 5.19.7)
- SDWebImage/Core (5.19.7)
- SDWebImageAVIFCoder (0.11.0):
- libavif/core (>= 0.11.0)
- SDWebImage (~> 5.10)
- SDWebImageSVGCoder (1.7.0):
- SDWebImage/Core (~> 5.6)
- SDWebImage (5.11.1):
- SDWebImage/Core (= 5.11.1)
- SDWebImage/Core (5.11.1)
- SDWebImageWebPCoder (0.8.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.7.1)
- SPAlert (4.2.0)
- SPIndicator (1.6.4)
- SwiftAudioEx (1.1.0)
- Yoga (0.0.0)
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- Burnt (from `../node_modules/burnt/ios`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXConstants (from `../node_modules/expo-constants/ios`)
- Expo (from `../node_modules/expo`)
- ExpoAsset (from `../node_modules/expo-asset/ios`)
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
- ExpoFont (from `../node_modules/expo-font/ios`)
- ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
- fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
@@ -2227,7 +2153,7 @@ DEPENDENCIES:
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
- React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-background-actions (from `../node_modules/react-native-background-actions`)
- react-native-blurhash (from `../node_modules/react-native-blurhash`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-carplay (from `../node_modules/react-native-carplay`)
- react-native-mmkv (from `../node_modules/react-native-mmkv`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
@@ -2267,6 +2193,7 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- RNFastImage (from `../node_modules/react-native-fast-image`)
- RNFS (from `../node_modules/react-native-fs`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
@@ -2277,39 +2204,17 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- libavif
- libdav1d
- libwebp
- SDWebImage
- SDWebImageAVIFCoder
- SDWebImageSVGCoder
- SDWebImageWebPCoder
- SocketRocket
- SPAlert
- SPIndicator
- SwiftAudioEx
EXTERNAL SOURCES:
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
Burnt:
:path: "../node_modules/burnt/ios"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EXConstants:
:path: "../node_modules/expo-constants/ios"
Expo:
:path: "../node_modules/expo"
ExpoAsset:
:path: "../node_modules/expo-asset/ios"
ExpoFileSystem:
:path: "../node_modules/expo-file-system/ios"
ExpoFont:
:path: "../node_modules/expo-font/ios"
ExpoImage:
:path: "../node_modules/expo-image/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
ExpoModulesCore:
:path: "../node_modules/expo-modules-core"
fast_float:
:podspec: "../node_modules/react-native/third-party-podspecs/fast_float.podspec"
FBLazyVector:
@@ -2385,8 +2290,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks"
react-native-background-actions:
:path: "../node_modules/react-native-background-actions"
react-native-blurhash:
:path: "../node_modules/react-native-blurhash"
react-native-blur:
:path: "../node_modules/@react-native-community/blur"
react-native-carplay:
:path: "../node_modules/react-native-carplay"
react-native-mmkv:
@@ -2465,6 +2370,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view"
RNDeviceInfo:
:path: "../node_modules/react-native-device-info"
RNFastImage:
:path: "../node_modules/react-native-fast-image"
RNFS:
:path: "../node_modules/react-native-fs"
RNGestureHandler:
@@ -2482,23 +2389,13 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
Burnt: 0616363a206ed96ba9af3e2eb0e184dc97be1458
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
EXConstants: 01a258d03504c87ef015dfd0fe8d92a7fc1d8345
Expo: 6dd724d3322082adfa13b092b3729709cb590155
ExpoAsset: c97af8e75cd5165aa0c48266eb1bb8d2a57e5caa
ExpoFileSystem: 175267faf2b38511b01ac110243b13754dac57d3
ExpoFont: ab96f88d5e959232152c8dcecafd8ba1235c5763
ExpoImage: d840b256050f4428d2942bc2b6e9251f9e0d7021
ExpoKeepAwake: 213acedecafb6fda8c0ffedad22ee9e2903400c5
ExpoModulesCore: f98b254540e59e7eabc9628ba636789959295ead
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: abbac80c6f89e71a8c55c7e92ec015c8a9496753
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: c32f2e405098bc1ebe30630a051ddce6f21d3c2e
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
RCTDeprecation: 0ada4fb1e5c5637bff940dc40b94e2d3bf96b0ab
RCTRequired: 76ca80ff10acb3834ed0dacba9645645009578a2
@@ -2531,7 +2428,7 @@ SPEC CHECKSUMS:
React-Mapbuffer: 8df5296f9d9a61f980d293b55026cfebcd8dfb0a
React-microtasksnativemodule: c8ed30f8ec30affbc73411c54207bd67b1125bbb
react-native-background-actions: 48e6bad9e2a47e3b04858634c5a05ea11062f680
react-native-blurhash: 773f3726f5932f05917a3f4828648f155f087c7d
react-native-blur: 06d0f9906ecd6cde3a42de16c6cd829a2bf0710c
react-native-carplay: 8f388f6f73e5e0f73ed154ad8794371343ee20c0
react-native-mmkv: d3cc73d2554fafa20dc5b86386359034d1faf8ff
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
@@ -2571,21 +2468,19 @@ SPEC CHECKSUMS:
ReactCommon: aa48e4fddbc6a0afa19dca39a1b6016c150b5db4
RNCMaskedView: ae521efb1c6c2b183ae0f8479487db03c826184c
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
RNGestureHandler: ebef699ea17e7c0006c1074e1e423ead60ce0121
RNReactNativeHapticFeedback: 851adf794e1fcdc0664d80820fa3272ee8a6a538
RNReanimated: 2313402fe27fecb7237619e9c6fcee3177f08a65
RNScreens: 5e0027417985f7b4619410f7fcbd391fa157cc71
RNVectorIcons: 941a39b5d3b9d8cf8ac2e2fc09b07bfafbcf9796
SDWebImage: 8a6b7b160b4d710e2a22b6900e25301075c34cb3
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SPAlert: 735da1f16a887e294719217572ce1f936d8c8782
SPIndicator: 93e0a4fb23de51294ac48e874c0f081a5e293e4f
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
Yoga: d15f5aa644c466e917569ac43b19cbf17975239a
PODFILE CHECKSUM: 8a165b08a274fa7a2ef8008684d2fa318fd691f2
PODFILE CHECKSUM: 6c43107ef8f93930ea4053b68b45c8912648b48c
COCOAPODS: 1.16.2

View File

@@ -1,16 +1,16 @@
// https://docs.swmansion.com/react-native-gesture-handler/docs/guides/testing
module.exports = {
preset: 'jest-expo',
preset: 'react-native',
setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'],
setupFilesAfterEnv: [
'./jest/setup.ts',
'./jest/setup-carplay.ts',
'./jest/setup-blurhash.ts',
'./jest/setup-device-info.js', // JS to prevent Typescript implicit any warning
'./jest/setup-reanimated.ts',
'./jest/setup-rnfs.ts',
'./jest/setup-rntp.ts',
'./tamagui.config.ts',
'./jest/setup-native-modules.ts',
],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
transformIgnorePatterns: [

View File

@@ -3,8 +3,8 @@ import React from 'react'
import { render } from '@testing-library/react-native'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueueProvider } from '../player/queue-provider'
import { PlayerProvider } from '../player/player-provider'
import { QueueProvider } from '../src/player/queue-provider'
import { PlayerProvider } from '../src/player/player-provider'
import { View } from 'react-native'
const queryClient = new QueryClient()

View File

@@ -1,11 +1,11 @@
import 'react-native'
import React from 'react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react-native'
import TrackPlayer, { Event } from 'react-native-track-player'
import { act, render, screen, waitFor } from '@testing-library/react-native'
import { Event } from 'react-native-track-player'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Button, Text } from 'react-native'
import { QueueProvider, useQueueContext } from '../player/queue-provider'
import { QueueProvider, useQueueContext } from '../src/player/queue-provider'
import { eventHandler } from './setup-rntp'
const queryClient = new QueryClient()

View File

@@ -1,5 +0,0 @@
jest.mock('react-native-blurhash', () => {
return {
Blurhash: () => null,
}
})

View File

@@ -1,17 +1,29 @@
import {
ListTemplateConfig,
NowPlayingTemplateConfig,
TabBarTemplateConfig,
} from 'react-native-carplay'
jest.mock('react-native-carplay', () => {
return {
ListTemplate: class {
constructor(config) {
config: ListTemplateConfig
constructor(config: ListTemplateConfig) {
this.config = config
}
},
NowPlayingTemplate: class {
constructor(config) {
config: NowPlayingTemplateConfig
constructor(config: NowPlayingTemplateConfig) {
this.config = config
}
},
TabBarTemplate: class {
constructor(config) {
config: TabBarTemplateConfig
constructor(config: TabBarTemplateConfig) {
this.config = config
}
},

View File

@@ -0,0 +1,5 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock'
jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo)

View File

@@ -1,4 +1,4 @@
jest.mock('../api/client')
jest.mock('../src/api/client')
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
@@ -9,11 +9,3 @@ jest.mock('react-native-haptic-feedback', () => {
},
}
})
jest.mock('burnt', () => {
return {
default: {
alert: jest.fn(),
},
}
})

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// Learn more https://docs.expo.io/guides/customizing-metro
const { wrapWithReanimatedMetroConfig } = require('react-native-reanimated/metro-config')
@@ -12,6 +13,6 @@ const config = getDefaultConfig(__dirname, {
// https://github.com/expo/expo/issues/23180
config.resolver.sourceExts.push('mjs')
config.watchFolders = ['components', 'api', 'player']
config.watchFolders = ['src']
module.exports = config
module.exports = wrapWithReanimatedMetroConfig(config)

View File

@@ -1,120 +1,120 @@
{
"name": "jellify",
"version": "0.11.15",
"private": true,
"scripts": {
"init-ios": "echo 'Please run `yarn init-ios:new-arch` to enable the new architecture'",
"init-ios:new-arch": "yarn install && yarn run pod:install:new-arch",
"reinstall": "rm -rf ./node_modules && yarn install",
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=0 bundle exec pod install",
"pod:install:new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"pod:clean": "cd ios && pod deintegrate",
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:match": "cd ios && bundle exec fastlane match development",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
"prepare": "husky",
"format:check": "prettier --check .",
"format": "prettier --write .",
"postinstall": "patch-package"
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",
"@react-native-community/cli": "^18.0.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/material-top-tabs": "^7.2.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10",
"@tamagui/config": "^1.126.4",
"@tamagui/toast": "^1.126.4",
"@tanstack/query-sync-storage-persister": "^5.74.6",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-persist-client": "^5.74.6",
"@testing-library/react-native": "^13.2.0",
"axios": "^1.8.4",
"bundle": "^2.1.0",
"burnt": "^0.13.0",
"expo": "^53.0.1",
"expo-image": "^2.0.7",
"gem": "^2.4.3",
"invert-color": "^2.0.0",
"jest-expo": "^53.0.1",
"lodash": "^4.17.21",
"react": "19.0.0",
"react-freeze": "^1.0.4",
"react-native": "0.79.1",
"react-native-background-actions": "^4.0.1",
"react-native-blurhash": "^2.1.1",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-device-info": "^14.0.4",
"react-native-draggable-flatlist": "^4.0.2",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-mmkv": "^3.2.0",
"react-native-pager-view": "^6.7.1",
"react-native-reanimated": "^3.17.5",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.11.0-beta.2",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-track-player": "4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.126.4"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.1",
"@react-native/eslint-config": "0.79.1",
"@react-native/metro-config": "0.79.1",
"@react-native/typescript-config": "0.79.1",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"@types/react": "^19.1.2",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "19.0.0",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.6.3",
"jscodeshift": "^0.15.2",
"lint-staged": "^15.5.0",
"patch-package": "8.0.0",
"prettier": "^2.8.8",
"react-native-cli-bump-version": "^1.5.1",
"react-test-renderer": "19.0.0",
"typescript": "5.8.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"engines": {
"node": ">=18"
}
}
"name": "jellify",
"version": "0.11.21",
"private": true,
"scripts": {
"init-android": "yarn",
"init-ios": "yarn init-ios:new-arch",
"init-ios:new-arch": "yarn && yarn pod:install:new-arch",
"reinstall": "rm -rf ./node_modules && yarn install",
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest",
"tsc": "tsc",
"clean:ios": "cd ios && pod deintegrate",
"clean:android": "cd android && rm -rf app/ build/",
"pod:install": "echo 'Please run `yarn pod:install:new-arch` to enable the new architecture'",
"pod:install:new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
"pod:clean": "cd ios && pod deintegrate",
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
"fastlane:ios:match": "cd ios && bundle exec fastlane match development",
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
"prepare": "husky",
"format:check": "prettier --check .",
"format": "prettier --write .",
"postinstall": "patch-package"
},
"dependencies": {
"@jellyfin/sdk": "^0.11.0",
"@react-native-community/blur": "^4.4.1",
"@react-native-community/cli": "^18.0.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-masked-view/masked-view": "^0.3.2",
"@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/material-top-tabs": "^7.2.10",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@react-navigation/stack": "^7.2.10",
"@tamagui/config": "^1.126.4",
"@tanstack/query-sync-storage-persister": "^5.74.6",
"@tanstack/react-query": "^5.74.4",
"@tanstack/react-query-persist-client": "^5.74.6",
"@testing-library/react-native": "^13.2.0",
"axios": "^1.8.4",
"bundle": "^2.1.0",
"gem": "^2.4.3",
"invert-color": "^2.0.0",
"lodash": "^4.17.21",
"react": "19.0.0",
"react-freeze": "^1.0.4",
"react-native": "0.79.1",
"react-native-background-actions": "^4.0.1",
"react-native-carplay": "^2.4.1-beta.0",
"react-native-device-info": "^14.0.4",
"react-native-draggable-flatlist": "^4.0.2",
"react-native-fast-image": "^8.6.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-haptic-feedback": "^2.3.3",
"react-native-mmkv": "3.2.0",
"react-native-pager-view": "^6.7.1",
"react-native-reanimated": "^3.17.5",
"react-native-safe-area-context": "^5.4.0",
"react-native-screens": "^4.11.0-beta.2",
"react-native-swipeable-item": "^2.0.9",
"react-native-text-ticker": "^1.14.0",
"react-native-toast-message": "^2.3.0",
"react-native-track-player": "git+https://github.com/riteshshukla04/react-native-track-player#APM",
"react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.3",
"react-native-vector-icons": "^10.2.0",
"ruby": "^0.6.1",
"tamagui": "^1.126.4"
},
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/runtime": "^7.27.1",
"@react-native-community/cli-platform-android": "18.0.0",
"@react-native-community/cli-platform-ios": "18.0.0",
"@react-native/babel-preset": "0.79.1",
"@react-native/eslint-config": "0.79.1",
"@react-native/metro-config": "0.79.1",
"@react-native/typescript-config": "0.79.1",
"@types/jest": "^29.5.13",
"@types/lodash": "^4.17.10",
"@types/react": "^19.1.2",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "19.0.0",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-native": "^5.0.0",
"husky": "^9.1.7",
"jest": "^29.6.3",
"jscodeshift": "^0.15.2",
"lint-staged": "^15.5.1",
"patch-package": "8.0.0",
"prettier": "^3.5.3",
"react-dom": "^19.0.0",
"react-native-cli-bump-version": "^1.5.1",
"react-test-renderer": "19.0.0",
"typescript": "5.8.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --fix"
]
},
"engines": {
"node": ">=18"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +0,0 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
export type Queue = BaseItemDto | 'Recently Played' | 'Search' | 'Favorite Tracks' | 'On Repeat'

View File

@@ -1,6 +1,6 @@
import { Jellyfin } from '@jellyfin/sdk'
import { getModel, getUniqueIdSync } from 'react-native-device-info'
import { name, version } from '../package.json'
import { name, version } from '../../package.json'
import { capitalize } from 'lodash'
console.debug(`Building Jellyfin Info`)

View File

@@ -1,4 +1,4 @@
import Client from '../../../api/client'
import Client from '../client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { isUndefined } from 'lodash'

View File

@@ -1,5 +1,5 @@
import { BaseItemDto, MediaType } from '@jellyfin/sdk/lib/generated-client/models'
import Client from '../../../api/client'
import Client from '../client'
import { getLibraryApi, getPlaylistsApi } from '@jellyfin/sdk/lib/utils/api'
export async function addToPlaylist(track: BaseItemDto, playlist: BaseItemDto) {

View File

@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../enums/query-keys'
import { createApi } from './queries/functions/api'
import { createApi } from './queries/api'
export const useApi = (
serverUrl?: string,

View File

@@ -1,5 +1,5 @@
import { Api } from '@jellyfin/sdk'
import { JellyfinInfo } from '../../info'
import { JellyfinInfo } from '../info'
import _ from 'lodash'
export function createApi(

View File

@@ -1,4 +1,4 @@
import Client from '../../client'
import Client from '../client'
import {
BaseItemDto,
BaseItemKind,

View File

@@ -1,4 +1,4 @@
import Client from '../../../api/client'
import Client from '../client'
import {
BaseItemDto,
BaseItemKind,
@@ -6,7 +6,6 @@ import {
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { reject } from 'lodash'
export function fetchFrequentlyPlayed(): Promise<BaseItemDto[]> {
return new Promise((resolve, reject) => {

View File

@@ -1,7 +1,7 @@
import { ImageFormat, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import _ from 'lodash'
import Client from '../../../api/client'
import Client from '../client'
export async function fetchItemImage(
itemId: string,

View File

@@ -0,0 +1,31 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getInstantMixApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../client'
import { isUndefined } from 'lodash'
import QueryConfig from './query.config'
/**
* Fetches an instant mix for a given item
* @param item The item to fetch an instant mix for
* @returns A promise of a {@link BaseItemDto} array, be it empty or not
*/
export function fetchInstantMixFromItem(item: BaseItemDto): Promise<BaseItemDto[]> {
return new Promise((resolve, reject) => {
if (isUndefined(Client.api)) return reject(new Error('Client not initialized'))
getInstantMixApi(Client.api)
.getInstantMixFromArtists({
itemId: item.Id!,
userId: Client.user!.id,
limit: QueryConfig.limits.instantMix,
})
.then(({ data }) => {
if (data.Items) return resolve(data.Items)
return resolve([])
})
.catch((error) => {
console.error(error)
return reject(error)
})
})
}

65
src/api/queries/item.ts Normal file
View File

@@ -0,0 +1,65 @@
import Client from '../client'
import { BaseItemDto, ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { groupBy, isEmpty, isEqual, isUndefined } from 'lodash'
export async function fetchItem(itemId: string): Promise<BaseItemDto> {
return new Promise((resolve, reject) => {
if (isEmpty(itemId)) reject('No item ID proviced')
getItemsApi(Client.api!)
.getItems({
ids: [itemId],
})
.then((response) => {
if (response.data.Items && response.data.TotalRecordCount == 1)
resolve(response.data.Items[0])
else reject(`${response.data.TotalRecordCount} items returned for ID`)
})
})
}
export async function fetchAlbumDiscs(
album: BaseItemDto,
): Promise<{ title: string; data: BaseItemDto[] }[]> {
return new Promise<{ title: string; data: BaseItemDto[] }[]>((resolve, reject) => {
if (isEmpty(album.Id)) reject('No album ID provided')
if (isUndefined(Client.api)) reject('Client not initialized')
let sortBy: ItemSortBy[] = []
sortBy = [ItemSortBy.ParentIndexNumber, ItemSortBy.IndexNumber, ItemSortBy.SortName]
getItemsApi(Client.api!)
.getItems({
parentId: album.Id!,
sortBy,
})
.then(({ data }) => {
const discs = data.Items
? Object.keys(groupBy(data.Items, (track) => track.ParentIndexNumber)).map(
(discNumber) => {
console.debug(discNumber)
return {
title: discNumber,
data: data.Items!.filter((track: BaseItemDto) =>
track.ParentIndexNumber
? isEqual(
discNumber,
(track.ParentIndexNumber ?? 0).toString(),
)
: track,
),
}
},
)
: [{ title: '1', data: [] }]
resolve(discs)
})
.catch((error) => {
reject(error)
})
})
}

View File

@@ -1,4 +1,4 @@
import Client from '../../client'
import Client from '../client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'

View File

@@ -1,5 +1,5 @@
import { PlaybackInfoResponse } from '@jellyfin/sdk/lib/generated-client/models'
import Client from '../../../api/client'
import Client from '../client'
import { getAudioApi, getMediaInfoApi } from '@jellyfin/sdk/lib/utils/api'
export async function fetchMediaInfo(itemId: string): Promise<PlaybackInfoResponse> {

View File

@@ -1,4 +1,4 @@
import Client from '../../client'
import Client from '../client'
import { BaseItemDto, ItemSortBy, SortOrder } from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'

View File

@@ -1,9 +1,11 @@
import { ImageFormat } from '@jellyfin/sdk/lib/generated-client/models'
export const QueryConfig = {
const QueryConfig = {
limits: {
recents: 20,
recents: 50,
instantMix: 50,
search: 50, // TODO: make this a paginated search so limits don't even matter
similar: 20,
},
images: {
height: 300,
@@ -31,3 +33,5 @@ export const QueryConfig = {
oneFortnight: 1000 * 60 * 60 * 24 * 7 * 14, // 14 Days
},
}
export default QueryConfig

View File

@@ -5,8 +5,8 @@ import {
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
import { QueryConfig } from '../query.config'
import Client from '../../client'
import QueryConfig from './query.config'
import Client from '../client'
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
export async function fetchRecentlyAdded(
@@ -28,6 +28,12 @@ export async function fetchRecentlyAdded(
})
}
/**
* Fetches recently played tracks for a user from the Jellyfin server.
* @param limit The number of items to fetch. Defaults to 50
* @param offset The offset of the items to fetch.
* @returns The recently played items.
*/
export async function fetchRecentlyPlayed(
limit: number = QueryConfig.limits.recents,
offset?: number | undefined,
@@ -56,6 +62,13 @@ export async function fetchRecentlyPlayed(
})
}
/**
* Fetches recently played artists for a user from the Jellyfin server,
* referencing the recently played tracks.
* @param limit The number of items to fetch. Defaults to 50
* @param offset The offset of the items to fetch.
* @returns The recently played artists.
*/
export function fetchRecentlyPlayedArtists(
limit: number = QueryConfig.limits.recents,
offset?: number | undefined,

View File

@@ -1,8 +1,8 @@
import Client from '../../../api/client'
import Client from '../client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { isEmpty, trim } from 'lodash'
import { QueryConfig } from '../query.config'
import QueryConfig from './query.config'
/**
* Performs a search for items against the Jellyfin server, trimming whitespace

View File

@@ -1,10 +1,10 @@
import Client from '../../../api/client'
import Client from '../client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api'
import QueryConfig from './query.config'
export default function fetchSimilar(
itemId: string,
limit: number = 10,
limit: number = QueryConfig.limits.similar,
startIndex: number = 0,
): Promise<BaseItemDto[]> {
return new Promise((resolve, reject) => {

View File

@@ -1,5 +1,5 @@
import { getItemsApi, getSuggestionsApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../../api/client'
import Client from '../client'
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
export async function fetchSearchSuggestions(): Promise<BaseItemDto[]> {

View File

@@ -0,0 +1,183 @@
import { HomeAlbumProps, StackParamList } from '../types'
import { YStack, XStack, Separator, getToken, Spacer } from 'tamagui'
import { H5, Text } from '../Global/helpers/text'
import { FlatList, SectionList, useWindowDimensions } from 'react-native'
import { RunTimeTicks } from '../Global/helpers/time-codes'
import Track from '../Global/components/track'
import FavoriteButton from '../Global/components/favorite-button'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../enums/query-keys'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../api/client'
import { ItemCard } from '../Global/components/item-card'
import { fetchAlbumDiscs } from '../../api/queries/item'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import InstantMixButton from '../Global/components/instant-mix-button'
import FastImage from 'react-native-fast-image'
import ItemImage from '../Global/components/image'
import React from 'react'
/**
* The screen for an Album's track list
*
* @param route The route object from the parent screen,
* containing the {@link BaseItemDto} of the album to display in the params
*
* @param navigation The navigation object from the parent screen
*
* @returns A React component
*/
export function AlbumScreen({ route, navigation }: HomeAlbumProps): React.JSX.Element {
const { album } = route.params
const { data: discs } = useQuery({
queryKey: [QueryKeys.ItemTracks, album.Id!],
queryFn: () => fetchAlbumDiscs(album),
})
return (
<SectionList
contentInsetAdjustmentBehavior='automatic'
sections={discs ? discs : [{ title: '1', data: [] }]}
keyExtractor={(item, index) => item.Id! + index}
ItemSeparatorComponent={() => <Separator />}
renderSectionHeader={({ section }) => {
return discs && discs.length >= 2 ? (
<Text
paddingVertical={'$2'}
paddingLeft={'$4.5'}
backgroundColor={'$background'}
bold
>{`Disc ${section.title}`}</Text>
) : null
}}
ListHeaderComponent={() => AlbumTrackListHeader(album, navigation)}
renderItem={({ item: track, index }) => (
<Track
track={track}
tracklist={discs?.flatMap((disc) => disc.data)}
index={discs?.flatMap((disc) => disc.data).indexOf(track) ?? index}
navigation={navigation}
queue={album}
/>
)}
ListFooterComponent={() => AlbumTrackListFooter(album, navigation)}
/>
)
}
/**
* Renders a header for an Album's track list
* @param album The {@link BaseItemDto} of the album to render the header for
* @param navigation The navigation object from the parent {@link AlbumScreen}
* @returns A React component
*/
function AlbumTrackListHeader(
album: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>,
): React.JSX.Element {
const { width } = useWindowDimensions()
return (
<YStack marginTop={'$4'} alignItems='center'>
<XStack justifyContent='center'>
<ItemImage item={album} width={'$20'} height={'$20'} />
<Spacer />
<YStack alignContent='center' justifyContent='center'>
<H5
lineBreakStrategyIOS='standard'
textAlign='center'
numberOfLines={5}
minWidth={width / 2.25}
maxWidth={width / 2.25}
>
{album.Name ?? 'Untitled Album'}
</H5>
<XStack justify='center' marginVertical={'$2'}>
<YStack flex={1}>
{album.ProductionYear ? (
<Text display='block' textAlign='right'>
{album.ProductionYear?.toString() ?? 'Unknown Year'}
</Text>
) : null}
</YStack>
<Separator vertical marginHorizontal={'$3'} />
<YStack flex={1}>
<RunTimeTicks>{album.RunTimeTicks}</RunTimeTicks>
</YStack>
</XStack>
<XStack justifyContent='center' marginVertical={'$2'}>
<FavoriteButton item={album} />
<Spacer />
<InstantMixButton item={album} navigation={navigation} />
</XStack>
</YStack>
</XStack>
<FlatList
contentContainerStyle={{
marginTop: getToken('$4'),
}}
style={{
alignSelf: 'center',
}}
horizontal
keyExtractor={(item) => item.Id!}
data={album.AlbumArtists}
renderItem={({ index, item: artist }) => (
<ItemCard
size={'$10'}
item={artist}
caption={artist.Name ?? 'Unknown Artist'}
onPress={() => {
navigation.navigate('Artist', {
artist,
})
}}
/>
)}
/>
</YStack>
)
}
function AlbumTrackListFooter(
album: BaseItemDto,
navigation: NativeStackNavigationProp<StackParamList>,
): React.JSX.Element {
return (
<YStack marginLeft={'$2'}>
{album.ArtistItems && album.ArtistItems.length > 1 && (
<>
<H5>Featuring</H5>
<FlatList
data={album.ArtistItems}
horizontal
renderItem={({ item: artist }) => (
<ItemCard
size={'$8'}
item={artist}
caption={artist.Name ?? 'Unknown Artist'}
onPress={() => {
navigation.navigate('Artist', {
artist,
})
}}
/>
)}
/>
</>
)}
</YStack>
)
}

View File

@@ -3,7 +3,7 @@ import { ItemCard } from '../Global/components/item-card'
import { FlatList, RefreshControl } from 'react-native'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../enums/query-keys'
import { fetchFavoriteAlbums } from '../../api/queries/functions/favorites'
import { fetchFavoriteAlbums } from '../../api/queries/favorites'
export default function Albums({ navigation, route }: AlbumsProps): React.JSX.Element {
const {

View File

@@ -41,7 +41,7 @@ export default function Albums({
convertRunTimeTicksToSeconds(album.RunTimeTicks ?? 0) /
60 <=
30)),
)
)
: []
}
numColumns={2} // TODO: Make this adjustable

View File

@@ -6,7 +6,7 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { YStack } from 'tamagui'
import Albums from './albums'
import SimilarArtists from './similar'
import { Image } from 'expo-image'
import FastImage from 'react-native-fast-image'
import {
createMaterialTopTabNavigator,
MaterialTopTabBar,
@@ -37,11 +37,13 @@ export default function ArtistNavigation(): React.JSX.Element {
tabBar={(props) => (
<>
<Animated.View style={[animatedBannerStyle]}>
<Image
source={getImageApi(Client.api!).getItemImageUrlById(
artist.Id!,
ImageType.Backdrop,
)}
<FastImage
source={{
uri: getImageApi(Client.api!).getItemImageUrlById(
artist.Id!,
ImageType.Backdrop,
),
}}
style={{ width: width, height: '100%' }}
/>
</Animated.View>

View File

@@ -1,4 +1,4 @@
import fetchSimilar from '../../api/queries/functions/similar'
import fetchSimilar from '../../api/queries/similar'
import Client from '../../api/client'
import { QueryKeys } from '../../enums/query-keys'
import {

View File

@@ -3,7 +3,7 @@ import { ItemCard } from '../Global/components/item-card'
import { ArtistsProps } from '../types'
import { QueryKeys } from '../../enums/query-keys'
import { useQuery } from '@tanstack/react-query'
import { fetchFavoriteArtists } from '../../api/queries/functions/favorites'
import { fetchFavoriteArtists } from '../../api/queries/favorites'
import { YStack } from 'tamagui'
import { Text } from '../Global/helpers/text'
import { FlatList } from 'react-native'

View File

@@ -4,6 +4,8 @@ import { CarPlay, ListTemplate } from 'react-native-carplay'
import TrackPlayer from 'react-native-track-player'
import uuid from 'react-native-uuid'
import CarPlayNowPlaying from './NowPlaying'
import { queryClient } from '../../constants/query-client'
import { QueryKeys } from '../../enums/query-keys'
const RecentTracksTemplate = (items: BaseItemDto[]) =>
new ListTemplate({
@@ -20,13 +22,17 @@ const RecentTracksTemplate = (items: BaseItemDto[]) =>
},
],
onItemSelect: async (item) => {
await TrackPlayer.setQueue(items.map((item) => mapDtoToTrack(item)))
await TrackPlayer.setQueue(
items.map((item) =>
mapDtoToTrack(item, queryClient.getQueryData([QueryKeys.AudioCache]) ?? []),
),
)
await TrackPlayer.skip(item.index)
await TrackPlayer.play()
CarPlay.pushTemplate(CarPlayNowPlaying())
CarPlay.pushTemplate(CarPlayNowPlaying)
},
})

View File

@@ -30,12 +30,9 @@ export default function RecentlyAdded({
<HorizontalCardList
squared
data={recentlyAdded?.length ?? 0 > 10 ? recentlyAdded!.slice(0, 10) : recentlyAdded}
onSeeMore={() => {
navigation.navigate('Albums', {
albums: recentlyAdded,
})
}}
data={
(recentlyAdded?.length ?? 0 > 10) ? recentlyAdded!.slice(0, 10) : recentlyAdded
}
renderItem={({ item }) => (
<ItemCard
caption={item.Name}

View File

@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { fetchRecentlyAdded } from '../../api/queries/functions/recents'
import { fetchRecentlyAdded, fetchRecentlyPlayed } from '../../api/queries/recents'
import { QueryKeys } from '../../enums/query-keys'
import { createContext, ReactNode, useContext, useState } from 'react'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
@@ -8,6 +8,7 @@ interface DiscoverContext {
refreshing: boolean
refresh: () => void
recentlyAdded: BaseItemDto[] | undefined
recentlyPlayed: BaseItemDto[] | undefined
}
const DiscoverContextInitializer = () => {
@@ -16,13 +17,17 @@ const DiscoverContextInitializer = () => {
const { data: recentlyAdded, refetch } = useQuery({
queryKey: [QueryKeys.RecentlyAdded],
queryFn: () => fetchRecentlyAdded(),
staleTime: 1000 * 60 * 5, // 5 minutes
})
const { data: recentlyPlayed, refetch: refetchRecentlyPlayed } = useQuery({
queryKey: [QueryKeys.RecentlyPlayed],
queryFn: () => fetchRecentlyPlayed(),
})
const refresh = async () => {
setRefreshing(true)
await Promise.all([refetch()])
await Promise.all([refetch(), refetchRecentlyPlayed()])
setRefreshing(false)
}
@@ -30,6 +35,7 @@ const DiscoverContextInitializer = () => {
refreshing,
refresh,
recentlyAdded,
recentlyPlayed,
}
}
@@ -37,6 +43,7 @@ const DiscoverContext = createContext<DiscoverContext>({
refreshing: false,
refresh: () => {},
recentlyAdded: undefined,
recentlyPlayed: undefined,
})
export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
@@ -44,19 +51,9 @@ export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.
}: {
children: ReactNode
}) => {
const { refreshing, refresh, recentlyAdded } = DiscoverContextInitializer()
const context = DiscoverContextInitializer()
return (
<DiscoverContext.Provider
value={{
refreshing,
refresh,
recentlyAdded,
}}
>
{children}
</DiscoverContext.Provider>
)
return <DiscoverContext.Provider value={context}>{children}</DiscoverContext.Provider>
}
export const useDiscoverContext = () => useContext(DiscoverContext)

View File

@@ -7,6 +7,7 @@ import Albums from '../Albums/component'
import { AlbumScreen } from '../Album'
import { ArtistScreen } from '../Artist'
import { DiscoverProvider } from './provider'
import InstantMix from '../InstantMix/component'
export const DiscoverStack = createNativeStackNavigator<StackParamList>()
@@ -47,6 +48,16 @@ export function Discover(): React.JSX.Element {
<DiscoverStack.Screen name='Albums' component={Albums} />
<DiscoverStack.Screen
name='InstantMix'
component={InstantMix}
options={({ route }) => ({
title: route.params.item.Name
? `${route.params.item.Name} Mix`
: 'Instant Mix',
})}
/>
<DiscoverStack.Group screenOptions={{ presentation: 'modal' }}>
<DiscoverStack.Screen
name='Details'

View File

@@ -4,7 +4,7 @@ import { Text } from '../helpers/text'
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchItemImage } from '../../../api/queries/functions/images'
import { fetchItemImage } from '../../../api/queries/images'
interface AvatarProps extends TamaguiAvatarProps {
item: BaseItemDto

View File

@@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'
import { isUndefined } from 'lodash'
import { getTokens, Spinner } from 'tamagui'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserData } from '../../../api/queries/functions/favorites'
import { fetchUserData } from '../../../api/queries/favorites'
import { useJellifyUserDataContext } from '../../../components/user-data-provider'
interface SetFavoriteMutation {
@@ -57,6 +57,6 @@ export function isFavoriteItem(item: BaseItemDto): boolean {
return isUndefined(item.UserData)
? false
: isUndefined(item.UserData.IsFavorite)
? false
: item.UserData.IsFavorite
? false
: item.UserData.IsFavorite
}

View File

@@ -3,7 +3,7 @@ import { getToken, Spacer, YStack } from 'tamagui'
import Icon from '../helpers/icon'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchUserData } from '../../../api/queries/functions/favorites'
import { fetchUserData } from '../../../api/queries/favorites'
import { useEffect, useState } from 'react'
export default function FavoriteIcon({ item }: { item: BaseItemDto }): React.JSX.Element {

View File

@@ -0,0 +1,58 @@
import Client from '../../../api/client'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import { isUndefined } from 'lodash'
import { StyleProp } from 'react-native'
import FastImage, { ImageStyle } from 'react-native-fast-image'
import { FontSizeTokens, getFontSizeToken, getToken, getTokenValue, Token } from 'tamagui'
interface ImageProps {
item: BaseItemDto
circular?: boolean | undefined
width?: Token | undefined
height?: Token | undefined
style?: ImageStyle | undefined
}
export default function ItemImage({
item,
circular,
width,
height,
style,
}: ImageProps): React.JSX.Element {
return (
<FastImage
source={{ uri: getImageApi(Client.api!).getItemImageUrlById(item.Id!) }}
style={{
borderRadius: getBorderRadius(circular, width),
width: !isUndefined(width)
? getTokenValue(width)
: getToken('$12') + getToken('$5'),
height: !isUndefined(height)
? getTokenValue(height)
: getToken('$12') + getToken('$5'),
alignSelf: 'center',
...style,
}}
/>
)
}
/**
* Get the border radius for the image
* @param circular - Whether the image is circular
* @param width - The width of the image
* @returns The border radius of the image
*/
function getBorderRadius(circular: boolean | undefined, width: Token | undefined): number {
let borderRadius
if (circular) {
borderRadius = width ? getTokenValue(width) : getTokenValue('$12') + getToken('$5')
} else if (!isUndefined(width)) {
borderRadius = getTokenValue(width) / 10
}
return borderRadius
}

View File

@@ -0,0 +1,39 @@
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import React from 'react'
import { StackParamList } from '../../types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { QueryKeys } from '../../../enums/query-keys'
import { useQuery } from '@tanstack/react-query'
import { fetchInstantMixFromItem } from '../../../api/queries/instant-mixes'
import Icon from '../helpers/icon'
import { getToken, Spacer, Spinner } from 'tamagui'
import { useColorScheme } from 'react-native'
export default function InstantMixButton({
item,
navigation,
}: {
item: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { data, isFetching, refetch } = useQuery({
queryKey: [QueryKeys.InstantMix, item.Id!],
queryFn: () => fetchInstantMixFromItem(item),
})
const isDarkMode = useColorScheme() === 'dark'
return data ? (
<Icon
name='compass-outline'
color={isDarkMode ? getToken('$color.success') : getToken('$color.grape')}
onPress={() =>
navigation.navigate('InstantMix', {
item,
mix: data,
})
}
/>
) : (
<Spacer />
)
}

View File

@@ -3,12 +3,12 @@ import type { CardProps as TamaguiCardProps } from 'tamagui'
import { getToken, Card as TamaguiCard, View, YStack } from 'tamagui'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { Text } from '../helpers/text'
import { Image } from 'expo-image'
import FastImage from 'react-native-fast-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../../api/client'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchMediaInfo } from '../../../api/queries/functions/media'
import { fetchMediaInfo } from '../../../api/queries/media'
interface CardProps extends TamaguiCardProps {
caption?: string | null | undefined
@@ -50,15 +50,12 @@ export function ItemCard(props: CardProps) {
)} */}
</TamaguiCard.Footer>
<TamaguiCard.Background>
<Image
source={getImageApi(Client.api!).getItemImageUrlById(
props.item.Type === 'Audio' ? props.item.AlbumId! : props.item.Id!,
)}
placeholder={
props.item.ImageBlurHashes && props.item.ImageBlurHashes['Primary']
? props.item.ImageBlurHashes['Primary'][0]
: undefined
}
<FastImage
source={{
uri: getImageApi(Client.api!).getItemImageUrlById(
props.item.Type === 'Audio' ? props.item.AlbumId! : props.item.Id!,
),
}}
style={{
width: '100%',
height: '100%',

View File

@@ -4,12 +4,12 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { getTokens, Separator, Spacer, View, XStack, YStack } from 'tamagui'
import { Text } from '../helpers/text'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import BlurhashedImage from './blurhashed-image'
import Icon from '../helpers/icon'
import { QueuingType } from '../../../enums/queuing-type'
import { RunTimeTicks } from '../helpers/time-codes'
import { useQueueContext } from '../../../player/queue-provider'
import { usePlayerContext } from '../../../player/player-provider'
import ItemImage from './image'
export default function Item({
item,
@@ -75,11 +75,9 @@ export default function Item({
paddingVertical={'$2'}
marginHorizontal={'$1'}
>
<BlurhashedImage
item={item}
width={width / 9}
borderRadius={item.Type === 'MusicArtist' ? width / 9 : 2}
/>
<YStack flex={1}>
<ItemImage item={item} height={'$12'} width={'$12'} />
</YStack>
<YStack
marginLeft={'$1'}
@@ -97,7 +95,7 @@ export default function Item({
)}
</YStack>
<XStack justifyContent='space-between' alignItems='center' flex={1}>
<XStack justifyContent='space-between' alignItems='center' flex={2}>
{item.UserData?.IsFavorite ? (
<Icon small color={getTokens().color.telemagenta.val} name='heart' />
) : (

View File

@@ -10,14 +10,14 @@ import { StackParamList } from '../../../components/types'
import { QueuingType } from '../../../enums/queuing-type'
import { Queue } from '../../../player/types/queue-item'
import FavoriteIcon from './favorite-icon'
import { Image } from 'expo-image'
import FastImage from 'react-native-fast-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../../api/client'
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
import { useNetworkContext } from '../../../components/Network/provider'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchMediaInfo } from '../../../api/queries/functions/media'
import { fetchMediaInfo } from '../../../api/queries/media'
import { useQueueContext } from '../../../player/queue-provider'
interface TrackProps {
@@ -34,6 +34,8 @@ interface TrackProps {
prependElement?: React.JSX.Element | undefined
showRemove?: boolean | undefined
onRemove?: () => void | undefined
dragHandle?: (() => void) | undefined // Optional drag handle trigger
showDragHandle?: boolean | undefined // Show drag handle
}
export default function Track({
@@ -50,6 +52,8 @@ export default function Track({
prependElement,
showRemove,
onRemove,
dragHandle,
showDragHandle,
}: TrackProps): React.JSX.Element {
const theme = useTheme()
const { nowPlaying, useStartPlayback } = usePlayerContext()
@@ -100,10 +104,23 @@ export default function Track({
item: track,
isNested: isNested,
})
}
}
}
paddingVertical={'$2'}
>
{/* Drag handle, only if enabled */}
{showDragHandle && dragHandle && (
<YStack alignContent='center' justifyContent='center' marginRight={'$2'}>
<Icon
name='drag-vertical'
size={22}
color={getTokens().color.amethyst}
onPress={dragHandle}
accessibilityLabel='Reorder'
/>
</YStack>
)}
{prependElement && (
<YStack alignContent='center' justifyContent='center' flex={1}>
{prependElement}
@@ -118,8 +135,10 @@ export default function Track({
minHeight={showArtwork ? '$4' : 'unset'}
>
{showArtwork ? (
<Image
source={getImageApi(Client.api!).getItemImageUrlById(track.AlbumId!)}
<FastImage
source={{
uri: getImageApi(Client.api!).getItemImageUrlById(track.AlbumId!),
}}
style={{
width: getToken('$12'),
height: getToken('$12'),
@@ -140,10 +159,10 @@ export default function Track({
isPlaying
? getTokens().color.telemagenta
: isOffline
? isDownloaded
? theme.color
: '$purpleGray'
: theme.color
? isDownloaded
? theme.color
: '$purpleGray'
: theme.color
}
lineBreakStrategyIOS='standard'
numberOfLines={1}

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