mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-17 19:25:34 -06:00
[skip actions]
run prettier
This commit is contained in:
@@ -10,10 +10,13 @@ module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'error', // Disallow usage of any
|
||||
'no-mixed-spaces-and-tabs': 'off', // refer https://github.com/prettier/prettier/issues/4199
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
63
README.md
63
README.md
@@ -1,31 +1,36 @@
|
||||

|
||||
|
||||
# 🪼 Jellify
|
||||
|
||||
[](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml)
|
||||
|
||||
### 🔗 Quick Links
|
||||
|
||||
[Discord Server](https://discord.gg/yf8fBatktn)
|
||||
|
||||
[TestFlight](https://testflight.apple.com/join/etVSc7ZQ)
|
||||
|
||||
### ℹ️ About
|
||||
|
||||
> **jellify** (verb) - *to make gelatinous* <br>
|
||||
[see also](https://www.merriam-webster.com/dictionary/jellify)
|
||||
> **jellify** (verb) - _to make gelatinous_ <br>
|
||||
> [see also](https://www.merriam-webster.com/dictionary/jellify)
|
||||
|
||||
*Jellify* is a free and open source music player for [Jellyfin](https://jellyfin.org/). Built with [React Native](https://reactnative.dev/), *Jellify* provides a user experience that feels familar to other popular music apps and a has featureset to match
|
||||
_Jellify_ is a free and open source music player for [Jellyfin](https://jellyfin.org/). Built with [React Native](https://reactnative.dev/), _Jellify_ provides a user experience that feels familar to other popular music apps and a has featureset to match
|
||||
|
||||
> *Jellify* requires a connection to a [Jellyfin](https://jellyfin.org/) server to work.
|
||||
> _Jellify_ requires a connection to a [Jellyfin](https://jellyfin.org/) server to work.
|
||||
|
||||
### 🤓 Background
|
||||
I was after a music app for Jellyfin that showcased my music with artwork, had a user interface congruent with what the big guys do, and had the ability to algorithmically curate music (not that you have to use *Jellify* that way). I also wanted to create a music app that could handle my extremely large music libraries (i.e., 100K+ songs) and not get bogged down.
|
||||
|
||||
I was after a music app for Jellyfin that showcased my music with artwork, had a user interface congruent with what the big guys do, and had the ability to algorithmically curate music (not that you have to use _Jellify_ that way). I also wanted to create a music app that could handle my extremely large music libraries (i.e., 100K+ songs) and not get bogged down.
|
||||
|
||||
This app was designed with me and my dad in mind, since I wanted to give him a sleek, one stop shop for live recordings of bands he likes (read: the Grateful Dead). The UI was designed so that he'd find it instantly familiar and useful. CarPlay / Android Auto support was also a must for us, as we both use CarPlay religiously.
|
||||
|
||||
**TL;DR** Designed to be lightweight and scalable, *Jellify* caters to those who want a mobile Jellyfin music experience similar to what's provided by the big music streaming services.
|
||||
**TL;DR** Designed to be lightweight and scalable, _Jellify_ caters to those who want a mobile Jellyfin music experience similar to what's provided by the big music streaming services.
|
||||
|
||||
## 💡 Features
|
||||
|
||||
### ✨ Current
|
||||
|
||||
- Available via Testflight and Android APK
|
||||
- APKs are associated with each [release](https://github.com/anultravioletaurora/Jellify/releases)
|
||||
- Light and Dark modes
|
||||
@@ -36,6 +41,7 @@ This app was designed with me and my dad in mind, since I wanted to give him a s
|
||||
- Full playlist support, including creating, updating, and reordering
|
||||
|
||||
### 🛠 Roadmap
|
||||
|
||||
- [Offline Playback](https://github.com/anultravioletaurora/Jellify/issues/10)
|
||||
- [CarPlay / Android Auto Support](https://github.com/anultravioletaurora/Jellify/issues/5)
|
||||
- [Support for Jellyfin Instant Mixes](https://github.com/anultravioletaurora/Jellify/issues/50)
|
||||
@@ -45,12 +51,15 @@ This app was designed with me and my dad in mind, since I wanted to give him a s
|
||||
- [TV (Android, Apple, Samsung) Support](https://github.com/anultravioletaurora/Jellify/issues/85)
|
||||
|
||||
## 👀 Lemme see!
|
||||
|
||||
### Home
|
||||
|
||||
Home
|
||||
|
||||
<img src="screenshots/home.png" alt="Jellify Home" width="275" height="600">
|
||||
|
||||
### Library
|
||||
|
||||
Library
|
||||
|
||||
<img src="screenshots/library.png" alt="Library" width="275" height="600">
|
||||
@@ -84,9 +93,11 @@ Playlist
|
||||
<img src="screenshots/playlist.png" alt="Playlist" width="275" height="600">
|
||||
|
||||
### Search
|
||||
|
||||
<img src="screenshots/search.png" alt="Search" width="275" height="600">
|
||||
|
||||
### Player
|
||||
|
||||
<img src="screenshots/player.png" alt="Player" width="275" height="600">
|
||||
|
||||
<img src="screenshots/player_queue.png" alt="Queue" width="275" height="600">
|
||||
@@ -94,14 +105,19 @@ Playlist
|
||||
<img src='screenshots/favorite_track.png' alt='Favorite Track' width='275' height='600”'>
|
||||
|
||||
### CarPlay (Sneak Preview)
|
||||
|
||||
<img src="screenshots/carplay_nowplaying.jpeg" alt="Now Playing (CarPlay)" width="500" height="350">
|
||||
|
||||
### On the Server
|
||||
|
||||
<img src="https://github.com/user-attachments/assets/741884a2-b9b7-4081-b3a0-6655d08071dc" alt="Playback Tracking" width="300" height="200">
|
||||
|
||||
## 🏗 Built with good stuff
|
||||
|
||||
[](https://reactjs.org “Go to React homepage”) [](https://typescriptlang.org “Go to TypeScript homepage”)
|
||||
|
||||
### 🎨 Frontend
|
||||
|
||||
[Tamagui](https://tamagui.dev/)\
|
||||
[Burnt](https://github.com/nandorojo/burnt)\
|
||||
[React Navigation](https://reactnavigation.org/)\
|
||||
@@ -109,9 +125,11 @@ Playlist
|
||||
[React Native Draggable Flatlist](https://github.com/computerjazz/react-native-draggable-flatlist)\
|
||||
[React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/)\
|
||||
[React Native Vector Icons](https://github.com/oblador/react-native-vector-icons)
|
||||
|
||||
- Specifically using [Material Community Icons](https://oblador.github.io/react-native-vector-icons/#MaterialCommunityIcons)
|
||||
|
||||
### 🎛️ Backend
|
||||
|
||||
[Expo SDK](https://expo.dev/)\
|
||||
[Jellyfin SDK](https://typescript-sdk.jellyfin.org/)\
|
||||
[Tanstack Query](https://tanstack.com/query/latest/docs/framework/react/react-native)\
|
||||
@@ -122,23 +140,30 @@ Playlist
|
||||
[React Native URL Polyfill](https://github.com/charpeni/react-native-url-polyfill)
|
||||
|
||||
### 👩💻 Monitoring
|
||||
|
||||
[GlitchTip](https://glitchtip.com/)
|
||||
|
||||
### 💜 Love from Wisconsin 🧀
|
||||
|
||||
This is undoubtedly a passion project of [mine](https://github.com/anultravioletaurora), and I've learned a lot from working on it (and the many failed attempts before it). I hope you enjoy using it! Feature requests and bug reports are welcome :)
|
||||
|
||||
## 🏃♀️Running Locally
|
||||
|
||||
### ⚛️ Universal Dependencies
|
||||
|
||||
- [Ruby](https://www.ruby-lang.org/en/documentation/installation/) for Fastlane
|
||||
- [NodeJS v22](https://nodejs.org/en/download) for React Native
|
||||
|
||||
### 🍎 iOS
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- [Xcode](https://developer.apple.com/xcode/) for building
|
||||
|
||||
#### Instructions
|
||||
|
||||
##### Setup
|
||||
|
||||
- Clone this repository
|
||||
- Run `npm run init` to initialize the project
|
||||
- This will install `npm` packages, install `bundler` and required gems, and installs CocoaPods
|
||||
@@ -146,54 +171,64 @@ This is undoubtedly a passion project of [mine](https://github.com/anultraviolet
|
||||
- *You will need access to the *Jellify Signing* private repository*
|
||||
|
||||
##### Running
|
||||
|
||||
- Run `npm run start` to start the dev server
|
||||
- Open the `Jellify.xcodeworkspace` with Xcode, *not* the `Jellify.xcodeproject`
|
||||
- Open the `Jellify.xcodeworkspace` with Xcode, _not_ the `Jellify.xcodeproject`
|
||||
- Run either on a device or in the simulator
|
||||
- *You will need to wait for Xcode to finish it's "Indexing" step*
|
||||
- _You will need to wait for Xcode to finish it's "Indexing" step_
|
||||
|
||||
##### Building
|
||||
|
||||
- To create a build, run `npm run fastlane:ios:build` to use fastlane to compile an `.ipa` for you
|
||||
|
||||
### 🤖 Android
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- [Android Studio](https://developer.android.com/studio)
|
||||
- [Java Development Kit](https://www.oracle.com/th/java/technologies/downloads/)
|
||||
|
||||
#### Instructions
|
||||
|
||||
##### Setup
|
||||
|
||||
- Clone this repository
|
||||
- Run `npm i` to install `npm` packages
|
||||
|
||||
##### Running
|
||||
|
||||
- Run `npm run start` to start the dev server
|
||||
- Open the `android` folder with Android Studio
|
||||
- *Android Studio should automatically grab the "Run Configurations" and initialize Gradle*
|
||||
- _Android Studio should automatically grab the "Run Configurations" and initialize Gradle_
|
||||
- Run either on a device or in the simulator
|
||||
|
||||
##### Building
|
||||
|
||||
- To create a build, run `npm run fastlane:android:build` to use fastlane to compile an `.apk` for you
|
||||
|
||||
#### References
|
||||
|
||||
- [Setting up Android SDK](https://developer.android.com/about/versions/14/setup-sdk)
|
||||
- [ANDROID_HOME not being set](https://stackoverflow.com/questions/26356359/error-android-home-is-not-set-and-android-command-not-in-your-path-you-must/54888107#54888107)
|
||||
|
||||
## 🙏 Special Thanks To
|
||||
|
||||
- The [Jellyfin Team](https://jellyfin.org/) for making this possible with their software, SDKs, and unequivocal helpfulness.
|
||||
- Extra thanks to [Niels](https://github.com/nielsvanvelzen) and [Bill](https://github.com/thornbill)
|
||||
- [James](https://github.com/jmshrv) and all other contributors of [Finamp](https://github.com/jmshrv/finamp). *Jellify* draws inspiration and wisdom from it, and is another fantastic music app for Jellyfin.
|
||||
- [James](https://github.com/jmshrv) and all other contributors of [Finamp](https://github.com/jmshrv/finamp). _Jellify_ draws inspiration and wisdom from it, and is another fantastic music app for Jellyfin.
|
||||
- James’ [API Blog Post](https://jmshrv.com/posts/jellyfin-api/) proved to be exceptionally valuable during development
|
||||
- The folks in the [Margelo Community Discord](https://discord.com/invite/6CSHz2qAvA) for their assistance
|
||||
- Extra thanks to [Ritesh](https://github.com/riteshshukla04) for your help, knowledge, and guidance
|
||||
- [Nicolas Charpentier](https://github.com/charpeni) for his [React Native URL Polyfill](https://github.com/charpeni/react-native-url-polyfill) module and for his assistance with getting Jest working
|
||||
- My fellow [contributors](https://github.com/anultravioletaurora/Jellify/graphs/contributors) who have poured so much heart and a lot of sweat into making *Jellify* a great experience
|
||||
- My fellow [contributors](https://github.com/anultravioletaurora/Jellify/graphs/contributors) who have poured so much heart and a lot of sweat into making _Jellify_ a great experience
|
||||
- Extra thanks to [John Grant](https://github.com/johngrantdev) for shaping and designing the user experience in many places
|
||||
- The friends I made along the way that have been critical in fostering an amazing community around *Jellify*
|
||||
- The friends I made along the way that have been critical in fostering an amazing community around _Jellify_
|
||||
- [Thalia](https://github.com/PercyGabriel1129)
|
||||
- [BotBlake](https://github.com/BotBlake)
|
||||
- My long time friends that have heard me talk about *Jellify* for literally **eons**. Thank you for testing *Jellify* during it's infancy and for supporting me all the way back at the beginning of this project
|
||||
- My long time friends that have heard me talk about _Jellify_ for literally **eons**. Thank you for testing _Jellify_ during it's infancy and for supporting me all the way back at the beginning of this project
|
||||
- Tony (iOS, Android)
|
||||
- Trevor (Android)
|
||||
- [Laine](https://github.com/lainie-ftw) (Android)
|
||||
- [Jordan](https://github.com/jordanbleu) (iOS)
|
||||
- My best(est) friend [Alyssa](https://www.instagram.com/uhh.lyssarae?igsh=MTRmczExempnbjBwZw==), for your design knowledge and for making various artwork for *Jellify*.
|
||||
- My best(est) friend [Alyssa](https://www.instagram.com/uhh.lyssarae?igsh=MTRmczExempnbjBwZw==), for your design knowledge and for making various artwork for _Jellify_.
|
||||
- You’ve been instrumental in shaping it’s user experience, my rock during development, and an overall inspiration in my life
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
fastlane documentation
|
||||
----
|
||||
## fastlane documentation
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -29,8 +28,6 @@ Runs all the tests
|
||||
[bundle exec] fastlane android build
|
||||
```
|
||||
|
||||
|
||||
|
||||
### android deploy
|
||||
|
||||
```sh
|
||||
@@ -39,7 +36,7 @@ Runs all the tests
|
||||
|
||||
Deploy a new version to the Google Play
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ module.exports = {
|
||||
'react-native-boost/plugin',
|
||||
// react-native-reanimated/plugin has to be listed last
|
||||
'react-native-reanimated/plugin',
|
||||
]
|
||||
};
|
||||
],
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@ export default function BlurhashedImage({
|
||||
<View
|
||||
minHeight={height ?? width}
|
||||
minWidth={width}
|
||||
borderRadius={borderRadius ? borderRadius : 25}>
|
||||
borderRadius={borderRadius ? borderRadius : 25}
|
||||
>
|
||||
{isSuccess ? (
|
||||
<Image
|
||||
source={{
|
||||
|
||||
@@ -24,7 +24,8 @@ export default function Login(): React.JSX.Element {
|
||||
? 'ServerAuthentication'
|
||||
: 'LibrarySelection'
|
||||
}
|
||||
screenOptions={{ headerShown: false }}>
|
||||
screenOptions={{ headerShown: false }}
|
||||
>
|
||||
<Stack.Screen
|
||||
name='ServerAddress'
|
||||
options={{
|
||||
|
||||
@@ -50,7 +50,8 @@ export default function ServerLibrary(): React.JSX.Element {
|
||||
type='single'
|
||||
disableDeactivation={true}
|
||||
value={libraryId}
|
||||
onValueChange={setLibraryId}>
|
||||
onValueChange={setLibraryId}
|
||||
>
|
||||
{libraries!
|
||||
.filter((library) => library.CollectionType === 'music')
|
||||
.map((library) => {
|
||||
@@ -63,7 +64,8 @@ export default function ServerLibrary(): React.JSX.Element {
|
||||
libraryId == library.Id!
|
||||
? getToken('$color.purpleGray')
|
||||
: 'unset'
|
||||
}>
|
||||
}
|
||||
>
|
||||
<Text>{library.Name ?? 'Unnamed Library'}</Text>
|
||||
</ToggleGroup.Item>
|
||||
)
|
||||
@@ -88,7 +90,8 @@ export default function ServerLibrary(): React.JSX.Element {
|
||||
playlistLibraryPrimaryImageId: playlistLibrary?.ImageTags!.Primary,
|
||||
})
|
||||
setLoggedIn(true)
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{`Let's Go!`}
|
||||
</Button>
|
||||
|
||||
@@ -96,7 +99,8 @@ export default function ServerLibrary(): React.JSX.Element {
|
||||
onPress={() => {
|
||||
Client.switchUser()
|
||||
setUser(undefined)
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
Switch User
|
||||
</Button>
|
||||
</SafeAreaView>
|
||||
|
||||
@@ -80,7 +80,8 @@ export const JellifyProvider: ({ children }: { children: ReactNode }) => React.J
|
||||
loggedIn,
|
||||
setLoggedIn,
|
||||
carPlayConnected,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</JellifyContext.Provider>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fonts } from "@tamagui/config/v4";
|
||||
import { createFont } from "tamagui";
|
||||
import { fonts } from '@tamagui/config/v4'
|
||||
import { createFont } from 'tamagui'
|
||||
|
||||
const aileronFace = {
|
||||
100: { normal: 'Aileron-UltraLight', italic: 'Aileron UltraLight Italic' },
|
||||
@@ -10,23 +10,23 @@ const aileronFace = {
|
||||
600: { normal: 'Aileron-SemiBold', italic: 'Aileron SemiBold Italic' },
|
||||
700: { normal: 'Aileron-Bold', italic: 'Aileron Bold Italic' },
|
||||
800: { normal: 'Aileron-Heavy', italic: 'Aileron Heavy Italic' },
|
||||
900: { normal: 'Aileron-Black', italic: 'Aileron-BlackItalic' }
|
||||
};
|
||||
900: { normal: 'Aileron-Black', italic: 'Aileron-BlackItalic' },
|
||||
}
|
||||
|
||||
export const bodyFont = createFont({
|
||||
family: "Aileron-Bold",
|
||||
family: 'Aileron-Bold',
|
||||
size: fonts.body.size,
|
||||
lineHeight: fonts.body.lineHeight,
|
||||
weight: fonts.body.weight,
|
||||
letterSpacing: fonts.body.letterSpacing,
|
||||
face: aileronFace
|
||||
face: aileronFace,
|
||||
})
|
||||
|
||||
export const headingFont = createFont({
|
||||
family: "Aileron-Black",
|
||||
family: 'Aileron-Black',
|
||||
size: fonts.heading.size,
|
||||
lineHeight: fonts.heading.lineHeight,
|
||||
weight: fonts.heading.weight,
|
||||
letterSpacing: fonts.heading.letterSpacing,
|
||||
face: aileronFace
|
||||
face: aileronFace,
|
||||
})
|
||||
@@ -1,2 +1,2 @@
|
||||
// Only import react-native-gesture-handler on native platforms
|
||||
import 'react-native-gesture-handler';
|
||||
import 'react-native-gesture-handler'
|
||||
|
||||
21
index.js
21
index.js
@@ -1,19 +1,20 @@
|
||||
import 'react-native-gesture-handler';
|
||||
import {AppRegistry} from 'react-native';
|
||||
import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
import 'react-native-gesture-handler'
|
||||
import { AppRegistry } from 'react-native'
|
||||
import App from './App'
|
||||
import { name as appName } from './app.json'
|
||||
import { PlaybackService } from './player/service'
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import Client from './api/client';
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import Client from './api/client'
|
||||
|
||||
// Initialize API client instance
|
||||
Client.instance;
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||
Client.instance
|
||||
|
||||
// Enable React Navigation freeze for detaching inactive screens
|
||||
// enableFreeze();
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
AppRegistry.registerComponent('RNCarPlayScene', () => App);
|
||||
AppRegistry.registerComponent(appName, () => App)
|
||||
AppRegistry.registerComponent('RNCarPlayScene', () => App)
|
||||
|
||||
// Register RNTP playback service for remote controls
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService);
|
||||
TrackPlayer.registerPlaybackService(() => PlaybackService)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
fastlane documentation
|
||||
----
|
||||
## fastlane documentation
|
||||
|
||||
# Installation
|
||||
|
||||
@@ -23,7 +22,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
||||
|
||||
Push a new beta build to TestFlight
|
||||
|
||||
----
|
||||
---
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// https://docs.swmansion.com/react-native-gesture-handler/docs/guides/testing
|
||||
module.exports = {
|
||||
preset: 'jest-expo',
|
||||
setupFiles: ["./node_modules/react-native-gesture-handler/jestSetup.js"],
|
||||
setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'],
|
||||
setupFilesAfterEnv: [
|
||||
"./jest/setup.js",
|
||||
"./jest/setup-carplay.js",
|
||||
"./jest/setup-blurhash.js",
|
||||
"./jest/setup-reanimated.js",
|
||||
"./tamagui.config.ts",
|
||||
'./jest/setup.js',
|
||||
'./jest/setup-carplay.js',
|
||||
'./jest/setup-blurhash.js',
|
||||
'./jest/setup-reanimated.js',
|
||||
'./tamagui.config.ts',
|
||||
],
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(@)?(react-native|react-native-.*|react-navigation|jellyfin|burnt|expo|expo-.*)/)',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import 'react-native';
|
||||
import React from 'react';
|
||||
import App from '../App';
|
||||
import 'react-native'
|
||||
import React from 'react'
|
||||
import App from '../App'
|
||||
|
||||
// Note: import explicitly to use the types shipped with jest.
|
||||
import {it} from '@jest/globals';
|
||||
import { it } from '@jest/globals'
|
||||
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer';
|
||||
import renderer from 'react-test-renderer'
|
||||
|
||||
it('renders correctly', () => {
|
||||
renderer.create(<App />);
|
||||
});
|
||||
renderer.create(<App />)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
jest.mock('react-native-blurhash', () => {
|
||||
return {
|
||||
Blurhash: ()=> null
|
||||
};
|
||||
});
|
||||
Blurhash: () => null,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
jest.mock('react-native-carplay', () => {
|
||||
return {
|
||||
default: {
|
||||
checkForConnection: jest.fn()
|
||||
}
|
||||
checkForConnection: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -3,4 +3,4 @@ jest.mock('react-native-reanimated', () => ({
|
||||
createAnimatedPropAdapter: jest.fn,
|
||||
useReducedMotion: jest.fn,
|
||||
LayoutAnimationConfig: jest.fn,
|
||||
}));
|
||||
}))
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// https://github.com/react-native-device-info/react-native-device-info/issues/1360
|
||||
import mockRNDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock';
|
||||
import mockRNDeviceInfo from 'react-native-device-info/jest/react-native-device-info-mock'
|
||||
|
||||
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter');
|
||||
jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter')
|
||||
|
||||
jest.mock('react-native-device-info', () => mockRNDeviceInfo);
|
||||
jest.mock('react-native-device-info', () => mockRNDeviceInfo)
|
||||
|
||||
jest.mock('react-native-haptic-feedback', () => {
|
||||
return {
|
||||
default: {
|
||||
trigger: jest.fn()
|
||||
trigger: jest.fn(),
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
jest.mock('burnt', () => {
|
||||
return {
|
||||
default: {
|
||||
alert: jest.fn()
|
||||
}
|
||||
alert: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -79,8 +79,7 @@ jest.mock('react-native-track-player', () => {
|
||||
IOSCategoryOptions: {
|
||||
MixWithOthers: 'mixWithOthers',
|
||||
DuckOthers: 'duckOthers',
|
||||
InterruptSpokenAudioAndMixWithOthers:
|
||||
'interruptSpokenAudioAndMixWithOthers',
|
||||
InterruptSpokenAudioAndMixWithOthers: 'interruptSpokenAudioAndMixWithOthers',
|
||||
AllowBluetooth: 'allowBluetooth',
|
||||
AllowBluetoothA2DP: 'allowBluetoothA2DP',
|
||||
AllowAirPlay: 'allowAirPlay',
|
||||
@@ -107,4 +106,4 @@ jest.mock('react-native-track-player', () => {
|
||||
PlayAndRecord: 'playAndRecord',
|
||||
},
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -10,10 +10,6 @@ const config = getDefaultConfig(__dirname, {
|
||||
// https://github.com/expo/expo/issues/23180
|
||||
config.resolver.sourceExts.push('mjs')
|
||||
|
||||
config.watchFolders = [
|
||||
"components",
|
||||
"api",
|
||||
"player"
|
||||
]
|
||||
config.watchFolders = ['components', 'api', 'player']
|
||||
|
||||
module.exports = config;
|
||||
module.exports = config
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export class ArtistModel {
|
||||
|
||||
name?: string | undefined | null;
|
||||
name?: string | undefined | null
|
||||
|
||||
constructor(itemDto: BaseItemDto) {
|
||||
|
||||
this.name = itemDto.Name
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Capability } from "react-native-track-player";
|
||||
import { Capability } from 'react-native-track-player'
|
||||
|
||||
export const CAPABILITIES: Capability[] = [
|
||||
Capability.Pause,
|
||||
|
||||
@@ -1,50 +1,60 @@
|
||||
import { Progress, State } from "react-native-track-player";
|
||||
import { JellifyTrack } from "../types/JellifyTrack";
|
||||
import { PlaystateApi } from "@jellyfin/sdk/lib/generated-client/api/playstate-api";
|
||||
import { convertSecondsToRunTimeTicks } from "../helpers/runtimeticks";
|
||||
import { Progress, State } from 'react-native-track-player'
|
||||
import { JellifyTrack } from '../types/JellifyTrack'
|
||||
import { PlaystateApi } from '@jellyfin/sdk/lib/generated-client/api/playstate-api'
|
||||
import { convertSecondsToRunTimeTicks } from '../helpers/runtimeticks'
|
||||
|
||||
export async function handlePlaybackState(sessionId: string, playstateApi: PlaystateApi, track: JellifyTrack, state: State) {
|
||||
export async function handlePlaybackState(
|
||||
sessionId: string,
|
||||
playstateApi: PlaystateApi,
|
||||
track: JellifyTrack,
|
||||
state: State,
|
||||
) {
|
||||
switch (state) {
|
||||
case (State.Playing) : {
|
||||
console.debug("Report playback started")
|
||||
case State.Playing: {
|
||||
console.debug('Report playback started')
|
||||
await playstateApi.reportPlaybackStart({
|
||||
playbackStartInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: track.item.Id,
|
||||
}
|
||||
});
|
||||
break;
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case (State.Ended) :
|
||||
case (State.Paused) :
|
||||
case (State.Stopped) : {
|
||||
console.debug("Report playback stopped")
|
||||
case State.Ended:
|
||||
case State.Paused:
|
||||
case State.Stopped: {
|
||||
console.debug('Report playback stopped')
|
||||
await playstateApi.reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: track.item.Id,
|
||||
}
|
||||
});
|
||||
break;
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function handlePlaybackProgressUpdated(sessionId: string, playstateApi: PlaystateApi, track: JellifyTrack, progress: Progress) {
|
||||
export async function handlePlaybackProgressUpdated(
|
||||
sessionId: string,
|
||||
playstateApi: PlaystateApi,
|
||||
track: JellifyTrack,
|
||||
progress: Progress,
|
||||
) {
|
||||
if (Math.floor(progress.duration - progress.position) === 5) {
|
||||
console.debug("Track finished, scrobbling...");
|
||||
console.debug('Track finished, scrobbling...')
|
||||
await playstateApi.reportPlaybackStopped({
|
||||
playbackStopInfo: {
|
||||
SessionId: sessionId,
|
||||
ItemId: track.item.Id,
|
||||
PositionTicks: convertSecondsToRunTimeTicks(track.duration!)
|
||||
}
|
||||
});
|
||||
PositionTicks: convertSecondsToRunTimeTicks(track.duration!),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// DO NOTHING, reporting playback will just eat up power
|
||||
// Jellyfin can keep track of progress, we're going to intentionally
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isEmpty } from "lodash";
|
||||
import { QueuingType } from "../../enums/queuing-type";
|
||||
import { JellifyTrack } from "../../types/JellifyTrack";
|
||||
import { getActiveTrackIndex } from "react-native-track-player/lib/src/trackPlayer";
|
||||
import { isEmpty } from 'lodash'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { JellifyTrack } from '../../types/JellifyTrack'
|
||||
import { getActiveTrackIndex } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
|
||||
/**
|
||||
* Finds and returns the index of the player queue to insert additional tracks into
|
||||
@@ -9,10 +9,9 @@ import { getActiveTrackIndex } from "react-native-track-player/lib/src/trackPlay
|
||||
* @returns The index to insert songs to play next at
|
||||
*/
|
||||
export const findPlayNextIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
if (isEmpty(playQueue))
|
||||
return 0;
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
return (await getActiveTrackIndex())! + 1;
|
||||
return (await getActiveTrackIndex())! + 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,17 +20,15 @@ export const findPlayNextIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
* @returns The index to insert songs to add to the user queue
|
||||
*/
|
||||
export const findPlayQueueIndexStart = async (playQueue: JellifyTrack[]) => {
|
||||
if (isEmpty(playQueue)) return 0
|
||||
|
||||
if (isEmpty(playQueue))
|
||||
return 0;
|
||||
const activeIndex = await getActiveTrackIndex()
|
||||
|
||||
const activeIndex = await getActiveTrackIndex();
|
||||
|
||||
if (playQueue.findIndex(track => track.QueuingType === QueuingType.FromSelection) === -1)
|
||||
if (playQueue.findIndex((track) => track.QueuingType === QueuingType.FromSelection) === -1)
|
||||
return activeIndex! + 1
|
||||
|
||||
return playQueue.findIndex((queuedTrack, index) =>
|
||||
queuedTrack.QueuingType === QueuingType.FromSelection &&
|
||||
index > activeIndex!
|
||||
);
|
||||
return playQueue.findIndex(
|
||||
(queuedTrack, index) =>
|
||||
queuedTrack.QueuingType === QueuingType.FromSelection && index > activeIndex!,
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
import _ from "lodash";
|
||||
import { JellifyTrack } from "../../types/JellifyTrack";
|
||||
import _ from 'lodash'
|
||||
import { JellifyTrack } from '../../types/JellifyTrack'
|
||||
|
||||
export function buildNewQueue(existingQueue: JellifyTrack[], tracksToInsert: JellifyTrack[], insertIndex: number) {
|
||||
export function buildNewQueue(
|
||||
existingQueue: JellifyTrack[],
|
||||
tracksToInsert: JellifyTrack[],
|
||||
insertIndex: number,
|
||||
) {
|
||||
console.debug(`Building new queue`)
|
||||
|
||||
console.debug(`Building new queue`);
|
||||
let newQueue: JellifyTrack[] = []
|
||||
|
||||
let newQueue : JellifyTrack[] = [];
|
||||
|
||||
if (_.isEmpty(existingQueue))
|
||||
newQueue = tracksToInsert;
|
||||
if (_.isEmpty(existingQueue)) newQueue = tracksToInsert
|
||||
else {
|
||||
newQueue = _.cloneDeep(existingQueue).splice(insertIndex, 0, ...tracksToInsert);
|
||||
newQueue = _.cloneDeep(existingQueue).splice(insertIndex, 0, ...tracksToInsert)
|
||||
}
|
||||
|
||||
console.debug(`Built new queue of ${newQueue.length} items`);
|
||||
console.debug(`Built new queue of ${newQueue.length} items`)
|
||||
|
||||
return newQueue;
|
||||
return newQueue
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import TrackPlayer, { RatingType } from "react-native-track-player"
|
||||
import { CAPABILITIES } from "../constants";
|
||||
import TrackPlayer, { RatingType } from 'react-native-track-player'
|
||||
import { CAPABILITIES } from '../constants'
|
||||
|
||||
export const useUpdateOptions = async (isFavorite: boolean) => {
|
||||
return await TrackPlayer.updateOptions({
|
||||
@@ -10,11 +10,11 @@ export const useUpdateOptions = async (isFavorite: boolean) => {
|
||||
ratingType: RatingType.Heart,
|
||||
likeOptions: {
|
||||
isActive: isFavorite,
|
||||
title: "Favorite"
|
||||
title: 'Favorite',
|
||||
},
|
||||
dislikeOptions: {
|
||||
isActive: !isFavorite,
|
||||
title: "Unfavorite"
|
||||
}
|
||||
});
|
||||
title: 'Unfavorite',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
import { JellifyTrack } from "../types/JellifyTrack";
|
||||
import { QueuingType } from "../enums/queuing-type";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { Queue } from "./types/queue-item";
|
||||
import { JellifyTrack } from '../types/JellifyTrack'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { Queue } from './types/queue-item'
|
||||
|
||||
export interface QueueMutation {
|
||||
track: BaseItemDto;
|
||||
index?: number | undefined;
|
||||
tracklist: BaseItemDto[];
|
||||
queue: Queue;
|
||||
queuingType?: QueuingType | undefined;
|
||||
track: BaseItemDto
|
||||
index?: number | undefined
|
||||
tracklist: BaseItemDto[]
|
||||
queue: Queue
|
||||
queuingType?: QueuingType | undefined
|
||||
}
|
||||
|
||||
export interface AddToQueueMutation {
|
||||
track: BaseItemDto,
|
||||
queuingType?: QueuingType | undefined;
|
||||
track: BaseItemDto
|
||||
queuingType?: QueuingType | undefined
|
||||
}
|
||||
|
||||
export interface QueueOrderMutation {
|
||||
newOrder: JellifyTrack[];
|
||||
from: number;
|
||||
to: number;
|
||||
newOrder: JellifyTrack[]
|
||||
from: number
|
||||
to: number
|
||||
}
|
||||
@@ -1,359 +1,376 @@
|
||||
import { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from "react";
|
||||
import { JellifyTrack } from "../types/JellifyTrack";
|
||||
import { storage } from "../constants/storage";
|
||||
import { MMKVStorageKeys } from "../enums/mmkv-storage-keys";
|
||||
import { findPlayNextIndexStart, findPlayQueueIndexStart } from "./helpers/index";
|
||||
import TrackPlayer, { Event, State, usePlaybackState, useTrackPlayerEvents } from "react-native-track-player";
|
||||
import { isEqual, isUndefined } from "lodash";
|
||||
import { handlePlaybackProgressUpdated, handlePlaybackState } from "./handlers";
|
||||
import { useUpdateOptions } from "../player/hooks";
|
||||
import { useMutation, UseMutationResult } from "@tanstack/react-query";
|
||||
import { mapDtoToTrack } from "../helpers/mappings";
|
||||
import { QueuingType } from "../enums/queuing-type";
|
||||
import { trigger } from "react-native-haptic-feedback";
|
||||
import { getActiveTrackIndex, getQueue, pause, seekTo, skip, skipToNext, skipToPrevious } from "react-native-track-player/lib/src/trackPlayer";
|
||||
import { convertRunTimeTicksToSeconds } from "../helpers/runtimeticks";
|
||||
import Client from "../api/client";
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from "./interfaces";
|
||||
import { Section } from "../components/Player/types";
|
||||
import { Queue } from "./types/queue-item";
|
||||
import { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react'
|
||||
import { JellifyTrack } from '../types/JellifyTrack'
|
||||
import { storage } from '../constants/storage'
|
||||
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
|
||||
import { findPlayNextIndexStart, findPlayQueueIndexStart } from './helpers/index'
|
||||
import TrackPlayer, {
|
||||
Event,
|
||||
State,
|
||||
usePlaybackState,
|
||||
useTrackPlayerEvents,
|
||||
} from 'react-native-track-player'
|
||||
import { isEqual, isUndefined } from 'lodash'
|
||||
import { handlePlaybackProgressUpdated, handlePlaybackState } from './handlers'
|
||||
import { useUpdateOptions } from '../player/hooks'
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query'
|
||||
import { mapDtoToTrack } from '../helpers/mappings'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import {
|
||||
getActiveTrackIndex,
|
||||
getQueue,
|
||||
pause,
|
||||
seekTo,
|
||||
skip,
|
||||
skipToNext,
|
||||
skipToPrevious,
|
||||
} from 'react-native-track-player/lib/src/trackPlayer'
|
||||
import { convertRunTimeTicksToSeconds } from '../helpers/runtimeticks'
|
||||
import Client from '../api/client'
|
||||
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
|
||||
import { Section } from '../components/Player/types'
|
||||
import { Queue } from './types/queue-item'
|
||||
|
||||
import * as Burnt from "burnt";
|
||||
import { markItemPlayed } from "../api/mutations/functions/item";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import * as Burnt from 'burnt'
|
||||
import { markItemPlayed } from '../api/mutations/functions/item'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
|
||||
interface PlayerContext {
|
||||
initialized: boolean;
|
||||
nowPlayingIsFavorite: boolean;
|
||||
setNowPlayingIsFavorite: React.Dispatch<SetStateAction<boolean>>;
|
||||
nowPlaying: JellifyTrack | undefined;
|
||||
playQueue: JellifyTrack[];
|
||||
queue: Queue;
|
||||
getQueueSectionData: () => Section[];
|
||||
useAddToQueue: UseMutationResult<void, Error, AddToQueueMutation, unknown>;
|
||||
useClearQueue: UseMutationResult<void, Error, void, unknown>;
|
||||
useRemoveFromQueue: UseMutationResult<void, Error, number, unknown>;
|
||||
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>;
|
||||
useTogglePlayback: UseMutationResult<void, Error, number | undefined, unknown>;
|
||||
useSeekTo: UseMutationResult<void, Error, number, unknown>;
|
||||
useSkip: UseMutationResult<void, Error, number | undefined, unknown>;
|
||||
usePrevious: UseMutationResult<void, Error, void, unknown>;
|
||||
usePlayNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>;
|
||||
playbackState: State | undefined;
|
||||
initialized: boolean
|
||||
nowPlayingIsFavorite: boolean
|
||||
setNowPlayingIsFavorite: React.Dispatch<SetStateAction<boolean>>
|
||||
nowPlaying: JellifyTrack | undefined
|
||||
playQueue: JellifyTrack[]
|
||||
queue: Queue
|
||||
getQueueSectionData: () => Section[]
|
||||
useAddToQueue: UseMutationResult<void, Error, AddToQueueMutation, unknown>
|
||||
useClearQueue: UseMutationResult<void, Error, void, unknown>
|
||||
useRemoveFromQueue: UseMutationResult<void, Error, number, unknown>
|
||||
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>
|
||||
useTogglePlayback: UseMutationResult<void, Error, number | undefined, unknown>
|
||||
useSeekTo: UseMutationResult<void, Error, number, unknown>
|
||||
useSkip: UseMutationResult<void, Error, number | undefined, unknown>
|
||||
usePrevious: UseMutationResult<void, Error, void, unknown>
|
||||
usePlayNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
|
||||
playbackState: State | undefined
|
||||
}
|
||||
|
||||
const PlayerContextInitializer = () => {
|
||||
|
||||
const nowPlayingJson = storage.getString(MMKVStorageKeys.NowPlaying)
|
||||
const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue);
|
||||
const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue)
|
||||
const queueJson = storage.getString(MMKVStorageKeys.Queue)
|
||||
|
||||
const playStateApi = getPlaystateApi(Client.api!)
|
||||
|
||||
//#region State
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
|
||||
const [nowPlayingIsFavorite, setNowPlayingIsFavorite] = useState<boolean>(false);
|
||||
const [nowPlaying, setNowPlaying] = useState<JellifyTrack | undefined>(nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined);
|
||||
const [isSkipping, setIsSkipping] = useState<boolean>(false);
|
||||
const [nowPlayingIsFavorite, setNowPlayingIsFavorite] = useState<boolean>(false)
|
||||
const [nowPlaying, setNowPlaying] = useState<JellifyTrack | undefined>(
|
||||
nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined,
|
||||
)
|
||||
const [isSkipping, setIsSkipping] = useState<boolean>(false)
|
||||
|
||||
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(playQueueJson ? JSON.parse(playQueueJson) : []);
|
||||
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(
|
||||
playQueueJson ? JSON.parse(playQueueJson) : [],
|
||||
)
|
||||
|
||||
const [queue, setQueue] = useState<Queue>(queueJson ? JSON.parse(queueJson) : 'Queue');
|
||||
const [queue, setQueue] = useState<Queue>(queueJson ? JSON.parse(queueJson) : 'Queue')
|
||||
//#endregion State
|
||||
|
||||
|
||||
//#region Functions
|
||||
const play = async (index?: number | undefined) => {
|
||||
|
||||
if (index && index > 0) {
|
||||
TrackPlayer.skip(index);
|
||||
TrackPlayer.skip(index)
|
||||
}
|
||||
|
||||
TrackPlayer.play();
|
||||
TrackPlayer.play()
|
||||
}
|
||||
|
||||
const getQueueSectionData: () => Section[] = () => {
|
||||
return Object.keys(QueuingType).map((type) => {
|
||||
return {
|
||||
title: type,
|
||||
data: playQueue.filter(track => track.QueuingType === type)
|
||||
data: playQueue.filter((track) => track.QueuingType === type),
|
||||
} as Section
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const resetQueue = async (hideMiniplayer?: boolean | undefined) => {
|
||||
console.debug("Clearing queue")
|
||||
await TrackPlayer.setQueue([]);
|
||||
setPlayQueue([]);
|
||||
console.debug('Clearing queue')
|
||||
await TrackPlayer.setQueue([])
|
||||
setPlayQueue([])
|
||||
}
|
||||
|
||||
const addToQueue = async (tracks: JellifyTrack[]) => {
|
||||
const insertIndex = await findPlayQueueIndexStart(playQueue);
|
||||
const insertIndex = await findPlayQueueIndexStart(playQueue)
|
||||
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`)
|
||||
|
||||
await TrackPlayer.add(tracks, insertIndex);
|
||||
await TrackPlayer.add(tracks, insertIndex)
|
||||
|
||||
setPlayQueue(await getQueue() as JellifyTrack[])
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
}
|
||||
|
||||
const addToNext = async (tracks: JellifyTrack[]) => {
|
||||
const insertIndex = await findPlayNextIndexStart(playQueue);
|
||||
const insertIndex = await findPlayNextIndexStart(playQueue)
|
||||
|
||||
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`);
|
||||
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`)
|
||||
|
||||
await TrackPlayer.add(tracks, insertIndex);
|
||||
await TrackPlayer.add(tracks, insertIndex)
|
||||
|
||||
setPlayQueue(await getQueue() as JellifyTrack[]);
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
}
|
||||
//#endregion Functions
|
||||
|
||||
//#region Hooks
|
||||
const useAddToQueue = useMutation({
|
||||
mutationFn: async (mutation: AddToQueueMutation) => {
|
||||
trigger("impactLight");
|
||||
trigger('impactLight')
|
||||
|
||||
if (mutation.queuingType === QueuingType.PlayingNext)
|
||||
return addToNext([mapDtoToTrack(mutation.track, mutation.queuingType)]);
|
||||
|
||||
else
|
||||
return addToQueue([mapDtoToTrack(mutation.track, mutation.queuingType)])
|
||||
return addToNext([mapDtoToTrack(mutation.track, mutation.queuingType)])
|
||||
else return addToQueue([mapDtoToTrack(mutation.track, mutation.queuingType)])
|
||||
},
|
||||
onSuccess: (data, { queuingType }) => {
|
||||
trigger("notificationSuccess");
|
||||
trigger('notificationSuccess')
|
||||
|
||||
Burnt.alert({
|
||||
title: queuingType === QueuingType.PlayingNext ? "Playing next" : "Added to queue",
|
||||
title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
|
||||
duration: 1,
|
||||
preset: 'done'
|
||||
});
|
||||
preset: 'done',
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
trigger("notificationError")
|
||||
}
|
||||
});
|
||||
trigger('notificationError')
|
||||
},
|
||||
})
|
||||
|
||||
const useRemoveFromQueue = useMutation({
|
||||
mutationFn: async (index: number) => {
|
||||
trigger("impactMedium");
|
||||
trigger('impactMedium')
|
||||
|
||||
await TrackPlayer.remove([index]);
|
||||
await TrackPlayer.remove([index])
|
||||
|
||||
setPlayQueue(await TrackPlayer.getQueue() as JellifyTrack[])
|
||||
}
|
||||
setPlayQueue((await TrackPlayer.getQueue()) as JellifyTrack[])
|
||||
},
|
||||
})
|
||||
|
||||
const useClearQueue = useMutation({
|
||||
mutationFn: async () => {
|
||||
trigger("effectDoubleClick")
|
||||
trigger('effectDoubleClick')
|
||||
|
||||
await TrackPlayer.removeUpcomingTracks();
|
||||
await TrackPlayer.removeUpcomingTracks()
|
||||
|
||||
setPlayQueue(await getQueue() as JellifyTrack[]);
|
||||
}
|
||||
});
|
||||
setPlayQueue((await getQueue()) as JellifyTrack[])
|
||||
},
|
||||
})
|
||||
|
||||
const useReorderQueue = useMutation({
|
||||
mutationFn: async (mutation: QueueOrderMutation) => {
|
||||
setPlayQueue(mutation.newOrder);
|
||||
await TrackPlayer.move(mutation.from, mutation.to);
|
||||
}
|
||||
setPlayQueue(mutation.newOrder)
|
||||
await TrackPlayer.move(mutation.from, mutation.to)
|
||||
},
|
||||
})
|
||||
|
||||
const useTogglePlayback = useMutation({
|
||||
mutationFn: (index?: number | undefined) => {
|
||||
trigger("impactMedium");
|
||||
if (playbackState === State.Playing)
|
||||
return pause();
|
||||
else
|
||||
return play(index);
|
||||
}
|
||||
});
|
||||
trigger('impactMedium')
|
||||
if (playbackState === State.Playing) return pause()
|
||||
else return play(index)
|
||||
},
|
||||
})
|
||||
|
||||
const useSeekTo = useMutation({
|
||||
mutationFn: async (position: number) => {
|
||||
trigger('impactLight');
|
||||
await seekTo(position);
|
||||
trigger('impactLight')
|
||||
await seekTo(position)
|
||||
|
||||
handlePlaybackProgressUpdated(Client.sessionId, playStateApi, nowPlaying!, {
|
||||
buffered: 0,
|
||||
position,
|
||||
duration: convertRunTimeTicksToSeconds(nowPlaying!.duration!)
|
||||
});
|
||||
}
|
||||
});
|
||||
duration: convertRunTimeTicksToSeconds(nowPlaying!.duration!),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const useSkip = useMutation({
|
||||
mutationFn: async (index?: number | undefined) => {
|
||||
trigger("impactMedium")
|
||||
trigger('impactMedium')
|
||||
|
||||
// Handle if this is the last track in the queue
|
||||
if (playQueue.length - 1 === await getActiveTrackIndex())
|
||||
return;
|
||||
|
||||
if (playQueue.length - 1 === (await getActiveTrackIndex())) return
|
||||
else {
|
||||
if (!isUndefined(index)) {
|
||||
setIsSkipping(true);
|
||||
setNowPlaying(playQueue[index]);
|
||||
await skip(index);
|
||||
setIsSkipping(false);
|
||||
}
|
||||
else {
|
||||
const nowPlayingIndex = playQueue.findIndex((track) => track.item.Id === nowPlaying!.item.Id);
|
||||
setIsSkipping(true)
|
||||
setNowPlaying(playQueue[index])
|
||||
await skip(index)
|
||||
setIsSkipping(false)
|
||||
} else {
|
||||
const nowPlayingIndex = playQueue.findIndex(
|
||||
(track) => track.item.Id === nowPlaying!.item.Id,
|
||||
)
|
||||
setNowPlaying(playQueue[nowPlayingIndex + 1])
|
||||
await skipToNext();
|
||||
await skipToNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
const usePrevious = useMutation({
|
||||
mutationFn: async () => {
|
||||
trigger("impactMedium");
|
||||
trigger('impactMedium')
|
||||
|
||||
const nowPlayingIndex = playQueue.findIndex((track) => track.item.Id === nowPlaying!.item.Id);
|
||||
const nowPlayingIndex = playQueue.findIndex(
|
||||
(track) => track.item.Id === nowPlaying!.item.Id,
|
||||
)
|
||||
|
||||
if (nowPlayingIndex > 0) {
|
||||
setNowPlaying(playQueue[nowPlayingIndex - 1])
|
||||
await skipToPrevious();
|
||||
}
|
||||
await skipToPrevious()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const usePlayNewQueue = useMutation({
|
||||
mutationFn: async (mutation: QueueMutation) => {
|
||||
trigger('effectDoubleClick')
|
||||
|
||||
trigger("effectDoubleClick");
|
||||
|
||||
setIsSkipping(true);
|
||||
setIsSkipping(true)
|
||||
|
||||
// Optimistically set now playing
|
||||
setNowPlaying(mapDtoToTrack(mutation.tracklist[mutation.index ?? 0], QueuingType.FromSelection));
|
||||
setNowPlaying(
|
||||
mapDtoToTrack(mutation.tracklist[mutation.index ?? 0], QueuingType.FromSelection),
|
||||
)
|
||||
|
||||
await resetQueue(false);
|
||||
await resetQueue(false)
|
||||
|
||||
await addToQueue(mutation.tracklist.map((track) => {
|
||||
await addToQueue(
|
||||
mutation.tracklist.map((track) => {
|
||||
return mapDtoToTrack(track, QueuingType.FromSelection)
|
||||
}));
|
||||
}),
|
||||
)
|
||||
|
||||
setQueue(mutation.queue);
|
||||
setQueue(mutation.queue)
|
||||
},
|
||||
onSuccess: async (data, mutation: QueueMutation) => {
|
||||
setIsSkipping(false);
|
||||
await play(mutation.index);
|
||||
setIsSkipping(false)
|
||||
await play(mutation.index)
|
||||
|
||||
if (typeof(mutation.queue) === 'object')
|
||||
await markItemPlayed(queue as BaseItemDto);
|
||||
if (typeof mutation.queue === 'object') await markItemPlayed(queue as BaseItemDto)
|
||||
},
|
||||
onError: async () => {
|
||||
setIsSkipping(false);
|
||||
setNowPlaying(await TrackPlayer.getActiveTrack() as JellifyTrack)
|
||||
}
|
||||
});
|
||||
setIsSkipping(false)
|
||||
setNowPlaying((await TrackPlayer.getActiveTrack()) as JellifyTrack)
|
||||
},
|
||||
})
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region RNTP Setup
|
||||
|
||||
const { state: playbackState } = usePlaybackState();
|
||||
const { state: playbackState } = usePlaybackState()
|
||||
|
||||
useTrackPlayerEvents([
|
||||
useTrackPlayerEvents(
|
||||
[
|
||||
Event.RemoteLike,
|
||||
Event.RemoteDislike,
|
||||
Event.PlaybackProgressUpdated,
|
||||
Event.PlaybackState,
|
||||
Event.PlaybackActiveTrackChanged,
|
||||
], async (event) => {
|
||||
],
|
||||
async (event) => {
|
||||
switch (event.type) {
|
||||
|
||||
case (Event.RemoteLike) : {
|
||||
|
||||
setNowPlayingIsFavorite(true);
|
||||
break;
|
||||
case Event.RemoteLike: {
|
||||
setNowPlayingIsFavorite(true)
|
||||
break
|
||||
}
|
||||
|
||||
case (Event.RemoteDislike) : {
|
||||
|
||||
setNowPlayingIsFavorite(false);
|
||||
break;
|
||||
case Event.RemoteDislike: {
|
||||
setNowPlayingIsFavorite(false)
|
||||
break
|
||||
}
|
||||
|
||||
case (Event.PlaybackState) : {
|
||||
handlePlaybackState(Client.sessionId, playStateApi, await TrackPlayer.getActiveTrack() as JellifyTrack, event.state);
|
||||
break;
|
||||
case Event.PlaybackState: {
|
||||
handlePlaybackState(
|
||||
Client.sessionId,
|
||||
playStateApi,
|
||||
(await TrackPlayer.getActiveTrack()) as JellifyTrack,
|
||||
event.state,
|
||||
)
|
||||
break
|
||||
}
|
||||
case (Event.PlaybackProgressUpdated) : {
|
||||
handlePlaybackProgressUpdated(Client.sessionId, playStateApi, nowPlaying!, event);
|
||||
break;
|
||||
case Event.PlaybackProgressUpdated: {
|
||||
handlePlaybackProgressUpdated(
|
||||
Client.sessionId,
|
||||
playStateApi,
|
||||
nowPlaying!,
|
||||
event,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case (Event.PlaybackActiveTrackChanged) : {
|
||||
|
||||
case Event.PlaybackActiveTrackChanged: {
|
||||
if (initialized && !isSkipping) {
|
||||
const activeTrack = await TrackPlayer.getActiveTrack() as JellifyTrack | undefined;
|
||||
const activeTrack = (await TrackPlayer.getActiveTrack()) as
|
||||
| JellifyTrack
|
||||
| undefined
|
||||
if (activeTrack && !isEqual(activeTrack, nowPlaying)) {
|
||||
setNowPlaying(activeTrack);
|
||||
setNowPlaying(activeTrack)
|
||||
|
||||
// Set player favorite state to user data IsFavorite
|
||||
// This is super nullish so we need to do a lot of
|
||||
// checks on the fields
|
||||
// TODO: Turn this check into a helper function
|
||||
setNowPlayingIsFavorite(
|
||||
isUndefined(activeTrack) ? false
|
||||
: isUndefined(activeTrack!.item.UserData) ? false
|
||||
: activeTrack.item.UserData.IsFavorite ?? false
|
||||
);
|
||||
|
||||
await useUpdateOptions(nowPlayingIsFavorite);
|
||||
isUndefined(activeTrack)
|
||||
? false
|
||||
: isUndefined(activeTrack!.item.UserData)
|
||||
? false
|
||||
: activeTrack.item.UserData.IsFavorite ?? false,
|
||||
)
|
||||
|
||||
await useUpdateOptions(nowPlayingIsFavorite)
|
||||
} else if (!activeTrack) {
|
||||
setNowPlaying(undefined)
|
||||
setNowPlayingIsFavorite(false);
|
||||
setNowPlayingIsFavorite(false)
|
||||
} else {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
|
||||
//#endregion RNTP Setup
|
||||
|
||||
//#region useEffects
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.Queue, JSON.stringify(queue))
|
||||
}, [
|
||||
queue
|
||||
])
|
||||
}, [queue])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized && playQueue)
|
||||
storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue))
|
||||
}, [
|
||||
playQueue
|
||||
])
|
||||
}, [playQueue])
|
||||
|
||||
useEffect(() => {
|
||||
if (initialized && nowPlaying)
|
||||
storage.set(MMKVStorageKeys.NowPlaying, JSON.stringify(nowPlaying))
|
||||
}, [
|
||||
nowPlaying
|
||||
])
|
||||
}, [nowPlaying])
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized && playQueue.length > 0 && nowPlaying) {
|
||||
TrackPlayer.setQueue(playQueue)
|
||||
.then(() => {
|
||||
TrackPlayer.skip(playQueue.findIndex(track => track.item.Id! === nowPlaying.item.Id!));
|
||||
});
|
||||
TrackPlayer.setQueue(playQueue).then(() => {
|
||||
TrackPlayer.skip(
|
||||
playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
}, [
|
||||
playQueue,
|
||||
nowPlaying
|
||||
])
|
||||
setInitialized(true)
|
||||
}, [playQueue, nowPlaying])
|
||||
//#endregion useEffects
|
||||
|
||||
//#region return
|
||||
@@ -386,7 +403,7 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
setNowPlayingIsFavorite: () => {},
|
||||
nowPlaying: undefined,
|
||||
playQueue: [],
|
||||
queue: "Recently Played",
|
||||
queue: 'Recently Played',
|
||||
getQueueSectionData: () => [],
|
||||
useAddToQueue: {
|
||||
mutate: () => {},
|
||||
@@ -399,12 +416,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
useClearQueue: {
|
||||
mutate: () => {},
|
||||
@@ -417,12 +434,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
useRemoveFromQueue: {
|
||||
mutate: () => {},
|
||||
@@ -435,12 +452,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
useReorderQueue: {
|
||||
mutate: () => {},
|
||||
@@ -453,12 +470,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
useTogglePlayback: {
|
||||
mutate: () => {},
|
||||
@@ -471,12 +488,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
useSeekTo: {
|
||||
mutate: () => {},
|
||||
@@ -489,12 +506,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
useSkip: {
|
||||
mutate: () => {},
|
||||
@@ -507,12 +524,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
usePrevious: {
|
||||
mutate: () => {},
|
||||
@@ -525,12 +542,12 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
usePlayNewQueue: {
|
||||
mutate: () => {},
|
||||
@@ -543,18 +560,22 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
isPaused: false,
|
||||
isPending: false,
|
||||
isSuccess: false,
|
||||
status: "idle",
|
||||
status: 'idle',
|
||||
reset: () => {},
|
||||
context: {},
|
||||
failureCount: 0,
|
||||
failureReason: null,
|
||||
submittedAt: 0
|
||||
submittedAt: 0,
|
||||
},
|
||||
playbackState: undefined,
|
||||
});
|
||||
})
|
||||
//#endregion Create PlayerContext
|
||||
|
||||
export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => {
|
||||
const {
|
||||
initialized,
|
||||
nowPlayingIsFavorite,
|
||||
@@ -573,9 +594,11 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS
|
||||
usePrevious,
|
||||
usePlayNewQueue,
|
||||
playbackState,
|
||||
} = PlayerContextInitializer();
|
||||
} = PlayerContextInitializer()
|
||||
|
||||
return <PlayerContext.Provider value={{
|
||||
return (
|
||||
<PlayerContext.Provider
|
||||
value={{
|
||||
initialized,
|
||||
nowPlayingIsFavorite,
|
||||
setNowPlayingIsFavorite,
|
||||
@@ -593,9 +616,11 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS
|
||||
usePrevious,
|
||||
usePlayNewQueue,
|
||||
playbackState,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PlayerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePlayerContext = () => useContext(PlayerContext);
|
||||
export const usePlayerContext = () => useContext(PlayerContext)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Client from "../api/client";
|
||||
import { JellifyTrack } from "../types/JellifyTrack";
|
||||
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import TrackPlayer, { Event, RatingType } from "react-native-track-player";
|
||||
import { getActiveTrack, getActiveTrackIndex } from "react-native-track-player/lib/src/trackPlayer";
|
||||
import Client from '../api/client'
|
||||
import { JellifyTrack } from '../types/JellifyTrack'
|
||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import TrackPlayer, { Event, RatingType } from 'react-native-track-player'
|
||||
import { getActiveTrack, getActiveTrackIndex } from 'react-native-track-player/lib/src/trackPlayer'
|
||||
|
||||
/**
|
||||
* Jellify Playback Service.
|
||||
@@ -11,25 +11,24 @@ import { getActiveTrack, getActiveTrackIndex } from "react-native-track-player/l
|
||||
* runs for the duration of the app lifecycle
|
||||
*/
|
||||
export async function PlaybackService() {
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemotePlay, async () => {
|
||||
await TrackPlayer.play();
|
||||
});
|
||||
await TrackPlayer.play()
|
||||
})
|
||||
TrackPlayer.addEventListener(Event.RemotePause, async () => {
|
||||
await TrackPlayer.pause();
|
||||
});
|
||||
await TrackPlayer.pause()
|
||||
})
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteNext, async () => {
|
||||
await TrackPlayer.skipToNext();
|
||||
});
|
||||
await TrackPlayer.skipToNext()
|
||||
})
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemotePrevious, async () => {
|
||||
await TrackPlayer.skipToPrevious();
|
||||
});
|
||||
await TrackPlayer.skipToPrevious()
|
||||
})
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteSeek, async (event) => {
|
||||
await TrackPlayer.seekTo(event.position);
|
||||
});
|
||||
await TrackPlayer.seekTo(event.position)
|
||||
})
|
||||
|
||||
// TrackPlayer.addEventListener(Event.RemoteJumpForward, async (event) => {
|
||||
// await TrackPlayer.seekBy(event.interval)
|
||||
@@ -40,32 +39,28 @@ export async function PlaybackService() {
|
||||
// });
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteLike, async () => {
|
||||
const nowPlaying = (await getActiveTrack()) as JellifyTrack
|
||||
const nowPlayingIndex = await getActiveTrackIndex()
|
||||
|
||||
const nowPlaying = await getActiveTrack() as JellifyTrack;
|
||||
const nowPlayingIndex = await getActiveTrackIndex();
|
||||
|
||||
await getUserLibraryApi(Client.api!)
|
||||
.markFavoriteItem({
|
||||
itemId: nowPlaying.item.Id!
|
||||
});
|
||||
await getUserLibraryApi(Client.api!).markFavoriteItem({
|
||||
itemId: nowPlaying.item.Id!,
|
||||
})
|
||||
|
||||
await TrackPlayer.updateMetadataForTrack(nowPlayingIndex!, {
|
||||
rating: RatingType.Heart
|
||||
rating: RatingType.Heart,
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener(Event.RemoteDislike, async () => {
|
||||
const nowPlaying = (await getActiveTrack()) as JellifyTrack
|
||||
const nowPlayingIndex = await getActiveTrackIndex()
|
||||
|
||||
const nowPlaying = await getActiveTrack() as JellifyTrack;
|
||||
const nowPlayingIndex = await getActiveTrackIndex();
|
||||
|
||||
await getUserLibraryApi(Client.api!)
|
||||
.markFavoriteItem({
|
||||
itemId: nowPlaying.item.Id!
|
||||
});
|
||||
await getUserLibraryApi(Client.api!).markFavoriteItem({
|
||||
itemId: nowPlaying.item.Id!,
|
||||
})
|
||||
|
||||
await TrackPlayer.updateMetadataForTrack(nowPlayingIndex!, {
|
||||
rating: undefined
|
||||
});
|
||||
});
|
||||
rating: undefined,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export type Queue = BaseItemDto | "Recently Played" | "Search" | "Favorite Tracks";
|
||||
export type Queue = BaseItemDto | 'Recently Played' | 'Search' | 'Favorite Tracks'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"
|
||||
import { QueuingType } from "../../enums/queuing-type"
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
|
||||
export interface QueuingRequest {
|
||||
song: BaseItemDto
|
||||
|
||||
@@ -11,4 +11,4 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
]
|
||||
"extends": ["config:recommended"]
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ import { headingFont, bodyFont } from './fonts.config'
|
||||
const tokens = createTokens({
|
||||
...TamaguiTokens,
|
||||
color: {
|
||||
danger: "#ff0000",
|
||||
purpleDark: "#0C0622",
|
||||
purple: "#100538",
|
||||
purpleGray: "#66617B",
|
||||
amethyst: "#7E72AF",
|
||||
grape: "#5638BB",
|
||||
telemagenta: "#cc2f71",
|
||||
white: "#ffffff",
|
||||
black: "#000000"
|
||||
danger: '#ff0000',
|
||||
purpleDark: '#0C0622',
|
||||
purple: '#100538',
|
||||
purpleGray: '#66617B',
|
||||
amethyst: '#7E72AF',
|
||||
grape: '#5638BB',
|
||||
telemagenta: '#cc2f71',
|
||||
white: '#ffffff',
|
||||
black: '#000000',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -35,26 +35,26 @@ const jellifyConfig = createTamagui({
|
||||
backgroundFocus: tokens.color.amethyst,
|
||||
backgroundHover: tokens.color.purpleGray,
|
||||
borderColor: tokens.color.amethyst,
|
||||
color: tokens.color.white
|
||||
color: tokens.color.white,
|
||||
},
|
||||
dark_inverted_purple: {
|
||||
color: tokens.color.purpleDark,
|
||||
borderColor: tokens.color.amethyst,
|
||||
background: tokens.color.amethyst
|
||||
background: tokens.color.amethyst,
|
||||
},
|
||||
light: {
|
||||
background: tokens.color.white,
|
||||
backgroundActive: tokens.color.amethyst,
|
||||
borderColor: tokens.color.purpleGray,
|
||||
color: tokens.color.purpleDark
|
||||
color: tokens.color.purpleDark,
|
||||
},
|
||||
light_inverted_purple: {
|
||||
color: tokens.color.purpleDark,
|
||||
borderColor: tokens.color.purpleDark,
|
||||
background: tokens.color.purpleGray
|
||||
}
|
||||
}
|
||||
});
|
||||
background: tokens.color.purpleGray,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type JellifyConfig = typeof jellifyConfig
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
"extends": "@react-native/typescript-config/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
export interface JellifyLibrary {
|
||||
musicLibraryId: string;
|
||||
musicLibraryName?: string | undefined;
|
||||
musicLibraryPrimaryImageId?: string | undefined;
|
||||
playlistLibraryId?: string | undefined;
|
||||
playlistLibraryPrimaryImageId?: string | undefined;
|
||||
musicLibraryId: string
|
||||
musicLibraryName?: string | undefined
|
||||
musicLibraryPrimaryImageId?: string | undefined
|
||||
playlistLibraryId?: string | undefined
|
||||
playlistLibraryPrimaryImageId?: string | undefined
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface JellifyServer {
|
||||
url: string;
|
||||
address: string;
|
||||
name: string;
|
||||
version: string;
|
||||
startUpComplete: boolean;
|
||||
url: string
|
||||
address: string
|
||||
name: string
|
||||
version: string
|
||||
startUpComplete: boolean
|
||||
}
|
||||
@@ -1,32 +1,34 @@
|
||||
import { PitchAlgorithm, RatingType, Track, TrackType } from "react-native-track-player"
|
||||
import { QueuingType } from "../enums/queuing-type";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { PitchAlgorithm, RatingType, Track, TrackType } from 'react-native-track-player'
|
||||
import { QueuingType } from '../enums/queuing-type'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
|
||||
export interface JellifyTrack extends Track {
|
||||
url: string;
|
||||
type?: TrackType | undefined;
|
||||
userAgent?: string | undefined;
|
||||
contentType?: string | undefined;
|
||||
pitchAlgorithm?: PitchAlgorithm | undefined;
|
||||
headers?: { [key: string]: any; } | undefined;
|
||||
url: string
|
||||
type?: TrackType | undefined
|
||||
userAgent?: string | undefined
|
||||
contentType?: string | undefined
|
||||
pitchAlgorithm?: PitchAlgorithm | undefined
|
||||
|
||||
title?: string | undefined;
|
||||
album?: string | undefined;
|
||||
artist?: string | undefined;
|
||||
duration?: number | undefined;
|
||||
artwork?: string | undefined;
|
||||
description?: string | undefined;
|
||||
genre?: string | undefined;
|
||||
date?: string | undefined;
|
||||
rating?: RatingType | undefined;
|
||||
isLiveStream?: boolean | undefined;
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
headers?: { [key: string]: any } | undefined
|
||||
|
||||
item: BaseItemDto;
|
||||
title?: string | undefined
|
||||
album?: string | undefined
|
||||
artist?: string | undefined
|
||||
duration?: number | undefined
|
||||
artwork?: string | undefined
|
||||
description?: string | undefined
|
||||
genre?: string | undefined
|
||||
date?: string | undefined
|
||||
rating?: RatingType | undefined
|
||||
isLiveStream?: boolean | undefined
|
||||
|
||||
item: BaseItemDto
|
||||
|
||||
/**
|
||||
* Represents the type of queuing for this song, be it that it was
|
||||
* queued from the selection chosen, queued by the user directly, or marked
|
||||
* to play next by the user
|
||||
*/
|
||||
QueuingType?: QueuingType | undefined ;
|
||||
QueuingType?: QueuingType | undefined
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface JellifyUser {
|
||||
id: string;
|
||||
name: string;
|
||||
accessToken: string;
|
||||
id: string
|
||||
name: string
|
||||
accessToken: string
|
||||
}
|
||||
Reference in New Issue
Block a user