mirror of
https://github.com/Jellify-Music/App.git
synced 2026-04-21 00:58:32 -05:00
Merge branch 'main' into selfsigned
This commit is contained in:
+1
-4
@@ -16,7 +16,6 @@ Here's the best way to get started:
|
||||
|
||||
### Universal Dependencies
|
||||
|
||||
- [Ruby](https://www.ruby-lang.org/en/documentation/installation/) for Fastlane
|
||||
- [NodeJS v22](https://nodejs.org/en/download) for React Native
|
||||
- [Bun](https://bun.sh/) for managing dependencies
|
||||
|
||||
@@ -31,10 +30,8 @@ Here's the best way to get started:
|
||||
##### Setup
|
||||
|
||||
- Clone this repository
|
||||
- Run `bun init-ios:new-arch` to initialize the project
|
||||
- Run `bun init-ios` to initialize the project
|
||||
- This will install `npm` packages, install `bundler` and required gems, and install required CocoaPods with [React Native's New Architecture](https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here#what-is-the-new-architecture)
|
||||
- In the `ios` directory, run `fastlane match development --readonly` to fetch the development signing certificates
|
||||
- _You will need access to the "Jellify Signing" private repository_
|
||||
|
||||
##### Running
|
||||
|
||||
|
||||
@@ -91,8 +91,8 @@ android {
|
||||
applicationId "com.cosmonautical.jellify"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 170
|
||||
versionName "1.0.11"
|
||||
versionCode 171
|
||||
versionName "1.0.12"
|
||||
resValue "string", "build_config_package", "com.jellify"
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "jellify",
|
||||
@@ -30,6 +29,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"openai": "5.21.0",
|
||||
"react": "19.2.0",
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
@@ -42,10 +42,10 @@
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-mmkv": "^4.1.0",
|
||||
"react-native-mmkv": "^4.1.1",
|
||||
"react-native-nitro-fetch": "^0.1.6",
|
||||
"react-native-nitro-modules": "0.32.0-beta.0",
|
||||
"react-native-nitro-ota": "0.9.0",
|
||||
"react-native-nitro-modules": "^0.32.1",
|
||||
"react-native-nitro-ota": "^0.10.0",
|
||||
"react-native-pager-view": "8.0.0",
|
||||
"react-native-reanimated": "4.1.6",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
@@ -1924,13 +1924,13 @@
|
||||
|
||||
"react-native-linear-gradient": ["react-native-linear-gradient@2.8.3", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA=="],
|
||||
|
||||
"react-native-mmkv": ["react-native-mmkv@4.1.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-ia76WnU6dkLZxFkSSflxqFgHT2pIaML763aucEu7nMglF41oEWTdTtBu0o8a1cxbhZOaONk6KF8RQp5fLvPitA=="],
|
||||
"react-native-mmkv": ["react-native-mmkv@4.1.1", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-nYFjM27l7zVhIiyAqWEFRagGASecb13JMIlzAuOeakRRz9GMJ49hCQntUBE2aeuZRE4u4ehSqTOomB0mTF56Ew=="],
|
||||
|
||||
"react-native-nitro-fetch": ["react-native-nitro-fetch@0.1.6", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "^0.29.2", "react-native-worklets-core": "^1.6.0" }, "optionalPeers": ["react-native-worklets-core"] }, "sha512-DbE/vN5B67SJM8Q0myHOwSSc7ASqJPaKYXVsWdNGIPS+csr9gygCKILT4RQ+xZ92iJGKn4bfyq+rRsacRWBV9A=="],
|
||||
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.32.0-beta.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-Yb3PaefFZXVLAj/2meruDX9IH6Q8P0eeKqLiTcwY8NG/bMb/cUqteYA9FZIhc8TUGmn/FiQu18hhCF0QYySOWA=="],
|
||||
"react-native-nitro-modules": ["react-native-nitro-modules@0.32.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-V+Vy76e4fxRxgVGu5Uh3cBPvuFQW8fM1OUKk1mqEA/JawjhX+hxHtBhpfuvNjV0BnV/uXCIg8/eK+rTpB6tqFg=="],
|
||||
|
||||
"react-native-nitro-ota": ["react-native-nitro-ota@0.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "^0.31.10" } }, "sha512-hxSGMy612Iz+DEgJu1fLBFV9w2Z9s3hR7glnj/qDmzpF5+Bb2NxPGmr19hhhL6riKZx/+BO3e1mY4OG1cek65w=="],
|
||||
"react-native-nitro-ota": ["react-native-nitro-ota@0.10.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "0.32.0" } }, "sha512-pxmdaeNdUdnYdD1M8BpbtQo4mZrtljWFg0gspuIohTJqi97JYIRq0b+SReN0sMMo0w912k4XXSGMr/IduGoMNg=="],
|
||||
|
||||
"react-native-pager-view": ["react-native-pager-view@8.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-oAwlWT1lhTkIs9HhODnjNNl/owxzn9DP1MbP+az6OTUdgbmzA16Up83sBH8NRKwrH8rNm7iuWnX1qMqiiWOLhg=="],
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import App from './App'
|
||||
import { name as appName } from './app.json'
|
||||
import { PlaybackService } from './src/player'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { enableScreens } from 'react-native-screens'
|
||||
import { enableFreeze, enableScreens } from 'react-native-screens'
|
||||
|
||||
enableScreens(true)
|
||||
enableFreeze(true)
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App)
|
||||
|
||||
|
||||
@@ -543,7 +543,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 279;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -554,7 +554,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.11;
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
NEW_SETTING = "";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -585,7 +585,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 279;
|
||||
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -595,7 +595,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.11;
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
NEW_SETTING = "";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
@@ -823,7 +823,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 278;
|
||||
CURRENT_PROJECT_VERSION = 279;
|
||||
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -834,7 +834,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0.11;
|
||||
MARKETING_VERSION = 1.0.12;
|
||||
NEW_SETTING = "";
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
|
||||
@@ -20,9 +20,6 @@ end
|
||||
target 'Jellify' do
|
||||
config = use_native_modules!
|
||||
|
||||
pod 'SDWebImage', :modular_headers => true
|
||||
pod 'NitroOtaBundleManager', :path => '../node_modules/react-native-nitro-ota'
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# An absolute path to your application root.
|
||||
@@ -30,8 +27,6 @@ target 'Jellify' do
|
||||
)
|
||||
|
||||
|
||||
pod 'SDWebImage', :modular_headers => true
|
||||
|
||||
post_install do |installer|
|
||||
# https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202
|
||||
react_native_post_install(
|
||||
|
||||
+14
-52
@@ -11,8 +11,7 @@ PODS:
|
||||
- fmt (11.0.2)
|
||||
- Gifu (3.5.1)
|
||||
- glog (0.3.5)
|
||||
- google-cast-sdk (4.8.3):
|
||||
- Protobuf (~> 3.13)
|
||||
- google-cast-sdk (4.8.4)
|
||||
- hermes-engine (0.14.0):
|
||||
- hermes-engine/Pre-built (= 0.14.0)
|
||||
- hermes-engine/Pre-built (0.14.0)
|
||||
@@ -47,7 +46,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroMmkv (4.1.0):
|
||||
- NitroMmkv (4.1.1):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -78,7 +77,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroModules (0.32.0-beta.0):
|
||||
- NitroModules (0.32.1):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -107,7 +106,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroOta (0.9.0):
|
||||
- NitroOta (0.10.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
@@ -115,6 +114,7 @@ PODS:
|
||||
- glog
|
||||
- hermes-engine
|
||||
- NitroModules
|
||||
- NitroOtaBundleManager (= 0.10.0)
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
@@ -138,36 +138,8 @@ PODS:
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
- Yoga
|
||||
- NitroOtaBundleManager (0.9.0):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
- fast_float
|
||||
- fmt
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly
|
||||
- RCT-Folly/Fabric
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- NitroOtaBundleManager (0.10.0)
|
||||
- PromisesObjC (2.4.0)
|
||||
- Protobuf (3.29.5)
|
||||
- RCT-Folly (2024.11.18.00):
|
||||
- boost
|
||||
- DoubleConversion
|
||||
@@ -3270,9 +3242,6 @@ PODS:
|
||||
- ReactCommon/turbomodule/core
|
||||
- SocketRocket
|
||||
- Yoga
|
||||
- SDWebImage (5.21.2):
|
||||
- SDWebImage/Core (= 5.21.2)
|
||||
- SDWebImage/Core (5.21.2)
|
||||
- Sentry/HybridSDK (8.57.3)
|
||||
- SocketRocket (0.7.1)
|
||||
- SSZipArchive (2.4.3)
|
||||
@@ -3293,7 +3262,6 @@ DEPENDENCIES:
|
||||
- NitroMmkv (from `../node_modules/react-native-mmkv`)
|
||||
- NitroModules (from `../node_modules/react-native-nitro-modules`)
|
||||
- NitroOta (from `../node_modules/react-native-nitro-ota`)
|
||||
- NitroOtaBundleManager (from `../node_modules/react-native-nitro-ota`)
|
||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
||||
@@ -3387,7 +3355,6 @@ DEPENDENCIES:
|
||||
- RNScreens (from `../node_modules/react-native-screens`)
|
||||
- "RNSentry (from `../node_modules/@sentry/react-native`)"
|
||||
- RNWorklets (from `../node_modules/react-native-worklets`)
|
||||
- SDWebImage
|
||||
- SocketRocket (~> 0.7.1)
|
||||
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
|
||||
|
||||
@@ -3398,9 +3365,8 @@ SPEC REPOS:
|
||||
- Gifu
|
||||
- google-cast-sdk
|
||||
- MMKVCore
|
||||
- NitroOtaBundleManager
|
||||
- PromisesObjC
|
||||
- Protobuf
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SocketRocket
|
||||
- SSZipArchive
|
||||
@@ -3433,8 +3399,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-nitro-modules"
|
||||
NitroOta:
|
||||
:path: "../node_modules/react-native-nitro-ota"
|
||||
NitroOtaBundleManager:
|
||||
:path: "../node_modules/react-native-nitro-ota"
|
||||
RCT-Folly:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||
RCTDeprecation:
|
||||
@@ -3633,16 +3597,15 @@ SPEC CHECKSUMS:
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
Gifu: 9f7e52357d41c0739709019eb80a71ad9aab1b6d
|
||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||
google-cast-sdk: 1fb6724e94cc5ff23b359176e0cf6360586bb97a
|
||||
hermes-engine: 83ac7cadb2a3a158ae6d9e4192417c5232065e99
|
||||
google-cast-sdk: 32f65af50d164e3c475e79ad123db3cc26fbcd37
|
||||
hermes-engine: 6878b8fefe82b91b24caae48ac97164746244d09
|
||||
MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df
|
||||
NitroFetch: 660adfb47f84b28db664f97b50e5dc28506ab6c1
|
||||
NitroMmkv: ce1df9b9f0e06dfbde2455d863047e0411fceb6e
|
||||
NitroModules: 7dc2f90b6122b23143848972adc5ac9513537d43
|
||||
NitroOta: d6a10da7e79dd58dace797d43b43e8e034c86eca
|
||||
NitroOtaBundleManager: 59a8392078acfd0f28d31c1360f15f7eddcd8482
|
||||
NitroMmkv: 8ed7ef6f41b91785fc580c975f68d6d675214767
|
||||
NitroModules: bbd7f9f8913dc8af187349463e4368a1144d6e31
|
||||
NitroOta: 92d4eb528566b6babf5e4a30adbda44bfa803a9b
|
||||
NitroOtaBundleManager: 8fad871db2daf6b9ee6f04a100c79605cfa81e8d
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
Protobuf: 164aea2ae380c3951abdc3e195220c01d17400e0
|
||||
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
|
||||
RCTDeprecation: a41bbdd9af30bf2e5715796b313e44ec43eefff1
|
||||
RCTRequired: 7be34aabb0b77c3cefe644528df0fa0afad4e4d0
|
||||
@@ -3735,7 +3698,6 @@ SPEC CHECKSUMS:
|
||||
RNScreens: ffbb0296608eb3560de641a711bbdb663ed1f6b4
|
||||
RNSentry: fdb39d5f294e492aa2f08ad80e510310dc223772
|
||||
RNWorklets: 69f7239afaf3a156f7f9549eeb0ae8d02adc095f
|
||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
||||
Sentry: c643eb180df401dd8c734c5036ddd9dd9218daa6
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||
@@ -3743,6 +3705,6 @@ SPEC CHECKSUMS:
|
||||
SwiftUIIntrospect: fee9aa07293ee280373a591e1824e8ddc869ba5d
|
||||
Yoga: 5456bb010373068fc92221140921b09d126b116e
|
||||
|
||||
PODFILE CHECKSUM: 05d07b9cff134e4c27345bc2b588e090e4d3431c
|
||||
PODFILE CHECKSUM: b9282cc589d492312a14687826c7e41a850a22cf
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { playLaterInQueue } from '../../src/providers/Player/functions/queue'
|
||||
import { playLaterInQueue } from '../../src/hooks/player/functions/queue'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getApi } from '../../src/stores'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
import calculateTrackVolume from '../../src/providers/Player/utils/normalization'
|
||||
import calculateTrackVolume from '../../src/hooks/player/functions/normalization'
|
||||
|
||||
describe('Normalization Module', () => {
|
||||
it('should calculate the volume for a track with a normalization gain of 6', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'react-native'
|
||||
import { shuffleJellifyTracks } from '../../src/providers/Player/utils/shuffle'
|
||||
import { shuffleJellifyTracks } from '../../src/hooks/player/functions/utils/shuffle'
|
||||
import { QueuingType } from '../../src/enums/queuing-type'
|
||||
import JellifyTrack from '../../src/types/JellifyTrack'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jellify",
|
||||
"version": "1.0.11",
|
||||
"version": "1.0.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"init-android": "bun i",
|
||||
@@ -62,6 +62,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"openai": "5.21.0",
|
||||
"react": "19.2.0",
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.83.1",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-blob-util": "^0.22.2",
|
||||
@@ -74,10 +75,10 @@
|
||||
"react-native-google-cast": "^4.9.1",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-linear-gradient": "^2.8.3",
|
||||
"react-native-mmkv": "^4.1.0",
|
||||
"react-native-mmkv": "^4.1.1",
|
||||
"react-native-nitro-fetch": "^0.1.6",
|
||||
"react-native-nitro-modules": "0.32.0-beta.0",
|
||||
"react-native-nitro-ota": "0.9.0",
|
||||
"react-native-nitro-modules": "^0.32.1",
|
||||
"react-native-nitro-ota": "^0.10.0",
|
||||
"react-native-pager-view": "8.0.0",
|
||||
"react-native-reanimated": "4.1.6",
|
||||
"react-native-safe-area-context": "5.6.2",
|
||||
|
||||
@@ -6,14 +6,14 @@ import { useUserPlaylists } from '../../queries/playlist'
|
||||
const useHomeQueries = () => {
|
||||
const { refetch: refetchUserPlaylists } = useUserPlaylists()
|
||||
|
||||
const { refetch: refetchRecentArtists } = useRecentArtists()
|
||||
|
||||
const { refetch: refetchRecentlyPlayed } = useRecentlyPlayedTracks()
|
||||
|
||||
const { refetch: refetchFrequentArtists } = useFrequentlyPlayedArtists()
|
||||
|
||||
const { refetch: refetchFrequentlyPlayed } = useFrequentlyPlayedTracks()
|
||||
|
||||
const { refetch: refetchRecentArtists } = useRecentArtists()
|
||||
|
||||
const { refetch: refetchFrequentArtists } = useFrequentlyPlayedArtists()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
await Promise.allSettled([
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import JellifyTrack from '../../../../types/JellifyTrack'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { getApi } from '../../../../stores'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
|
||||
export default async function reportPlaybackCompleted(
|
||||
api: Api | undefined,
|
||||
track: JellifyTrack,
|
||||
): Promise<AxiosResponse<void, unknown>> {
|
||||
const api = getApi()
|
||||
|
||||
): Promise<void> {
|
||||
if (!api) return Promise.reject('API instance not set')
|
||||
|
||||
const { sessionId, item, mediaSourceInfo } = track
|
||||
|
||||
return await getPlaystateApi(api).reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
PositionTicks: mediaSourceInfo?.RunTimeTicks || item.RunTimeTicks,
|
||||
},
|
||||
})
|
||||
try {
|
||||
await getPlaystateApi(api).reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
PositionTicks: mediaSourceInfo?.RunTimeTicks || item.RunTimeTicks,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Unable to report playback stopped', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { getApi } from '../../../../stores'
|
||||
import JellifyTrack from '../../../../types/JellifyTrack'
|
||||
import { convertSecondsToRunTimeTicks } from '../../../../utils/mapping/ticks-to-seconds'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
|
||||
export default async function reportPlaybackProgress(
|
||||
api: Api | undefined,
|
||||
track: JellifyTrack,
|
||||
position: number,
|
||||
): Promise<AxiosResponse<void, unknown>> {
|
||||
const api = getApi()
|
||||
|
||||
): Promise<void> {
|
||||
if (!api) return Promise.reject('API instance not set')
|
||||
|
||||
const { sessionId, item } = track
|
||||
|
||||
return await getPlaystateApi(api).reportPlaybackProgress({
|
||||
playbackProgressInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
PositionTicks: convertSecondsToRunTimeTicks(position),
|
||||
},
|
||||
})
|
||||
try {
|
||||
await getPlaystateApi(api).reportPlaybackProgress({
|
||||
playbackProgressInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
PositionTicks: convertSecondsToRunTimeTicks(position),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Unable to report playback progress', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import JellifyTrack from '../../../../types/JellifyTrack'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { getApi } from '../../../../stores'
|
||||
|
||||
export default async function reportPlaybackStarted(track: JellifyTrack) {
|
||||
const api = getApi()
|
||||
import { convertSecondsToRunTimeTicks } from '../../../../utils/mapping/ticks-to-seconds'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
|
||||
export default async function reportPlaybackStarted(
|
||||
api: Api | undefined,
|
||||
track: JellifyTrack,
|
||||
position?: number | undefined,
|
||||
) {
|
||||
if (!api) return Promise.reject('API instance not set')
|
||||
|
||||
const { sessionId, item } = track
|
||||
|
||||
return await getPlaystateApi(api).reportPlaybackStart({
|
||||
playbackStartInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
},
|
||||
})
|
||||
try {
|
||||
await getPlaystateApi(api).reportPlaybackStart({
|
||||
playbackStartInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
PositionTicks: position ? convertSecondsToRunTimeTicks(position) : undefined,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Unable to report playback started', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import JellifyTrack from '../../../../types/JellifyTrack'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api/playstate-api'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { getApi } from '../../../../stores'
|
||||
import { convertSecondsToRunTimeTicks } from '../../../../utils/mapping/ticks-to-seconds'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
|
||||
export default async function reportPlaybackStopped(
|
||||
api: Api | undefined,
|
||||
track: JellifyTrack,
|
||||
): Promise<AxiosResponse<void, unknown>> {
|
||||
const api = getApi()
|
||||
|
||||
lastPosition?: number | undefined,
|
||||
): Promise<void> {
|
||||
if (!api) return Promise.reject('API instance not set')
|
||||
|
||||
const { sessionId, item } = track
|
||||
|
||||
return await getPlaystateApi(api).reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
},
|
||||
})
|
||||
try {
|
||||
await getPlaystateApi(api).reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: item.Id,
|
||||
PositionTicks: lastPosition
|
||||
? convertSecondsToRunTimeTicks(lastPosition)
|
||||
: undefined,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Unable to report playback stopped', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import reportPlaybackCompleted from './functions/playback-completed'
|
||||
import reportPlaybackStopped from './functions/playback-stopped'
|
||||
import isPlaybackFinished from './utils'
|
||||
import reportPlaybackProgress from './functions/playback-progress'
|
||||
import reportPlaybackStarted from './functions/playback-started'
|
||||
import { useApi } from '../../../stores'
|
||||
|
||||
interface PlaybackStartedMutation {
|
||||
track: JellifyTrack
|
||||
}
|
||||
|
||||
export const useReportPlaybackStarted = () =>
|
||||
useMutation({
|
||||
onMutate: () => {},
|
||||
mutationFn: async ({ track }: PlaybackStartedMutation) => reportPlaybackStarted(track),
|
||||
onError: (error) => console.error(`Reporting playback started failed`, error),
|
||||
onSuccess: () => {},
|
||||
})
|
||||
|
||||
interface PlaybackStoppedMutation {
|
||||
track: JellifyTrack
|
||||
lastPosition: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export const useReportPlaybackStopped = () =>
|
||||
useMutation({
|
||||
onMutate: ({ lastPosition, duration }) => {},
|
||||
mutationFn: async ({ track, lastPosition, duration }: PlaybackStoppedMutation) => {
|
||||
return isPlaybackFinished(lastPosition, duration)
|
||||
? await reportPlaybackCompleted(track)
|
||||
: await reportPlaybackStopped(track)
|
||||
},
|
||||
onError: (error, { lastPosition, duration }) =>
|
||||
console.error(
|
||||
`Reporting playback ${isPlaybackFinished(lastPosition, duration) ? 'completed' : 'stopped'} failed`,
|
||||
error,
|
||||
),
|
||||
onSuccess: (_, { lastPosition, duration }) => {},
|
||||
})
|
||||
|
||||
interface PlaybackProgressMutation {
|
||||
track: JellifyTrack
|
||||
position: number
|
||||
}
|
||||
|
||||
export const useReportPlaybackProgress = () =>
|
||||
useMutation({
|
||||
onMutate: ({ position }) => {},
|
||||
mutationFn: async ({ track, position }: PlaybackProgressMutation) =>
|
||||
reportPlaybackProgress(track, position),
|
||||
})
|
||||
@@ -7,6 +7,9 @@ import { isUndefined } from 'lodash'
|
||||
/**
|
||||
* Adds a track to a Jellyfin playlist.
|
||||
*
|
||||
* @deprecated Let's just use the {@link addManyToPlaylist} mutation instead
|
||||
* and not factor in for a singular track
|
||||
*
|
||||
* @param api The Jellyfin {@link Api} client
|
||||
* @param user The signed in {@link JellifyUser}
|
||||
* @param track The {@link BaseItemDto} to add
|
||||
|
||||
@@ -14,7 +14,7 @@ import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { fetchRecentlyAdded } from '../recents/utils'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { getApi, useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
import { fetchAlbumDiscs } from '../item'
|
||||
import { Api } from '@jellyfin/sdk/lib/api'
|
||||
@@ -24,8 +24,8 @@ const useAlbums: () => [
|
||||
RefObject<Set<string>>,
|
||||
UseInfiniteQueryResult<(string | number | BaseItemDto)[]>,
|
||||
] = () => {
|
||||
const api = useApi()
|
||||
const [user] = useJellifyUser()
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const isFavorites = useLibraryStore((state) => state.isFavorites)
|
||||
@@ -65,7 +65,7 @@ const useAlbums: () => [
|
||||
export default useAlbums
|
||||
|
||||
export const useRecentlyAddedAlbums = () => {
|
||||
const api = useApi()
|
||||
const api = getApi()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useInfiniteQuery({
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import fetchStorageInUse from './utils/storage-in-use'
|
||||
import { getAudioCache } from '../../mutations/download/offlineModeUtils'
|
||||
import DownloadQueryKeys from './keys'
|
||||
import { AUDIO_CACHE_QUERY } from './constants'
|
||||
|
||||
export const useStorageInUse = () =>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { UserPlaylistsQueryKey } from './keys'
|
||||
import { PlaylistTracksQueryKey, PublicPlaylistsQueryKey, UserPlaylistsQueryKey } from './keys'
|
||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { fetchUserPlaylists, fetchPublicPlaylists, fetchPlaylistTracks } from './utils'
|
||||
import { ApiLimits } from '../../../configs/query.config'
|
||||
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
|
||||
export const useUserPlaylists = () => {
|
||||
const api = useApi()
|
||||
const [user] = useJellifyUser()
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useInfiniteQuery({
|
||||
@@ -23,12 +23,12 @@ export const useUserPlaylists = () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const usePlaylistTracks = (playlist: BaseItemDto) => {
|
||||
const api = useApi()
|
||||
export const usePlaylistTracks = (playlist: BaseItemDto, disabled?: boolean | undefined) => {
|
||||
const api = getApi()
|
||||
|
||||
return useInfiniteQuery({
|
||||
// Changed from QueryKeys.ItemTracks to avoid cache conflicts with old useQuery data
|
||||
queryKey: [QueryKeys.ItemTracks, 'infinite', playlist.Id!],
|
||||
queryKey: PlaylistTracksQueryKey(playlist),
|
||||
queryFn: ({ pageParam }) => fetchPlaylistTracks(api, playlist.Id!, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
@@ -36,16 +36,16 @@ export const usePlaylistTracks = (playlist: BaseItemDto) => {
|
||||
if (!lastPage) return undefined
|
||||
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
||||
},
|
||||
enabled: Boolean(api && playlist.Id),
|
||||
enabled: Boolean(api && playlist.Id && !disabled),
|
||||
})
|
||||
}
|
||||
|
||||
export const usePublicPlaylists = () => {
|
||||
const api = useApi()
|
||||
const api = getApi()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId],
|
||||
queryKey: PublicPlaylistsQueryKey(library),
|
||||
queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
|
||||
enum PlaylistQueryKeys {
|
||||
UserPlaylists,
|
||||
@@ -10,6 +12,12 @@ export const UserPlaylistsQueryKey = (library: JellifyLibrary | undefined) => [
|
||||
library?.playlistLibraryId,
|
||||
]
|
||||
|
||||
export const PlaylistTracksQueryKey = (playlist: BaseItemDto) => [
|
||||
QueryKeys.ItemTracks,
|
||||
'infinite',
|
||||
playlist.Id!,
|
||||
]
|
||||
|
||||
export const PublicPlaylistsQueryKey = (library: JellifyLibrary | undefined) => [
|
||||
PlaylistQueryKeys.PublicPlaylists,
|
||||
library?.playlistLibraryId,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { RecentlyPlayedArtistsQueryKey, RecentlyPlayedTracksQueryKey } from './keys'
|
||||
import { InfiniteData, useInfiniteQuery } from '@tanstack/react-query'
|
||||
import { InfiniteData, useInfiniteQuery, useQueries } from '@tanstack/react-query'
|
||||
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from './utils'
|
||||
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useApi, useJellifyUser, useJellifyLibrary, getUser, getApi } from '../../../stores'
|
||||
import { useJellifyLibrary, getUser, getApi } from '../../../stores'
|
||||
import { ONE_MINUTE } from '../../../constants/query-client'
|
||||
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
@@ -11,7 +11,6 @@ import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
const RECENTS_QUERY_CONFIG = {
|
||||
maxPages: MaxPages.Home,
|
||||
staleTime: ONE_MINUTE * 5,
|
||||
refetchOnMount: false,
|
||||
} as const
|
||||
|
||||
export const useRecentlyPlayedTracks = () => {
|
||||
@@ -44,11 +43,15 @@ export const PlayItAgainQuery = (library: JellifyLibrary | undefined) => {
|
||||
}
|
||||
|
||||
export const useRecentArtists = () => {
|
||||
const api = useApi()
|
||||
const [user] = useJellifyUser()
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const { data: recentlyPlayedTracks } = useRecentlyPlayedTracks()
|
||||
const {
|
||||
data: recentlyPlayedTracks,
|
||||
isPending: recentlyPlayedTracksPending,
|
||||
isStale: recentlyPlayedTracksStale,
|
||||
} = useRecentlyPlayedTracks()
|
||||
|
||||
return useInfiniteQuery({
|
||||
queryKey: RecentlyPlayedArtistsQueryKey(user, library),
|
||||
@@ -58,7 +61,10 @@ export const useRecentArtists = () => {
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
return lastPage.length > 0 ? lastPageParam + 1 : undefined
|
||||
},
|
||||
enabled: !isUndefined(recentlyPlayedTracks),
|
||||
enabled:
|
||||
!isUndefined(recentlyPlayedTracks) &&
|
||||
!recentlyPlayedTracksPending &&
|
||||
!recentlyPlayedTracksStale,
|
||||
...RECENTS_QUERY_CONFIG,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,10 +13,9 @@ import { isUndefined } from 'lodash'
|
||||
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||
import { queryClient } from '../../../../constants/query-client'
|
||||
import { InfiniteData } from '@tanstack/react-query'
|
||||
import { fetchItems } from '../../item'
|
||||
import { RecentlyPlayedTracksQueryKey } from '../keys'
|
||||
import { RECENTLY_PLAYED_ALBUM_THRESHOLD } from '../../../../configs/home.config'
|
||||
import { PlayItAgainQuery } from '..'
|
||||
|
||||
export async function fetchRecentlyAdded(
|
||||
api: Api | undefined,
|
||||
@@ -146,53 +145,53 @@ export function fetchRecentlyPlayedArtists(
|
||||
if (isUndefined(library)) return reject('Library instance not set')
|
||||
|
||||
// Get the recently played tracks from the query client
|
||||
const recentlyPlayedTracks = queryClient.getQueryData<InfiniteData<BaseItemDto[]>>(
|
||||
RecentlyPlayedTracksQueryKey(user, library),
|
||||
)
|
||||
if (!recentlyPlayedTracks) {
|
||||
return resolve([])
|
||||
}
|
||||
queryClient
|
||||
.ensureInfiniteQueryData(PlayItAgainQuery(library))
|
||||
.then((recentlyPlayedTracks) => {
|
||||
if (!recentlyPlayedTracks) {
|
||||
return resolve([])
|
||||
}
|
||||
|
||||
// Get the artists from the recently played tracks
|
||||
const artists = recentlyPlayedTracks.pages[page]
|
||||
// Get the artists from the recently played tracks
|
||||
const artists = recentlyPlayedTracks.pages[page]
|
||||
|
||||
// Map artist from the recently played tracks
|
||||
.map((track) => (track.ArtistItems ? track.ArtistItems[0] : undefined))
|
||||
// Map artist from the recently played tracks
|
||||
.map((track) => (track.ArtistItems ? track.ArtistItems[0] : undefined))
|
||||
|
||||
// Filter out undefined artists
|
||||
.filter((artist) => artist !== undefined)
|
||||
// Filter out undefined artists
|
||||
.filter((artist) => artist !== undefined)
|
||||
|
||||
// Filter out duplicate artists
|
||||
.filter(
|
||||
(artist, index, artists) =>
|
||||
artists.findIndex((duplicateArtist) => duplicateArtist.Id === artist.Id) ===
|
||||
index,
|
||||
)
|
||||
// Filter out duplicate artists
|
||||
.filter(
|
||||
(artist, index, artists) =>
|
||||
artists.findIndex(
|
||||
(duplicateArtist) => duplicateArtist.Id === artist.Id,
|
||||
) === index,
|
||||
)
|
||||
|
||||
fetchItems(
|
||||
api,
|
||||
user,
|
||||
library,
|
||||
[BaseItemKind.MusicArtist],
|
||||
page,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
artists.map((artist) => artist.Id!),
|
||||
)
|
||||
.then((artistPages) => {
|
||||
resolve(
|
||||
artistPages.data.sort((a, b) => {
|
||||
const aIndex = artists.findIndex((artist) => artist.Id === a.Id)
|
||||
const bIndex = artists.findIndex((artist) => artist.Id === b.Id)
|
||||
return aIndex - bIndex
|
||||
}),
|
||||
fetchItems(
|
||||
api,
|
||||
user,
|
||||
library,
|
||||
[BaseItemKind.MusicArtist],
|
||||
page,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
artists.map((artist) => artist.Id!),
|
||||
)
|
||||
.then((artistPages) => {
|
||||
resolve(
|
||||
artistPages.data.sort((a, b) => {
|
||||
const aIndex = artists.findIndex((artist) => artist.Id === a.Id)
|
||||
const bIndex = artists.findIndex((artist) => artist.Id === b.Id)
|
||||
return aIndex - bIndex
|
||||
}),
|
||||
)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
return reject(error)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useAllDownloadedTracks } from '../download'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import UserDataQueryKey from '../user-data/keys'
|
||||
import { JellifyUser } from '@/src/types/JellifyUser'
|
||||
import { useApi, useJellifyUser, useJellifyLibrary } from '../../../stores'
|
||||
import { useJellifyLibrary, getApi, getUser } from '../../../stores'
|
||||
import useLibraryStore from '../../../stores/library'
|
||||
|
||||
const useTracks: (
|
||||
@@ -28,8 +28,8 @@ const useTracks: (
|
||||
sortOrder,
|
||||
isFavoritesParam,
|
||||
) => {
|
||||
const api = useApi()
|
||||
const [user] = useJellifyUser()
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
const {
|
||||
isFavorites: isLibraryFavorites,
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { UseInfiniteQueryResult, useMutation, InfiniteData } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { addManyToPlaylist, addToPlaylist } from '../../api/mutations/playlists'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
YStack,
|
||||
XStack,
|
||||
Spacer,
|
||||
YGroup,
|
||||
Separator,
|
||||
ListItem,
|
||||
getTokens,
|
||||
Spinner,
|
||||
View,
|
||||
} from 'tamagui'
|
||||
import { addManyToPlaylist } from '../../api/mutations/playlists'
|
||||
import { YStack, XStack, Spacer, Spinner, View } from 'tamagui'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { AddToPlaylistMutation } from './types'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
@@ -24,14 +13,16 @@ import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import { usePlaylistTracks, useUserPlaylists } from '../../api/queries/playlist'
|
||||
import { getApi, getUser } from '../../stores'
|
||||
import Animated, { Easing, FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { FlashList, ViewToken } from '@shopify/flash-list'
|
||||
import { useState } from 'react'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { PlaylistTracksQueryKey } from '../../api/queries/playlist/keys'
|
||||
|
||||
export default function AddToPlaylist({
|
||||
track,
|
||||
tracks,
|
||||
source,
|
||||
}: {
|
||||
track?: BaseItemDto
|
||||
tracks?: BaseItemDto[]
|
||||
tracks: BaseItemDto[]
|
||||
source?: BaseItemDto
|
||||
}): React.JSX.Element {
|
||||
const {
|
||||
@@ -40,23 +31,34 @@ export default function AddToPlaylist({
|
||||
isSuccess: playlistsFetchSuccess,
|
||||
} = useUserPlaylists()
|
||||
|
||||
const [visiblePlaylistIds, setVisiblePlaylistIds] = useState<string[]>([])
|
||||
|
||||
const onViewableItemsChanged = ({
|
||||
viewableItems,
|
||||
}: {
|
||||
viewableItems: ViewToken<BaseItemDto>[]
|
||||
}) => {
|
||||
const visibleIds = viewableItems.map(({ item }) => item.Id!)
|
||||
setVisiblePlaylistIds(visibleIds)
|
||||
}
|
||||
|
||||
return (
|
||||
<View flex={1}>
|
||||
{(source ?? track) && (
|
||||
{(source ?? tracks[0]) && (
|
||||
<XStack gap={'$2'} margin={'$4'}>
|
||||
<ItemImage item={source ?? track!} width={'$12'} height={'$12'} />
|
||||
<ItemImage item={source ?? tracks[0]} width={'$12'} height={'$12'} />
|
||||
|
||||
<YStack gap={'$2'}>
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold fontSize={'$6'}>
|
||||
{getItemName(source ?? track!)}
|
||||
{getItemName(source ?? tracks[0])}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
|
||||
{(source ?? track)?.ArtistItems && (
|
||||
{(source ?? tracks[0])?.ArtistItems && (
|
||||
<TextTicker {...TextTickerConfig}>
|
||||
<Text bold>
|
||||
{`${(source ?? track)!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`}
|
||||
{`${(source ?? tracks[0])!.ArtistItems?.map((artist) => getItemName(artist)).join(', ')}`}
|
||||
</Text>
|
||||
</TextTicker>
|
||||
)}
|
||||
@@ -65,15 +67,19 @@ export default function AddToPlaylist({
|
||||
)}
|
||||
|
||||
{!playlistsFetchPending && playlistsFetchSuccess && (
|
||||
<YGroup separator={<Separator />} scrollable flex={1}>
|
||||
{playlists?.map((playlist) => (
|
||||
<FlashList
|
||||
data={playlists}
|
||||
renderItem={({ item: playlist }) => (
|
||||
<AddToPlaylistRow
|
||||
key={playlist.Id}
|
||||
playlist={playlist}
|
||||
tracks={tracks ? tracks : track ? [track] : []}
|
||||
tracks={tracks ? tracks : tracks[0] ? [tracks[0]] : []}
|
||||
visible={visiblePlaylistIds.includes(playlist.Id!)}
|
||||
/>
|
||||
))}
|
||||
</YGroup>
|
||||
)}
|
||||
keyExtractor={(item) => item.Id!}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
@@ -82,96 +88,94 @@ export default function AddToPlaylist({
|
||||
function AddToPlaylistRow({
|
||||
playlist,
|
||||
tracks,
|
||||
visible,
|
||||
}: {
|
||||
playlist: BaseItemDto
|
||||
tracks: BaseItemDto[]
|
||||
visible: boolean
|
||||
}): React.JSX.Element {
|
||||
const trigger = useHapticFeedback()
|
||||
|
||||
const {
|
||||
data: playlistTracks,
|
||||
isPending: fetchingPlaylistTracks,
|
||||
refetch: refetchPlaylistTracks,
|
||||
} = usePlaylistTracks(playlist)
|
||||
const { data: playlistTracks, isPending: fetchingPlaylistTracks } = usePlaylistTracks(
|
||||
playlist,
|
||||
!visible,
|
||||
)
|
||||
|
||||
const useAddToPlaylist = useMutation({
|
||||
mutationFn: ({
|
||||
track,
|
||||
playlist,
|
||||
tracks,
|
||||
}: AddToPlaylistMutation & { tracks?: BaseItemDto[] }) => {
|
||||
mutationFn: ({ playlist, tracks }: AddToPlaylistMutation) => {
|
||||
trigger('impactLight')
|
||||
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
|
||||
if (tracks && tracks.length > 0) {
|
||||
return addManyToPlaylist(api, user, tracks, playlist)
|
||||
}
|
||||
|
||||
return addToPlaylist(api, user, track!, playlist)
|
||||
return addManyToPlaylist(api, user, tracks, playlist)
|
||||
},
|
||||
onSuccess: (data, { playlist }) => {
|
||||
onSuccess: (_, { tracks }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
setIsInPlaylist(true)
|
||||
queryClient.setQueryData(
|
||||
PlaylistTracksQueryKey(playlist),
|
||||
(prev: InfiniteData<BaseItemDto[]> | undefined) => {
|
||||
if (!prev) return prev
|
||||
|
||||
refetchPlaylistTracks()
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map((page: BaseItemDto[], idx: number) =>
|
||||
idx === prev.pages.length - 1 ? [...page, ...tracks] : page,
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
const [isInPlaylist, setIsInPlaylist] = useState<boolean>(
|
||||
const isInPlaylist =
|
||||
tracks.filter((track) =>
|
||||
playlistTracks?.map((playlistTrack) => playlistTrack.Id).includes(track.Id),
|
||||
).length > 0,
|
||||
)
|
||||
).length > 0
|
||||
|
||||
return (
|
||||
<YGroup.Item key={playlist.Id!}>
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
disabled={isInPlaylist}
|
||||
hoverTheme
|
||||
opacity={isInPlaylist ? 0.5 : 1}
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
onPress={() => {
|
||||
if (!isInPlaylist) {
|
||||
useAddToPlaylist.mutate({
|
||||
track: undefined,
|
||||
tracks,
|
||||
playlist,
|
||||
})
|
||||
}
|
||||
}}
|
||||
<XStack
|
||||
animation={'quick'}
|
||||
disabled={isInPlaylist}
|
||||
alignItems='center'
|
||||
gap={'$2'}
|
||||
margin={'$2'}
|
||||
opacity={isInPlaylist ? 0.5 : 1}
|
||||
pressStyle={{ opacity: 0.6 }}
|
||||
onPress={() => {
|
||||
if (!isInPlaylist) {
|
||||
useAddToPlaylist.mutate({
|
||||
tracks,
|
||||
playlist,
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ItemImage item={playlist} height={'$11'} width={'$11'} />
|
||||
|
||||
<YStack alignItems='flex-start' flexGrow={1}>
|
||||
<Text bold>{playlist.Name ?? 'Untitled Playlist'}</Text>
|
||||
|
||||
<Text color={'$neutral'}>{`${playlistTracks?.length ?? 0} tracks`}</Text>
|
||||
</YStack>
|
||||
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
>
|
||||
<XStack alignItems='center' gap={'$2'}>
|
||||
<ItemImage item={playlist} height={'$11'} width={'$11'} />
|
||||
|
||||
<YStack alignItems='flex-start' flexGrow={1}>
|
||||
<Text bold>{playlist.Name ?? 'Untitled Playlist'}</Text>
|
||||
|
||||
<Text color={getTokens().color.amethyst.val}>{`${
|
||||
playlistTracks?.length ?? 0
|
||||
} tracks`}</Text>
|
||||
</YStack>
|
||||
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
>
|
||||
{isInPlaylist ? (
|
||||
<Icon flex={1} name='check-circle-outline' color={'$success'} />
|
||||
) : fetchingPlaylistTracks ? (
|
||||
<Spinner color={'$primary'} />
|
||||
) : (
|
||||
<Spacer flex={1} />
|
||||
)}
|
||||
</Animated.View>
|
||||
</XStack>
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
{isInPlaylist ? (
|
||||
<Icon flex={1} name='check-circle-outline' color={'$success'} />
|
||||
) : fetchingPlaylistTracks ? (
|
||||
<Spinner color={'$primary'} />
|
||||
) : (
|
||||
<Spacer flex={1} />
|
||||
)}
|
||||
</Animated.View>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export interface AddToPlaylistMutation {
|
||||
track?: BaseItemDto
|
||||
tracks: BaseItemDto[]
|
||||
playlist: BaseItemDto
|
||||
}
|
||||
|
||||
@@ -13,8 +13,15 @@ import navigationRef from '../../../navigation'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import formatArtistNames from '../../utils/formatting/artist-names'
|
||||
import { Freeze } from 'react-freeze'
|
||||
|
||||
export default function AlbumTrackListFooter({ album }: { album: BaseItemDto }): React.JSX.Element {
|
||||
export default function AlbumTrackListFooter({
|
||||
album,
|
||||
freeze,
|
||||
}: {
|
||||
album: BaseItemDto
|
||||
freeze: boolean
|
||||
}): React.JSX.Element {
|
||||
const navigation =
|
||||
useNavigation<
|
||||
NativeStackNavigationProp<
|
||||
@@ -25,77 +32,79 @@ export default function AlbumTrackListFooter({ album }: { album: BaseItemDto }):
|
||||
const { data: suggestions, isPending: isLoadingSuggestions } = useSimilarItems(album)
|
||||
|
||||
return (
|
||||
<YStack gap={'$3'} marginVertical={'$2'} flex={1}>
|
||||
{album.ArtistItems && album.ArtistItems.length > 1 && (
|
||||
<YStack>
|
||||
<Text marginHorizontal={'$2'} fontWeight='bold'>
|
||||
Featuring
|
||||
</Text>
|
||||
<Freeze freeze={freeze}>
|
||||
<YStack gap={'$3'} marginVertical={'$2'} flex={1}>
|
||||
{album.ArtistItems && album.ArtistItems.length > 1 && (
|
||||
<YStack>
|
||||
<Text marginHorizontal={'$2'} fontWeight='bold'>
|
||||
Featuring
|
||||
</Text>
|
||||
|
||||
<FlashList
|
||||
data={album.ArtistItems}
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemRow
|
||||
circular
|
||||
item={artist}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', {
|
||||
artist,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', { item: artist })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
<FlashList
|
||||
data={album.ArtistItems}
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemRow
|
||||
circular
|
||||
item={artist}
|
||||
onPress={() => {
|
||||
navigation.navigate('Artist', {
|
||||
artist,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', { item: artist })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</YStack>
|
||||
)}
|
||||
|
||||
{suggestions && suggestions.length > 0 && (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Text marginHorizontal={'$2'} fontWeight='bold'>
|
||||
Similar Albums
|
||||
</Text>
|
||||
<HorizontalCardList
|
||||
data={suggestions}
|
||||
renderItem={({ item: album }) => (
|
||||
<ItemCard
|
||||
size={'$8'}
|
||||
item={album}
|
||||
squared
|
||||
caption={album.Name ?? 'Unknown Album'}
|
||||
subCaption={formatArtistNames(album.Artists ?? [])}
|
||||
onPress={() => {
|
||||
navigation.push('Album', {
|
||||
album,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', { item: album })
|
||||
}}
|
||||
captionAlign='left'
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<YStack alignContent='center'>
|
||||
{isLoadingSuggestions ? (
|
||||
<Spinner alignSelf='center' color={'$primary'} />
|
||||
) : (
|
||||
<Text justifyContent='center' textAlign='center'>
|
||||
No similar albums found
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</YStack>
|
||||
{suggestions && suggestions.length > 0 && (
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
layout={LinearTransition.springify()}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Text marginHorizontal={'$2'} fontWeight='bold'>
|
||||
Similar Albums
|
||||
</Text>
|
||||
<HorizontalCardList
|
||||
data={suggestions}
|
||||
renderItem={({ item: album }) => (
|
||||
<ItemCard
|
||||
size={'$8'}
|
||||
item={album}
|
||||
squared
|
||||
caption={album.Name ?? 'Unknown Album'}
|
||||
subCaption={formatArtistNames(album.Artists ?? [])}
|
||||
onPress={() => {
|
||||
navigation.push('Album', {
|
||||
album,
|
||||
})
|
||||
}}
|
||||
onLongPress={() => {
|
||||
navigationRef.navigate('Context', { item: album })
|
||||
}}
|
||||
captionAlign='left'
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
<YStack alignContent='center'>
|
||||
{isLoadingSuggestions ? (
|
||||
<Spinner alignSelf='center' color={'$primary'} />
|
||||
) : (
|
||||
<Text justifyContent='center' textAlign='center'>
|
||||
No similar albums found
|
||||
</Text>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
</YStack>
|
||||
</Freeze>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { useLoadNewQueue } from '../../providers/Player/hooks/callbacks'
|
||||
import { useLoadNewQueue } from '../../hooks/player/callbacks'
|
||||
import { BaseStackParamList } from '../../screens/types'
|
||||
import { useApi } from '../../stores'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { YStack, XStack, Separator, Spinner } from 'tamagui'
|
||||
import { YStack, XStack, Separator, Spinner, useTheme } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { SectionList } from 'react-native'
|
||||
import { RefreshControl, SectionList } from 'react-native'
|
||||
import Track from '../Global/components/Track'
|
||||
import FavoriteButton from '../Global/components/favorite-button'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
@@ -10,7 +10,7 @@ import Icon from '../Global/components/icon'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { BaseStackParamList } from '../../screens/types'
|
||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||
import { useApi } from '../../stores'
|
||||
import { getApi } from '../../stores'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchAlbumDiscs } from '../../api/queries/item'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
@@ -18,13 +18,7 @@ import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network
|
||||
import { useIsDownloaded } from '../../api/queries/download'
|
||||
import AlbumTrackListFooter from './footer'
|
||||
import AlbumTrackListHeader from './header'
|
||||
import Animated, {
|
||||
Easing,
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
FadeOutDown,
|
||||
LinearTransition,
|
||||
} from 'react-native-reanimated'
|
||||
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
|
||||
import { useStorageContext } from '../../providers/Storage'
|
||||
|
||||
/**
|
||||
@@ -38,9 +32,15 @@ import { useStorageContext } from '../../providers/Storage'
|
||||
export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
|
||||
|
||||
const api = useApi()
|
||||
const api = getApi()
|
||||
|
||||
const { data: discs, isPending } = useQuery({
|
||||
const theme = useTheme()
|
||||
|
||||
const {
|
||||
data: discs,
|
||||
isPending,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.ItemTracks, album.Id],
|
||||
queryFn: () => fetchAlbumDiscs(api, album),
|
||||
})
|
||||
@@ -144,17 +144,20 @@ export function Album({ album }: { album: BaseItemDto }): React.JSX.Element {
|
||||
queue={album}
|
||||
/>
|
||||
)}
|
||||
ListFooterComponent={() => <AlbumTrackListFooter album={album} />}
|
||||
ListFooterComponent={() => <AlbumTrackListFooter album={album} freeze={isPending} />}
|
||||
ListEmptyComponent={() => (
|
||||
<YStack flex={1} alignContent='center' margin={'$4'}>
|
||||
{isPending ? (
|
||||
<Spinner color={'$primary'} />
|
||||
) : (
|
||||
<Text color={'$borderColor'}>No album tracks</Text>
|
||||
)}
|
||||
{isPending ? null : <Text color={'$borderColor'}>No album tracks</Text>}
|
||||
</YStack>
|
||||
)}
|
||||
onScrollBeginDrag={closeAllSwipeableRows}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isPending}
|
||||
onRefresh={refetch}
|
||||
tintColor={theme.primary.val}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
|
||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||
import useLibraryStore from '../../stores/library'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import MAX_ITEMS_IN_RECYCLE_POOL from '../../configs/library.config'
|
||||
|
||||
interface AlbumsProps {
|
||||
albumsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
@@ -147,6 +148,7 @@ export default function Albums({
|
||||
}}
|
||||
onScrollBeginDrag={closeAllSwipeableRows}
|
||||
removeClippedSubviews
|
||||
maxItemsInRecyclePool={MAX_ITEMS_IN_RECYCLE_POOL}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && albumPageParams && (
|
||||
|
||||
@@ -2,45 +2,50 @@ import React from 'react'
|
||||
import { useArtistContext } from '../../providers/Artist'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '@/src/screens/types'
|
||||
import { DefaultSectionT, SectionList, SectionListData } from 'react-native'
|
||||
import { DefaultSectionT, RefreshControl, SectionList, SectionListData } from 'react-native'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import ArtistHeader from './header'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import SimilarArtists from './similar'
|
||||
import { Spinner, useTheme, YStack } from 'tamagui'
|
||||
|
||||
export default function ArtistOverviewTab({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<BaseStackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { featuredOn, artist, albums } = useArtistContext()
|
||||
const { featuredOn, albums, fetchingAlbums, refresh } = useArtistContext()
|
||||
|
||||
const sections: SectionListData<BaseItemDto>[] = [
|
||||
{
|
||||
title: 'Albums',
|
||||
data: albums?.filter(({ ChildCount }) => (ChildCount ?? 0) > 6) ?? [],
|
||||
},
|
||||
{
|
||||
title: 'EPs',
|
||||
data:
|
||||
albums?.filter(
|
||||
({ ChildCount }) => (ChildCount ?? 0) <= 6 && (ChildCount ?? 0) >= 3,
|
||||
) ?? [],
|
||||
},
|
||||
{
|
||||
title: 'Singles',
|
||||
data: albums?.filter(({ ChildCount }) => (ChildCount ?? 0) === 1) ?? [],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: albums?.filter(({ ChildCount }) => typeof ChildCount !== 'number') ?? [],
|
||||
},
|
||||
{
|
||||
title: 'Featured On',
|
||||
data: featuredOn ?? [],
|
||||
},
|
||||
]
|
||||
const theme = useTheme()
|
||||
|
||||
const sections: SectionListData<BaseItemDto>[] = albums
|
||||
? [
|
||||
{
|
||||
title: 'Albums',
|
||||
data: albums.filter(({ ChildCount }) => (ChildCount ?? 0) > 6) ?? [],
|
||||
},
|
||||
{
|
||||
title: 'EPs',
|
||||
data:
|
||||
albums.filter(
|
||||
({ ChildCount }) => (ChildCount ?? 0) <= 6 && (ChildCount ?? 0) >= 3,
|
||||
) ?? [],
|
||||
},
|
||||
{
|
||||
title: 'Singles',
|
||||
data: albums.filter(({ ChildCount }) => (ChildCount ?? 0) === 1) ?? [],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
data: albums.filter(({ ChildCount }) => typeof ChildCount !== 'number') ?? [],
|
||||
},
|
||||
{
|
||||
title: 'Featured On',
|
||||
data: featuredOn ?? [],
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
const renderSectionHeader = ({
|
||||
section,
|
||||
@@ -60,7 +65,23 @@ export default function ArtistOverviewTab({
|
||||
ListHeaderComponent={ArtistHeader}
|
||||
renderSectionHeader={renderSectionHeader}
|
||||
renderItem={({ item }) => <ItemRow item={item} navigation={navigation} />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={fetchingAlbums}
|
||||
onRefresh={refresh}
|
||||
tintColor={theme.primary.val}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={SimilarArtists}
|
||||
ListEmptyComponent={
|
||||
<YStack justifyContent='center' alignContent='center'>
|
||||
{fetchingAlbums ? (
|
||||
<Spinner color={'$primary'} flex={1} />
|
||||
) : (
|
||||
<Text color={'$neutral'}>No albums</Text>
|
||||
)}
|
||||
</YStack>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '@/src/screens/types'
|
||||
import IconButton from '../Global/helpers/icon-button'
|
||||
import { fetchAlbumDiscs } from '../../api/queries/item'
|
||||
import { useLoadNewQueue } from '../../providers/Player/hooks/callbacks'
|
||||
import { useLoadNewQueue } from '../../hooks/player/callbacks'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { getApi } from '../../stores'
|
||||
import Icon from '../Global/components/icon'
|
||||
|
||||
@@ -7,41 +7,46 @@ import { ActivityIndicator } from 'react-native'
|
||||
import { YStack } from 'tamagui'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import React from 'react'
|
||||
import { Freeze } from 'react-freeze'
|
||||
|
||||
export default function SimilarArtists(): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<BaseStackParamList>>()
|
||||
const { artist, similarArtists, fetchingSimilarArtists } = useArtistContext()
|
||||
const { artist, similarArtists, fetchingSimilarArtists, fetchingAlbums, albums } =
|
||||
useArtistContext()
|
||||
|
||||
return (
|
||||
<YStack flex={1}>
|
||||
<Text
|
||||
margin={'$2'}
|
||||
fontSize={'$6'}
|
||||
bold
|
||||
>{`Similar to ${artist.Name ?? 'Unknown Artist'}`}</Text>
|
||||
<Freeze freeze={fetchingAlbums && !albums}>
|
||||
<YStack flex={1}>
|
||||
<Text
|
||||
margin={'$2'}
|
||||
fontSize={'$6'}
|
||||
bold
|
||||
>{`Similar to ${artist.Name ?? 'Unknown Artist'}`}</Text>
|
||||
|
||||
<FlashList
|
||||
data={similarArtists}
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemRow
|
||||
item={artist}
|
||||
onPress={() => {
|
||||
navigation.push('Artist', {
|
||||
artist,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
fetchingSimilarArtists ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text justify={'center'} textAlign='center'>
|
||||
No similar artists
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</YStack>
|
||||
<FlashList
|
||||
data={similarArtists}
|
||||
renderItem={({ item: artist }) => (
|
||||
<ItemRow
|
||||
item={artist}
|
||||
onPress={() => {
|
||||
navigation.push('Artist', {
|
||||
artist,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
fetchingSimilarArtists ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text justify={'center'} textAlign='center'>
|
||||
No similar artists
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</YStack>
|
||||
</Freeze>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
|
||||
import { closeAllSwipeableRows } from '../Global/components/swipeable-row-registry'
|
||||
import useLibraryStore from '../../stores/library'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import MAX_ITEMS_IN_RECYCLE_POOL from '../../configs/library.config'
|
||||
|
||||
export interface ArtistsProps {
|
||||
artistsInfiniteQuery: UseInfiniteQueryResult<
|
||||
@@ -162,6 +163,7 @@ export default function Artists({
|
||||
}}
|
||||
onScrollBeginDrag={closeAllSwipeableRows}
|
||||
removeClippedSubviews
|
||||
maxItemsInRecyclePool={MAX_ITEMS_IN_RECYCLE_POOL}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && artistPageParams && (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import { CarPlay, ListTemplate } from 'react-native-carplay'
|
||||
import CarPlayNowPlaying from './NowPlaying'
|
||||
import { loadQueue } from '../../providers/Player/functions/queue'
|
||||
import { loadQueue } from '../../hooks/player/functions/queue'
|
||||
import formatArtistNames from '../../utils/formatting/artist-names'
|
||||
|
||||
const AlbumTemplate = (
|
||||
|
||||
@@ -45,6 +45,9 @@ const CarPlayHome = new ListTemplate({
|
||||
switch (index) {
|
||||
case 0: {
|
||||
// Recent Artists
|
||||
|
||||
await queryClient.ensureInfiniteQueryData(PlayItAgainQuery(library))
|
||||
|
||||
const artists = queryClient.getQueryData<InfiniteData<BaseItemDto[], unknown>>(
|
||||
RecentlyPlayedArtistsQueryKey(user, library),
|
||||
) ?? { pages: [], pageParams: [] }
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AlbumDiscsQuery } from '../../api/queries/album'
|
||||
import { getApi } from '../../stores'
|
||||
import AlbumTemplate from './Album'
|
||||
import { AlbumDiscsQueryKey } from '../../api/queries/album/keys'
|
||||
import { loadQueue } from '../../providers/Player/functions/queue'
|
||||
import { loadQueue } from '../../hooks/player/functions/queue'
|
||||
|
||||
const TracksTemplate = (items: BaseItemDto[], queuingRef: Queue) =>
|
||||
new ListTemplate({
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { AddToQueueMutation } from '../../providers/Player/interfaces'
|
||||
import { AddToQueueMutation } from '../../hooks/player/interfaces'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { useEffect } from 'react'
|
||||
import navigationRef from '../../../navigation'
|
||||
@@ -23,7 +23,7 @@ import ItemImage from '../Global/components/image'
|
||||
import { StackActions } from '@react-navigation/native'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import { useAddToQueue } from '../../providers/Player/hooks/callbacks'
|
||||
import { useAddToQueue } from '../../hooks/player/callbacks'
|
||||
import { useIsDownloaded } from '../../api/queries/download'
|
||||
import { useDeleteDownloads } from '../../api/mutations/download'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
@@ -119,8 +119,7 @@ export default function ItemContext({
|
||||
|
||||
{renderAddToPlaylistRow && (
|
||||
<AddToPlaylistRow
|
||||
track={isTrack ? item : undefined}
|
||||
tracks={isAlbum && discs ? discs.flatMap((d) => d.data) : undefined}
|
||||
tracks={isAlbum && discs ? discs.flatMap((d) => d.data) : [item]}
|
||||
source={isAlbum ? item : undefined}
|
||||
/>
|
||||
)}
|
||||
@@ -148,12 +147,10 @@ export default function ItemContext({
|
||||
}
|
||||
|
||||
function AddToPlaylistRow({
|
||||
track,
|
||||
tracks,
|
||||
source,
|
||||
}: {
|
||||
track?: BaseItemDto
|
||||
tracks?: BaseItemDto[]
|
||||
tracks: BaseItemDto[]
|
||||
source?: BaseItemDto
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
@@ -167,7 +164,6 @@ function AddToPlaylistRow({
|
||||
navigationRef.goBack()
|
||||
navigationRef.dispatch(
|
||||
StackActions.push('AddToPlaylist', {
|
||||
track,
|
||||
tracks,
|
||||
source,
|
||||
}),
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useNetworkStatus } from '../../../../stores/network'
|
||||
import navigationRef from '../../../../../navigation'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '../../../../screens/types'
|
||||
import { useAddToQueue, useLoadNewQueue } from '../../../../providers/Player/hooks/callbacks'
|
||||
import { useAddToQueue, useLoadNewQueue } from '../../../../hooks/player/callbacks'
|
||||
import { useDownloadedTrack } from '../../../../api/queries/download'
|
||||
import SwipeableRow from '../SwipeableRow'
|
||||
import { useSwipeSettingsStore } from '../../../../stores/settings/swipe'
|
||||
@@ -158,7 +158,7 @@ export default function Track({
|
||||
},
|
||||
addToPlaylist: () => {
|
||||
console.info('Running add to playlist swipe handler')
|
||||
navigationRef.dispatch(StackActions.push('AddToPlaylist', { track }))
|
||||
navigationRef.dispatch(StackActions.push('AddToPlaylist', { tracks: [track] }))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import FavoriteIcon from './favorite-icon'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '../../../screens/types'
|
||||
import { useAddToQueue, useLoadNewQueue } from '../../../providers/Player/hooks/callbacks'
|
||||
import { useAddToQueue, useLoadNewQueue } from '../../../hooks/player/callbacks'
|
||||
import useItemContext from '../../../hooks/use-item-context'
|
||||
import { RouteProp, useRoute } from '@react-navigation/native'
|
||||
import React from 'react'
|
||||
@@ -132,7 +132,7 @@ function ItemRow({
|
||||
queuingType: QueuingType.DirectlyQueued,
|
||||
}),
|
||||
toggleFavorite: () => (isFavorite ? removeFavorite({ item }) : addFavorite({ item })),
|
||||
addToPlaylist: () => navigationRef.navigate('AddToPlaylist', { track: item }),
|
||||
addToPlaylist: () => navigationRef.navigate('AddToPlaylist', { tracks: [item] }),
|
||||
})
|
||||
|
||||
const swipeConfig = isAudio
|
||||
|
||||
@@ -4,7 +4,7 @@ import HorizontalCardList from '../../../components/Global/components/horizontal
|
||||
import ItemCard from '../../../components/Global/components/item-card'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useLoadNewQueue } from '../../../providers/Player/hooks/callbacks'
|
||||
import { useLoadNewQueue } from '../../../hooks/player/callbacks'
|
||||
import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
import HomeStackParamList from '../../../screens/Home/types'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useLoadNewQueue } from '../../../providers/Player/hooks/callbacks'
|
||||
import { useLoadNewQueue } from '../../../hooks/player/callbacks'
|
||||
import { useDisplayContext } from '../../../providers/Display/display-provider'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import HomeStackParamList from '../../../screens/Home/types'
|
||||
|
||||
@@ -2,8 +2,8 @@ import { State } from 'react-native-track-player'
|
||||
import { Circle, Spinner, View } from 'tamagui'
|
||||
import IconButton from '../../../components/Global/helpers/icon-button'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useTogglePlayback } from '../../../providers/Player/hooks/callbacks'
|
||||
import { usePlaybackState } from '../../../providers/Player/hooks/queries'
|
||||
import { useTogglePlayback } from '../../../hooks/player/callbacks'
|
||||
import { usePlaybackState } from '../../../hooks/player/queries'
|
||||
import React from 'react'
|
||||
import Icon from '../../Global/components/icon'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
useSkip,
|
||||
useToggleRepeatMode,
|
||||
useToggleShuffle,
|
||||
} from '../../../providers/Player/hooks/callbacks'
|
||||
} from '../../../hooks/player/callbacks'
|
||||
import { useRepeatModeStoreValue, useShuffle } from '../../../stores/player/queue'
|
||||
|
||||
export default function Controls(): React.JSX.Element {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { Text, useWindowDimensions, View, YStack, ZStack, useTheme, XStack, Spacer } from 'tamagui'
|
||||
import BlurredBackground from './blurred-background'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useProgress } from '../../../providers/Player/hooks/queries'
|
||||
import { useSeekTo } from '../../../providers/Player/hooks/callbacks'
|
||||
import { useProgress } from '../../../hooks/player/queries'
|
||||
import { useSeekTo } from '../../../hooks/player/callbacks'
|
||||
import { UPDATE_INTERVAL } from '../../../configs/player.config'
|
||||
import React, { useEffect, useMemo, useRef, useCallback } from 'react'
|
||||
import Animated, {
|
||||
|
||||
@@ -25,8 +25,6 @@ export default function QualityBadge({
|
||||
|
||||
return bitrate && container ? (
|
||||
<Square
|
||||
enterStyle={{ opacity: 1 }}
|
||||
exitStyle={{ opacity: 0 }}
|
||||
animation={'bouncy'}
|
||||
justifyContent='center'
|
||||
backgroundColor={'$primary'}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { HorizontalSlider } from '../../../components/Global/helpers/slider'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { Spacer, XStack, YStack } from 'tamagui'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { useSeekTo } from '../../../providers/Player/hooks/callbacks'
|
||||
import { useSeekTo } from '../../../hooks/player/callbacks'
|
||||
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
|
||||
import { UPDATE_INTERVAL } from '../../../configs/player.config'
|
||||
import { ProgressMultiplier } from '../component.config'
|
||||
import { useProgress } from '../../../providers/Player/hooks/queries'
|
||||
import { useProgress } from '../../../hooks/player/queries'
|
||||
import QualityBadge from './quality-badge'
|
||||
import { useDisplayAudioQualityBadge } from '../../../stores/settings/player'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Gesture } from 'react-native-gesture-handler'
|
||||
import { useSharedValue, withDelay, withSpring } from 'react-native-reanimated'
|
||||
import type { SharedValue } from 'react-native-reanimated'
|
||||
import { runOnJS } from 'react-native-worklets'
|
||||
import { usePrevious, useSkip } from '../../../providers/Player/hooks/callbacks'
|
||||
import { usePrevious, useSkip } from '../../../hooks/player/callbacks'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
import { useCurrentTrack } from '../../../stores/player/queue'
|
||||
import { useApi } from '../../../stores'
|
||||
|
||||
@@ -18,7 +18,7 @@ import Animated, {
|
||||
} from 'react-native-reanimated'
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import { runOnJS } from 'react-native-worklets'
|
||||
import { usePrevious, useSkip } from '../../providers/Player/hooks/callbacks'
|
||||
import { usePrevious, useSkip } from '../../hooks/player/callbacks'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { useCurrentTrack } from '../../stores/player/queue'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PlayPauseIcon } from './components/buttons'
|
||||
import { TextTickerConfig } from './component.config'
|
||||
import { UPDATE_INTERVAL } from '../../configs/player.config'
|
||||
import { Progress as TrackPlayerProgress } from 'react-native-track-player'
|
||||
import { useProgress } from '../../providers/Player/hooks/queries'
|
||||
import { useProgress } from '../../hooks/player/queries'
|
||||
|
||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
@@ -23,7 +23,7 @@ import { runOnJS } from 'react-native-worklets'
|
||||
import { RootStackParamList } from '../../screens/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import ItemImage from '../Global/components/image'
|
||||
import { usePrevious, useSkip } from '../../providers/Player/hooks/callbacks'
|
||||
import { usePrevious, useSkip } from '../../hooks/player/callbacks'
|
||||
import { useCurrentTrack } from '../../stores/player/queue'
|
||||
|
||||
export default function Miniplayer(): React.JSX.Element {
|
||||
@@ -83,18 +83,17 @@ export default function Miniplayer(): React.JSX.Element {
|
||||
<Animated.View
|
||||
collapsable={false}
|
||||
testID='miniplayer-test-id'
|
||||
entering={FadeInDown.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOutDown.easing(Easing.out(Easing.ease))}
|
||||
entering={FadeInDown.springify()}
|
||||
exiting={FadeOutDown.springify()}
|
||||
>
|
||||
<YStack>
|
||||
<YStack
|
||||
pressStyle={pressStyle}
|
||||
animation={'quick'}
|
||||
onPress={openPlayer}
|
||||
backgroundColor='$background'
|
||||
>
|
||||
<MiniPlayerProgress />
|
||||
<XStack
|
||||
alignItems='center'
|
||||
pressStyle={pressStyle}
|
||||
animation={'quick'}
|
||||
onPress={openPlayer}
|
||||
padding={'$2'}
|
||||
>
|
||||
<XStack alignItems='center' padding={'$2'}>
|
||||
<YStack justify='center' alignItems='center'>
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
useRemoveUpcomingTracks,
|
||||
useReorderQueue,
|
||||
useSkip,
|
||||
} from '../../providers/Player/hooks/callbacks'
|
||||
} from '../../hooks/player/callbacks'
|
||||
import { usePlayerQueueStore, useQueueRef } from '../../stores/player/queue'
|
||||
import Sortable from 'react-native-sortables'
|
||||
import { OrderChangeParams, RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useNetworkStatus } from '../../../stores/network'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import LibraryStackParamList from '@/src/screens/Library/types'
|
||||
import { useLoadNewQueue } from '../../../providers/Player/hooks/callbacks'
|
||||
import { useLoadNewQueue } from '../../../hooks/player/callbacks'
|
||||
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
||||
import ItemImage from '../../Global/components/image'
|
||||
import { useApi } from '../../../stores'
|
||||
|
||||
@@ -11,8 +11,7 @@ import { RenderItemInfo } from 'react-native-sortables/dist/typescript/types'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||
import PlaylistTracklistHeader from './components/header'
|
||||
import navigationRef from '../../../navigation'
|
||||
import { useLoadNewQueue } from '../../providers/Player/hooks/callbacks'
|
||||
import { useNetworkStatus } from '../../stores/network'
|
||||
import { useLoadNewQueue } from '../../hooks/player/callbacks'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { useApi } from '../../stores'
|
||||
import useStreamingDeviceProfile from '../../stores/device-profile'
|
||||
@@ -20,13 +19,11 @@ import { useEffect, useLayoutEffect, useState } from 'react'
|
||||
import { updatePlaylist } from '../../../src/api/mutations/playlists'
|
||||
import { usePlaylistTracks } from '../../../src/api/queries/playlist'
|
||||
import useHapticFeedback from '../../hooks/use-haptic-feedback'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { InfiniteData, useMutation } from '@tanstack/react-query'
|
||||
import Animated, {
|
||||
Easing,
|
||||
FadeIn,
|
||||
FadeInUp,
|
||||
FadeOut,
|
||||
FadeOutDown,
|
||||
LinearTransition,
|
||||
SlideInLeft,
|
||||
SlideOutRight,
|
||||
@@ -37,6 +34,8 @@ import { RefreshControl } from 'react-native'
|
||||
import { useIsDownloaded } from '../../api/queries/download'
|
||||
import useAddToPendingDownloads, { useIsDownloading } from '../../stores/network/downloads'
|
||||
import { useStorageContext } from '../../providers/Storage'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { PlaylistTracksQueryKey } from '../../api/queries/playlist/keys'
|
||||
|
||||
export default function Playlist({
|
||||
playlist,
|
||||
@@ -85,11 +84,23 @@ export default function Playlist({
|
||||
tracks.map((track) => track.Id!),
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (_, { playlist, tracks }) => {
|
||||
trigger('notificationSuccess')
|
||||
|
||||
// Refresh playlist component data
|
||||
refetch()
|
||||
queryClient.setQueryData<InfiniteData<BaseItemDto[]>>(
|
||||
PlaylistTracksQueryKey(playlist),
|
||||
(prev) => {
|
||||
if (!prev) return prev
|
||||
|
||||
return {
|
||||
...prev,
|
||||
pages: prev.pages.map((page: BaseItemDto[]) =>
|
||||
page.filter((track) => tracks.some((t) => t.Id === track.Id)),
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
onError: () => {
|
||||
trigger('notificationError')
|
||||
|
||||
@@ -14,6 +14,7 @@ import { closeAllSwipeableRows } from '../Global/components/swipeable-row-regist
|
||||
import FlashListStickyHeader from '../Global/helpers/flashlist-sticky-header'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import ItemRow from '../Global/components/item-row'
|
||||
import MAX_ITEMS_IN_RECYCLE_POOL from '../../configs/library.config'
|
||||
|
||||
interface TracksProps {
|
||||
tracksInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>
|
||||
@@ -68,24 +69,30 @@ export default function Tracks({
|
||||
}: {
|
||||
index: number
|
||||
item: string | number | BaseItemDto
|
||||
}) =>
|
||||
typeof track === 'string' ? (
|
||||
<FlashListStickyHeader text={track.toUpperCase()} />
|
||||
) : typeof track === 'number' ? null : typeof track === 'object' ? (
|
||||
track.Type === BaseItemKind.Audio ? (
|
||||
<Track
|
||||
navigation={navigation}
|
||||
showArtwork
|
||||
index={0}
|
||||
track={track}
|
||||
testID={`track-item-${index}`}
|
||||
tracklist={tracks.slice(tracks.indexOf(track), tracks.indexOf(track) + 50)}
|
||||
queue={queue}
|
||||
/>
|
||||
) : (
|
||||
<ItemRow navigation={navigation} item={track} />
|
||||
)
|
||||
) : null
|
||||
}) => {
|
||||
switch (typeof track) {
|
||||
case 'string':
|
||||
return <FlashListStickyHeader text={track.toUpperCase()} />
|
||||
case 'object':
|
||||
return track.Type === BaseItemKind.Audio ? (
|
||||
<Track
|
||||
navigation={navigation}
|
||||
showArtwork
|
||||
index={0}
|
||||
track={track}
|
||||
testID={`track-item-${index}`}
|
||||
tracklist={tracks.slice(tracks.indexOf(track), tracks.indexOf(track) + 50)}
|
||||
queue={queue}
|
||||
/>
|
||||
) : (
|
||||
<ItemRow navigation={navigation} item={track} />
|
||||
)
|
||||
|
||||
case 'number':
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const ItemSeparatorComponent = ({
|
||||
leadingItem,
|
||||
@@ -171,6 +178,7 @@ export default function Tracks({
|
||||
</YStack>
|
||||
}
|
||||
removeClippedSubviews
|
||||
maxItemsInRecyclePool={MAX_ITEMS_IN_RECYCLE_POOL}
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && trackPageParams && (
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* The maximum number of items that will be in
|
||||
* a library list's recycle pool
|
||||
*/
|
||||
const MAX_ITEMS_IN_RECYCLE_POOL = 40
|
||||
|
||||
export default MAX_ITEMS_IN_RECYCLE_POOL
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Platform } from 'react-native'
|
||||
import { Event } from 'react-native-track-player'
|
||||
|
||||
/**
|
||||
* Interval in milliseconds for progress updates from the track player
|
||||
@@ -28,3 +29,9 @@ export const BUFFERS =
|
||||
backBuffer: 5, // 5 seconds back buffer
|
||||
}
|
||||
: {}
|
||||
|
||||
export const PLAYER_EVENTS: Event[] = [
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
Event.PlaybackProgressUpdated,
|
||||
Event.PlaybackState,
|
||||
]
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import TrackPlayer, { RepeatMode, State } from 'react-native-track-player'
|
||||
import { loadQueue, playLaterInQueue, playNextInQueue } from '../functions/queue'
|
||||
import { previous, skip } from '../functions/controls'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from '../interfaces'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { loadQueue, playLaterInQueue, playNextInQueue } from './functions/queue'
|
||||
import { previous, skip } from './functions/controls'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { handleDeshuffle, handleShuffle } from '../functions/shuffle'
|
||||
import { handleDeshuffle, handleShuffle } from './functions/shuffle'
|
||||
import JellifyTrack from '@/src/types/JellifyTrack'
|
||||
import calculateTrackVolume from '../utils/normalization'
|
||||
import usePlayerEngineStore, { PlayerEngine } from '../../../stores/player/engine'
|
||||
import calculateTrackVolume from './functions/normalization'
|
||||
import usePlayerEngineStore, { PlayerEngine } from '../../stores/player/engine'
|
||||
import { useRemoteMediaClient } from 'react-native-google-cast'
|
||||
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
import useHapticFeedback from '../use-haptic-feedback'
|
||||
import { usePlayerQueueStore } from '../../stores/player/queue'
|
||||
|
||||
/**
|
||||
* A mutation to handle toggling the playback state
|
||||
@@ -151,7 +151,6 @@ export const useLoadNewQueue = () => {
|
||||
const trigger = useHapticFeedback()
|
||||
return async (variables: QueueMutation) => {
|
||||
trigger('impactLight')
|
||||
await TrackPlayer.pause()
|
||||
const { finalStartIndex, tracks } = await loadQueue({ ...variables })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { isUndefined } from 'lodash'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from '../../../configs/player.config'
|
||||
import TrackPlayer, { State } from 'react-native-track-player'
|
||||
|
||||
/**
|
||||
* A function that will skip to the previous track if
|
||||
* we are still at the beginning of the track.
|
||||
*
|
||||
* This behavior is configured via {@link SKIP_TO_PREVIOUS_THRESHOLD},
|
||||
* which determines how many seconds until we will instead skip to the
|
||||
* beginning of the track for convenience.
|
||||
*
|
||||
* Stops buffering the current track for performance.
|
||||
*
|
||||
* Starts playback at the end of the operation.
|
||||
*/
|
||||
export async function previous(): Promise<void> {
|
||||
const { position } = await TrackPlayer.getProgress()
|
||||
|
||||
if (Math.floor(position) < SKIP_TO_PREVIOUS_THRESHOLD) {
|
||||
await TrackPlayer.stop() // Stop buffering the current track
|
||||
await TrackPlayer.skipToPrevious()
|
||||
} else await TrackPlayer.seekTo(0)
|
||||
|
||||
const { state } = await TrackPlayer.getPlaybackState()
|
||||
|
||||
if (state !== State.Playing) await TrackPlayer.play()
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that will skip to the next track or the specified
|
||||
* track index.
|
||||
*
|
||||
* Stops buffering the current track for performance.
|
||||
*
|
||||
* Starts playback at the end of the operation.
|
||||
*
|
||||
* @param index The track index to skip to, to skip multiple tracks
|
||||
*/
|
||||
export async function skip(index: number | undefined): Promise<void> {
|
||||
await TrackPlayer.stop() // Stop buffering the current track
|
||||
|
||||
if (!isUndefined(index)) await TrackPlayer.skip(index)
|
||||
else await TrackPlayer.skipToNext()
|
||||
|
||||
const { state } = await TrackPlayer.getPlaybackState()
|
||||
if (state !== State.Playing) await TrackPlayer.play()
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
|
||||
export function handleActiveTrackChanged(
|
||||
activeTrack: JellifyTrack | undefined,
|
||||
activeIndex: number | undefined,
|
||||
): void {
|
||||
usePlayerQueueStore.getState().setCurrentTrack(activeTrack as JellifyTrack)
|
||||
usePlayerQueueStore.getState().setCurrentIndex(activeIndex)
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { mapDtoToTrack } from '../../../utils/mapping/item-to-track'
|
||||
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
|
||||
import { filterTracksOnNetworkStatus } from '../utils/queue'
|
||||
import { filterTracksOnNetworkStatus } from './utils/queue'
|
||||
import { AddToQueueMutation, QueueMutation } from '../interfaces'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { shuffleJellifyTracks } from '../utils/shuffle'
|
||||
import { shuffleJellifyTracks } from './utils/shuffle'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { getCurrentTrack } from '.'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
import { getAudioCache } from '../../../api/mutations/download/offlineModeUtils'
|
||||
import { isUndefined } from 'lodash'
|
||||
@@ -25,6 +24,8 @@ export async function loadQueue({
|
||||
shuffled = false,
|
||||
startPlayback,
|
||||
}: QueueMutation): Promise<LoadQueueResult> {
|
||||
await TrackPlayer.stop()
|
||||
|
||||
const deviceProfile = useStreamingDeviceProfileStore.getState().deviceProfile!
|
||||
const networkStatus = useNetworkStore.getState().networkStatus ?? networkStatusTypes.ONLINE
|
||||
|
||||
@@ -61,8 +62,6 @@ export async function loadQueue({
|
||||
// The start index for the shuffled queue is always 0 (starting track is first)
|
||||
const finalStartIndex = availableAudioItems.findIndex((item) => item.Id === startingTrack.Id)
|
||||
|
||||
await TrackPlayer.stop()
|
||||
|
||||
/**
|
||||
* Keep the requested track as the currently playing track so there
|
||||
* isn't any flickering in the miniplayer
|
||||
@@ -120,13 +119,17 @@ export const playNextInQueue = async ({ tracks }: AddToQueueMutation) => {
|
||||
.getState()
|
||||
.unShuffledQueue.slice(
|
||||
0,
|
||||
usePlayerQueueStore.getState().unShuffledQueue.indexOf(getCurrentTrack()!) + 1,
|
||||
usePlayerQueueStore
|
||||
.getState()
|
||||
.unShuffledQueue.indexOf(currentQueue[currentIndex!]) + 1,
|
||||
),
|
||||
...tracksToPlayNext,
|
||||
...usePlayerQueueStore
|
||||
.getState()
|
||||
.unShuffledQueue.slice(
|
||||
usePlayerQueueStore.getState().unShuffledQueue.indexOf(getCurrentTrack()!) + 1,
|
||||
usePlayerQueueStore
|
||||
.getState()
|
||||
.unShuffledQueue.indexOf(currentQueue[currentIndex!]) + 1,
|
||||
),
|
||||
])
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { shuffleJellifyTracks } from '../utils/shuffle'
|
||||
import { shuffleJellifyTracks } from './utils/shuffle'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { cloneDeep, isUndefined } from 'lodash'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
@@ -1,9 +1,9 @@
|
||||
import _, { isNull, isUndefined } from 'lodash'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import JellifyTrack from '../../../../types/JellifyTrack'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { JellifyDownload } from '../../../types/JellifyDownload'
|
||||
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { JellifyDownload } from '../../../../types/JellifyDownload'
|
||||
import { networkStatusTypes } from '../../../../components/Network/internetConnectionWatcher'
|
||||
import { QueuingType } from '../../../../enums/queuing-type'
|
||||
|
||||
export function buildNewQueue(
|
||||
existingQueue: JellifyTrack[],
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { QueuingType } from '../../../../enums/queuing-type'
|
||||
import JellifyTrack from '../../../../types/JellifyTrack'
|
||||
import { fetchManuallyQueuedTracks } from './queue'
|
||||
|
||||
export function shuffleJellifyTracks(tracks: JellifyTrack[]): {
|
||||
@@ -1,9 +1,6 @@
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { BaseItemDto, DeviceProfile } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Queue } from '../../player/types/queue-item'
|
||||
import { Api } from '@jellyfin/sdk'
|
||||
import { networkStatusTypes } from '../../components/Network/internetConnectionWatcher'
|
||||
import { JellifyDownload } from '@/src/types/JellifyDownload'
|
||||
|
||||
/**
|
||||
* A mutation to handle loading a new queue.
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
useProgress as useProgressRNTP,
|
||||
usePlaybackState as usePlaybackStateRNTP,
|
||||
} from 'react-native-track-player'
|
||||
import usePlayerEngineStore from '../../../stores/player/engine'
|
||||
import { PlayerEngine } from '../../../stores/player/engine'
|
||||
import usePlayerEngineStore from '../../stores/player/engine'
|
||||
import { PlayerEngine } from '../../stores/player/engine'
|
||||
import { MediaPlayerState, useRemoteMediaClient, useStreamPosition } from 'react-native-google-cast'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
@@ -8,23 +8,25 @@ import { fetchAlbumDiscs, fetchItem } from '../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import fetchUserData from '../api/queries/user-data/utils'
|
||||
import { useRef } from 'react'
|
||||
import useStreamingDeviceProfile, { useDownloadingDeviceProfile } from '../stores/device-profile'
|
||||
import UserDataQueryKey from '../api/queries/user-data/keys'
|
||||
import MediaInfoQueryKey from '../api/queries/media/keys'
|
||||
import useJellifyStore, { getApi } from '../stores'
|
||||
import { getApi, getUser } from '../stores'
|
||||
import {
|
||||
useDownloadingDeviceProfileStore,
|
||||
useStreamingDeviceProfileStore,
|
||||
} from '../stores/device-profile'
|
||||
|
||||
export default function useItemContext(): (item: BaseItemDto) => void {
|
||||
const api = getApi()
|
||||
const user = useJellifyStore.getState().user
|
||||
|
||||
const streamingDeviceProfile = useStreamingDeviceProfile()
|
||||
|
||||
const downloadingDeviceProfile = useDownloadingDeviceProfile()
|
||||
const user = getUser()
|
||||
|
||||
const prefetchedContext = useRef<Set<string>>(new Set())
|
||||
|
||||
return (item: BaseItemDto) => {
|
||||
const effectSig = `${item.Id}-${item.Type}`
|
||||
const streamingDeviceProfile = useStreamingDeviceProfileStore.getState().deviceProfile
|
||||
const downloadingDeviceProfile = useDownloadingDeviceProfileStore.getState().deviceProfile
|
||||
|
||||
const effectSig = `${item.Id}-${item.Type}-${streamingDeviceProfile.Id}-${downloadingDeviceProfile.Id}`
|
||||
|
||||
// If we've already warmed the cache for this item, return
|
||||
if (prefetchedContext.current.has(effectSig)) return
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import useAppActive from './use-app-active'
|
||||
import { useCurrentTrack } from '../stores/player/queue'
|
||||
import { useIsPlayerFocused } from '../stores/player/display'
|
||||
|
||||
export default function useIsMiniPlayerActive(): boolean {
|
||||
const isAppActive = useAppActive()
|
||||
|
||||
const nowPlaying = useCurrentTrack()
|
||||
|
||||
return !!nowPlaying && isAppActive
|
||||
const isPlayerFocused = useIsPlayerFocused()
|
||||
|
||||
return !!nowPlaying && isAppActive && !isPlayerFocused
|
||||
}
|
||||
|
||||
+3
-10
@@ -1,6 +1,6 @@
|
||||
import TrackPlayer, { Event } from 'react-native-track-player'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from '../configs/player.config'
|
||||
import { CarPlay } from 'react-native-carplay'
|
||||
import { previous, skip } from '../hooks/player/functions/controls'
|
||||
|
||||
/**
|
||||
* Jellify Playback Service.
|
||||
@@ -16,16 +16,9 @@ export async function PlaybackService() {
|
||||
await TrackPlayer.pause()
|
||||
})
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteNext, async () => {
|
||||
await TrackPlayer.skipToNext()
|
||||
})
|
||||
TrackPlayer.addEventListener(Event.RemoteNext, async () => skip(undefined))
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemotePrevious, async () => {
|
||||
const progress = await TrackPlayer.getProgress()
|
||||
|
||||
if (progress.position < SKIP_TO_PREVIOUS_THRESHOLD) await TrackPlayer.skipToPrevious()
|
||||
else await TrackPlayer.seekTo(0)
|
||||
})
|
||||
TrackPlayer.addEventListener(Event.RemotePrevious, previous)
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteSeek, async (event) => {
|
||||
await TrackPlayer.seekTo(event.position)
|
||||
|
||||
@@ -2,11 +2,10 @@ import fetchSimilarArtists from '../../api/queries/suggestions/utils/similar'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { createContext, ReactNode, useContext } from 'react'
|
||||
import { SharedValue, useSharedValue } from 'react-native-reanimated'
|
||||
import { createContext, ReactNode, use } from 'react'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { useArtistAlbums, useArtistFeaturedOn } from '../../api/queries/artist'
|
||||
import { useApi, useJellifyUser, useJellifyLibrary } from '../../stores'
|
||||
import { useJellifyLibrary, getApi, getUser } from '../../stores'
|
||||
|
||||
interface ArtistContext {
|
||||
fetchingAlbums: boolean
|
||||
@@ -17,7 +16,6 @@ interface ArtistContext {
|
||||
featuredOn: BaseItemDto[] | undefined
|
||||
similarArtists: BaseItemDto[] | undefined
|
||||
artist: BaseItemDto
|
||||
scroll: SharedValue<number>
|
||||
}
|
||||
|
||||
const ArtistContext = createContext<ArtistContext>({
|
||||
@@ -29,7 +27,6 @@ const ArtistContext = createContext<ArtistContext>({
|
||||
featuredOn: [],
|
||||
similarArtists: [],
|
||||
refresh: () => {},
|
||||
scroll: { value: 0 } as SharedValue<number>,
|
||||
})
|
||||
|
||||
export const ArtistProvider = ({
|
||||
@@ -39,8 +36,8 @@ export const ArtistProvider = ({
|
||||
artist: BaseItemDto
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const api = useApi()
|
||||
const [user] = useJellifyUser()
|
||||
const api = getApi()
|
||||
const user = getUser()
|
||||
const [library] = useJellifyLibrary()
|
||||
|
||||
const {
|
||||
@@ -71,8 +68,6 @@ export const ArtistProvider = ({
|
||||
refetchSimilar()
|
||||
}
|
||||
|
||||
const scroll = useSharedValue(0)
|
||||
|
||||
const value = {
|
||||
artist,
|
||||
albums,
|
||||
@@ -82,10 +77,9 @@ export const ArtistProvider = ({
|
||||
fetchingFeaturedOn,
|
||||
fetchingSimilarArtists,
|
||||
refresh,
|
||||
scroll,
|
||||
}
|
||||
|
||||
return <ArtistContext.Provider value={value}>{children}</ArtistContext.Provider>
|
||||
return <ArtistContext value={value}>{children}</ArtistContext>
|
||||
}
|
||||
|
||||
export const useArtistContext = () => useContext(ArtistContext)
|
||||
export const useArtistContext = () => use(ArtistContext)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import {
|
||||
ACTIVE_INDEX_QUERY_KEY,
|
||||
NOW_PLAYING_QUERY_KEY,
|
||||
PLAY_QUEUE_QUERY_KEY,
|
||||
REPEAT_MODE_QUERY_KEY,
|
||||
} from './query-keys'
|
||||
import TrackPlayer, { Track } from 'react-native-track-player'
|
||||
|
||||
const PLAYER_QUERY_OPTIONS = {
|
||||
enabled: true,
|
||||
retry: false,
|
||||
staleTime: Infinity,
|
||||
gcTime: Infinity,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
networkMode: 'always',
|
||||
} as const
|
||||
|
||||
export const QUEUE_QUERY = {
|
||||
queryKey: PLAY_QUEUE_QUERY_KEY,
|
||||
queryFn: TrackPlayer.getQueue,
|
||||
select: (data: Track[]) => data as JellifyTrack[],
|
||||
...PLAYER_QUERY_OPTIONS,
|
||||
}
|
||||
|
||||
export const CURRENT_INDEX_QUERY = {
|
||||
queryKey: ACTIVE_INDEX_QUERY_KEY,
|
||||
queryFn: TrackPlayer.getActiveTrackIndex,
|
||||
...PLAYER_QUERY_OPTIONS,
|
||||
}
|
||||
|
||||
export const NOW_PLAYING_QUERY = {
|
||||
queryKey: NOW_PLAYING_QUERY_KEY,
|
||||
queryFn: TrackPlayer.getActiveTrack,
|
||||
select: (data: Track | undefined) => data as JellifyTrack | undefined,
|
||||
...PLAYER_QUERY_OPTIONS,
|
||||
}
|
||||
|
||||
export const REPEAT_MODE_QUERY = {
|
||||
queryKey: REPEAT_MODE_QUERY_KEY,
|
||||
queryFn: TrackPlayer.getRepeatMode,
|
||||
...PLAYER_QUERY_OPTIONS,
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import PlayerQueryKeys from '../enums/queue-keys'
|
||||
|
||||
export const ACTIVE_INDEX_QUERY_KEY = [PlayerQueryKeys.ActiveIndex]
|
||||
|
||||
export const NOW_PLAYING_QUERY_KEY = [PlayerQueryKeys.NowPlaying]
|
||||
|
||||
export const PLAY_QUEUE_QUERY_KEY = [PlayerQueryKeys.PlayQueue]
|
||||
|
||||
export const QUEUE_REF_QUERY_KEY = [PlayerQueryKeys.PlayQueueRef]
|
||||
|
||||
export const REPEAT_MODE_QUERY_KEY = [PlayerQueryKeys.RepeatMode]
|
||||
|
||||
export const UNSHUFFLED_QUEUE_QUERY_KEY = [PlayerQueryKeys.UnshuffledQueue]
|
||||
|
||||
export const SHUFFLED_QUERY_KEY = [PlayerQueryKeys.Shuffled]
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* A prefix used for the player query keys
|
||||
*
|
||||
* This is used as to avoid collisions with other keys
|
||||
*/
|
||||
const QUERY_KEY_PREFEX = 'PLAYER'
|
||||
|
||||
enum PlayerQueryKeys {
|
||||
PlayQueue = QUERY_KEY_PREFEX + 'PLAY_QUEUE',
|
||||
NowPlaying = QUERY_KEY_PREFEX + 'NOW_PLAYING',
|
||||
ActiveIndex = QUERY_KEY_PREFEX + 'ACTIVE_INDEX',
|
||||
PlaybackState = QUERY_KEY_PREFEX + 'PLAYBACK_STATE',
|
||||
PlayQueueRef = QUERY_KEY_PREFEX + 'PlayQueueRef',
|
||||
UnshuffledQueue = QUERY_KEY_PREFEX + 'UNSHUFFLED_QUEUE',
|
||||
RepeatMode = QUERY_KEY_PREFEX + 'REPEAT_MODE',
|
||||
Shuffled = QUERY_KEY_PREFEX + 'SHUFFLED',
|
||||
}
|
||||
|
||||
export default PlayerQueryKeys
|
||||
@@ -1,18 +0,0 @@
|
||||
import { isUndefined } from 'lodash'
|
||||
import { SKIP_TO_PREVIOUS_THRESHOLD } from '../../../configs/player.config'
|
||||
import TrackPlayer, { State } from 'react-native-track-player'
|
||||
|
||||
export async function previous(): Promise<void> {
|
||||
const { position } = await TrackPlayer.getProgress()
|
||||
|
||||
if (Math.floor(position) < SKIP_TO_PREVIOUS_THRESHOLD) TrackPlayer.skipToPrevious()
|
||||
else await TrackPlayer.seekTo(0)
|
||||
}
|
||||
|
||||
export async function skip(index: number | undefined): Promise<void> {
|
||||
if (!isUndefined(index)) await TrackPlayer.skip(index)
|
||||
else await TrackPlayer.skipToNext()
|
||||
|
||||
const { state } = await TrackPlayer.getPlaybackState()
|
||||
if (state !== State.Playing) await TrackPlayer.play()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import JellifyTrack from '../../../types/JellifyTrack'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import {
|
||||
ACTIVE_INDEX_QUERY_KEY,
|
||||
NOW_PLAYING_QUERY_KEY,
|
||||
PLAY_QUEUE_QUERY_KEY,
|
||||
} from '../constants/query-keys'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
|
||||
export function getActiveIndex(): number | undefined {
|
||||
return queryClient.getQueryData(ACTIVE_INDEX_QUERY_KEY) as number | undefined
|
||||
}
|
||||
|
||||
export function setActiveIndex(index: number): void {
|
||||
queryClient.setQueryData(ACTIVE_INDEX_QUERY_KEY, index)
|
||||
}
|
||||
|
||||
export function getCurrentTrack(): JellifyTrack | undefined {
|
||||
return queryClient.getQueryData(NOW_PLAYING_QUERY_KEY)
|
||||
}
|
||||
|
||||
export function getPlayQueue(): JellifyTrack[] | undefined {
|
||||
return queryClient.getQueryData(PLAY_QUEUE_QUERY_KEY) as JellifyTrack[] | undefined
|
||||
}
|
||||
|
||||
export function setPlayQueue(tracks: JellifyTrack[]): void {
|
||||
queryClient.setQueryData(PLAY_QUEUE_QUERY_KEY, tracks)
|
||||
}
|
||||
|
||||
export async function handleActiveTrackChanged(): Promise<void> {
|
||||
const [queue, activeTrack, activeIndex] = await Promise.all([
|
||||
TrackPlayer.getQueue(),
|
||||
TrackPlayer.getActiveTrack(),
|
||||
TrackPlayer.getActiveTrackIndex(),
|
||||
])
|
||||
|
||||
usePlayerQueueStore.getState().setQueue(queue as JellifyTrack[])
|
||||
usePlayerQueueStore.getState().setCurrentTrack(activeTrack as JellifyTrack)
|
||||
usePlayerQueueStore.getState().setCurrentIndex(activeIndex)
|
||||
}
|
||||
@@ -1,26 +1,23 @@
|
||||
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
|
||||
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
|
||||
import { createContext, useEffect, useState } from 'react'
|
||||
import { handleActiveTrackChanged } from './functions'
|
||||
import { handleActiveTrackChanged } from '../../hooks/player/functions'
|
||||
import JellifyTrack from '../../types/JellifyTrack'
|
||||
import { useAutoDownload } from '../../stores/settings/usage'
|
||||
import reportPlaybackStopped from '../../api/mutations/playback/functions/playback-stopped'
|
||||
import reportPlaybackCompleted from '../../api/mutations/playback/functions/playback-completed'
|
||||
import isPlaybackFinished from '../../api/mutations/playback/utils'
|
||||
import reportPlaybackStarted from '../../api/mutations/playback/functions/playback-started'
|
||||
import calculateTrackVolume from './utils/normalization'
|
||||
import calculateTrackVolume from '../../hooks/player/functions/normalization'
|
||||
import saveAudioItem from '../../api/mutations/download/utils'
|
||||
import { useDownloadingDeviceProfile } from '../../stores/device-profile'
|
||||
import Initialize from './functions/initialization'
|
||||
import Initialize from './utils/initialization'
|
||||
import { useEnableAudioNormalization } from '../../stores/settings/player'
|
||||
import { usePlayerQueueStore } from '../../stores/player/queue'
|
||||
import usePostFullCapabilities from '../../api/mutations/session'
|
||||
|
||||
const PLAYER_EVENTS: Event[] = [
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
Event.PlaybackProgressUpdated,
|
||||
Event.PlaybackState,
|
||||
]
|
||||
import reportPlaybackProgress from '../../api/mutations/playback/functions/playback-progress'
|
||||
import { PLAYER_EVENTS } from '../../configs/player.config'
|
||||
import { getApi } from '../../stores'
|
||||
|
||||
interface PlayerContext {}
|
||||
|
||||
@@ -39,42 +36,44 @@ export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
|
||||
usePerformanceMonitor('PlayerProvider', 3)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const eventHandler = async (event: any) => {
|
||||
useTrackPlayerEvents(PLAYER_EVENTS, async (event) => {
|
||||
const api = getApi()
|
||||
|
||||
switch (event.type) {
|
||||
case Event.PlaybackActiveTrackChanged: {
|
||||
// When we load a new queue, our index is updated before RNTP
|
||||
// Because of this, we only need to respond to this event
|
||||
// if the index from the event differs from what we have stored
|
||||
if (event.track && enableAudioNormalization) {
|
||||
const volume = calculateTrackVolume(event.track)
|
||||
await TrackPlayer.setVolume(volume)
|
||||
} else if (event.track) {
|
||||
try {
|
||||
await reportPlaybackStarted(event.track)
|
||||
} catch (error) {
|
||||
console.error('Unable to report playback started for track', error)
|
||||
if (event.track) {
|
||||
handleActiveTrackChanged(event.track as JellifyTrack, event.index)
|
||||
|
||||
reportPlaybackStarted(api, event.track as JellifyTrack, 0)
|
||||
|
||||
if (enableAudioNormalization) {
|
||||
const volume = calculateTrackVolume(event.track as JellifyTrack)
|
||||
await TrackPlayer.setVolume(volume)
|
||||
}
|
||||
}
|
||||
|
||||
await handleActiveTrackChanged()
|
||||
|
||||
if (event.lastTrack) {
|
||||
try {
|
||||
if (isPlaybackFinished(event.lastPosition, event.lastTrack.duration ?? 1))
|
||||
await reportPlaybackCompleted(event.lastTrack as JellifyTrack)
|
||||
else await reportPlaybackStopped(event.lastTrack as JellifyTrack)
|
||||
} catch (error) {
|
||||
console.error('Unable to report playback stopped for lastTrack', error)
|
||||
}
|
||||
if (event.lastTrack && event.lastPosition) {
|
||||
if (isPlaybackFinished(event.lastPosition, event.lastTrack.duration ?? 1))
|
||||
reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
|
||||
else
|
||||
reportPlaybackStopped(
|
||||
api,
|
||||
event.lastTrack as JellifyTrack,
|
||||
event.lastPosition,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
case Event.PlaybackProgressUpdated: {
|
||||
const currentTrack = usePlayerQueueStore.getState().currentTrack
|
||||
|
||||
if (event.position && currentTrack)
|
||||
reportPlaybackProgress(api, currentTrack, event.position)
|
||||
|
||||
if (event.position / event.duration > 0.3 && autoDownload && currentTrack) {
|
||||
await saveAudioItem(currentTrack.item, downloadingDeviceProfile, true)
|
||||
await saveAudioItem(currentTrack.item, downloadingDeviceProfile, true).then(
|
||||
(value) => console.log('Track downloaded'),
|
||||
)
|
||||
}
|
||||
|
||||
break
|
||||
@@ -82,20 +81,19 @@ export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
|
||||
case Event.PlaybackState: {
|
||||
const currentTrack = usePlayerQueueStore.getState().currentTrack
|
||||
|
||||
switch (event.state) {
|
||||
case State.Playing:
|
||||
if (currentTrack) await reportPlaybackStarted(currentTrack)
|
||||
if (currentTrack) reportPlaybackStarted(api, currentTrack)
|
||||
break
|
||||
default:
|
||||
if (currentTrack) await reportPlaybackStopped(currentTrack)
|
||||
if (currentTrack) reportPlaybackStopped(api, currentTrack)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useTrackPlayerEvents(PLAYER_EVENTS, eventHandler)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
@@ -104,9 +102,5 @@ export const PlayerProvider: () => React.JSX.Element = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PlayerContext.Provider value={{}}>
|
||||
<></>
|
||||
</PlayerContext.Provider>
|
||||
)
|
||||
return <PlayerContext value={{}} />
|
||||
}
|
||||
|
||||
+2
-8
@@ -1,16 +1,12 @@
|
||||
import { isUndefined } from 'lodash'
|
||||
import { getActiveIndex, getCurrentTrack, getPlayQueue } from '.'
|
||||
import TrackPlayer, { RepeatMode } from 'react-native-track-player'
|
||||
import { usePlayerQueueStore } from '../../../stores/player/queue'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { REPEAT_MODE_QUERY_KEY } from '../constants/query-keys'
|
||||
import { createMMKV } from 'react-native-mmkv'
|
||||
|
||||
export default async function Initialize() {
|
||||
const {
|
||||
queue: persistedQueue,
|
||||
currentIndex: persistedIndex,
|
||||
currentTrack: persistedTrack,
|
||||
repeatMode,
|
||||
} = usePlayerQueueStore.getState()
|
||||
|
||||
@@ -19,9 +15,8 @@ export default async function Initialize() {
|
||||
const savedPosition = progressStorage.getNumber('player-key') ?? 0
|
||||
console.log('savedPosition before reset', savedPosition)
|
||||
|
||||
const storedPlayQueue = persistedQueue.length > 0 ? persistedQueue : getPlayQueue()
|
||||
const storedIndex = persistedIndex ?? getActiveIndex()
|
||||
const storedTrack = persistedTrack ?? getCurrentTrack()
|
||||
const storedPlayQueue = persistedQueue.length > 0 ? persistedQueue : undefined
|
||||
const storedIndex = persistedIndex
|
||||
|
||||
if (
|
||||
Array.isArray(storedPlayQueue) &&
|
||||
@@ -40,7 +35,6 @@ export default async function Initialize() {
|
||||
|
||||
const restoredRepeatMode = repeatMode ?? RepeatMode.Off
|
||||
await TrackPlayer.setRepeatMode(restoredRepeatMode)
|
||||
queryClient.setQueryData(REPEAT_MODE_QUERY_KEY, restoredRepeatMode)
|
||||
|
||||
// Restore saved playback position after queue is loaded
|
||||
if (savedPosition > 0) {
|
||||
@@ -2,11 +2,5 @@ import AddToPlaylist from '../../components/AddToPlaylist/index'
|
||||
import { AddToPlaylistProps } from '../types'
|
||||
|
||||
export default function AddToPlaylistSheet({ route }: AddToPlaylistProps): React.JSX.Element {
|
||||
return (
|
||||
<AddToPlaylist
|
||||
track={route.params.track}
|
||||
tracks={route.params.tracks}
|
||||
source={route.params.source}
|
||||
/>
|
||||
)
|
||||
return <AddToPlaylist tracks={route.params.tracks} source={route.params.source} />
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import PlayerScreen from '../../components/Player'
|
||||
import Queue from '../../components/Player/queue'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import MultipleArtistsSheet from '../Context/multiple-artists'
|
||||
import { PlayerParamList } from './types'
|
||||
import Lyrics from '../../components/Player/components/lyrics'
|
||||
import usePlayerDisplayStore from '../../stores/player/display'
|
||||
|
||||
const PlayerStack = createNativeStackNavigator<PlayerParamList>()
|
||||
|
||||
export default function Player(): React.JSX.Element {
|
||||
useEffect(() => {
|
||||
usePlayerDisplayStore.getState().setIsPlayerFocused(true)
|
||||
|
||||
return () => usePlayerDisplayStore.getState().setIsPlayerFocused(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PlayerStack.Navigator initialRouteName='PlayerScreen'>
|
||||
<PlayerStack.Screen
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SignOutModalProps } from './types'
|
||||
import { H5, Text } from '../../components/Global/helpers/text'
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import Icon from '../../components/Global/components/icon'
|
||||
import { useResetQueue } from '../../providers/Player/hooks/callbacks'
|
||||
import { useResetQueue } from '../../hooks/player/callbacks'
|
||||
import { useClearAllDownloads } from '../../api/mutations/download'
|
||||
import { useJellifyServer } from '../../stores'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
Vendored
+1
-2
@@ -61,8 +61,7 @@ export type RootStackParamList = {
|
||||
}
|
||||
|
||||
AddToPlaylist: {
|
||||
track?: BaseItemDto
|
||||
tracks?: BaseItemDto[]
|
||||
tracks: BaseItemDto[]
|
||||
source?: BaseItemDto
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
|
||||
type PlayerDisplayStore = {
|
||||
isPlayerFocused: boolean
|
||||
setIsPlayerFocused: (isPlayerFocused: boolean) => void
|
||||
}
|
||||
|
||||
const usePlayerDisplayStore = create<PlayerDisplayStore>()(
|
||||
devtools((set) => ({
|
||||
isPlayerFocused: false,
|
||||
setIsPlayerFocused: (isPlayerFocused) => set({ isPlayerFocused }),
|
||||
})),
|
||||
)
|
||||
|
||||
export const useIsPlayerFocused = () => usePlayerDisplayStore((state) => state.isPlayerFocused)
|
||||
|
||||
export default usePlayerDisplayStore
|
||||
Reference in New Issue
Block a user