[skip actions]

run prettier
This commit is contained in:
Violet Caulfield
2025-04-12 10:16:10 -05:00
parent fef070d95d
commit 5ca00df6f7
48 changed files with 23654 additions and 23592 deletions

View File

@@ -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
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -1,31 +1,36 @@
![Jellify App Icon](assets/icon_dark_60pt_3x.png)
# 🪼 Jellify
[![publish-beta](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml/badge.svg?branch=main)](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml)
### 🔗 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
[![Made with React](https://img.shields.io/badge/React-18-blue?logo=react&logoColor=white)](https://reactjs.org “Go to React homepage”) [![Made with TypeScript](https://img.shields.io/badge/TypeScript-5-blue?logo=typescript&logoColor=white)](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_.
- Youve been instrumental in shaping its user experience, my rock during development, and an overall inspiration in my life

View File

@@ -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.

View File

@@ -4,5 +4,5 @@ module.exports = {
'react-native-boost/plugin',
// react-native-reanimated/plugin has to be listed last
'react-native-reanimated/plugin',
]
};
],
}

View File

@@ -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={{

View File

@@ -24,7 +24,8 @@ export default function Login(): React.JSX.Element {
? 'ServerAuthentication'
: 'LibrarySelection'
}
screenOptions={{ headerShown: false }}>
screenOptions={{ headerShown: false }}
>
<Stack.Screen
name='ServerAddress'
options={{

View File

@@ -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>

View File

@@ -80,7 +80,8 @@ export const JellifyProvider: ({ children }: { children: ReactNode }) => React.J
loggedIn,
setLoggedIn,
carPlayConnected,
}}>
}}
>
{children}
</JellifyContext.Provider>
)

View File

@@ -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,
})

View File

@@ -1,2 +1,2 @@
// Only import react-native-gesture-handler on native platforms
import 'react-native-gesture-handler';
import 'react-native-gesture-handler'

View File

@@ -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)

View File

@@ -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.

View File

@@ -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-.*)/)',
],
};
}

View File

@@ -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 />)
})

View File

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

View File

@@ -1,7 +1,7 @@
jest.mock('react-native-carplay', () => {
return {
default: {
checkForConnection: jest.fn()
}
checkForConnection: jest.fn(),
},
}
})

View File

@@ -3,4 +3,4 @@ jest.mock('react-native-reanimated', () => ({
createAnimatedPropAdapter: jest.fn,
useReducedMotion: jest.fn,
LayoutAnimationConfig: jest.fn,
}));
}))

View File

@@ -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',
},
}
});
})

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -1,4 +1,4 @@
import { Capability } from "react-native-track-player";
import { Capability } from 'react-native-track-player'
export const CAPABILITIES: Capability[] = [
Capability.Pause,

View File

@@ -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

View File

@@ -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!,
)
}

View File

@@ -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
}

View File

@@ -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',
},
})
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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,
})
})
}

View File

@@ -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'

View File

@@ -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

View File

@@ -11,4 +11,4 @@ module.exports = {
},
},
},
};
}

View File

@@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
"extends": ["config:recommended"]
}

View File

@@ -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

View File

@@ -2,9 +2,7 @@
"extends": "@react-native/typescript-config/tsconfig.json",
"compilerOptions": {
"paths": {
"@/*": [
"./*"
]
"@/*": ["./*"]
}
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,5 +1,5 @@
export interface JellifyUser {
id: string;
name: string;
accessToken: string;
id: string
name: string
accessToken: string
}