Merge branch 'main' of github.com:Jellify-Music/App into 466-feature-add-quick-connect-support
0
.env.devrelease
Normal file
2
.github/FUNDING.yml
vendored
@@ -3,7 +3,7 @@
|
|||||||
github: [anultravioletaurora, riteshshukla04, felinusfish, skalthoff] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
github: [anultravioletaurora, riteshshukla04, felinusfish, skalthoff] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
patreon: anultravioletaurora # Replace with a single Patreon username
|
patreon: anultravioletaurora # Replace with a single Patreon username
|
||||||
open_collective: # Replace with a single Open Collective username
|
open_collective: # Replace with a single Open Collective username
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
ko_fi: jellify # Replace with a single Ko-fi username
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
|||||||
121
.github/ISSUE_TEMPLATE/01_report_issue.yaml
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
name: Report an Issue
|
||||||
|
description: Did you experience an issue using Jellify? Please report it here.
|
||||||
|
title: "[BUG] "
|
||||||
|
labels: ["bug"]
|
||||||
|
type: bug
|
||||||
|
assignees:
|
||||||
|
- anultravioletaurora
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "Please provide the following information to help us investigate and resolve the issue."
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: summary
|
||||||
|
attributes:
|
||||||
|
label: Describe the issue.
|
||||||
|
description: A clear and concise summary of the issue you are experiencing.
|
||||||
|
placeholder: "When I do X, Y happens instead of Z. This causes..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction_steps
|
||||||
|
attributes:
|
||||||
|
label: Can the issue be reproduced?
|
||||||
|
description: "If you can reproduce the issue, please provide step-by-step instructions below. Otherwise, leave this field blank."
|
||||||
|
placeholder: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected_behavior
|
||||||
|
attributes:
|
||||||
|
label: What is the expected behavior?
|
||||||
|
description: What did you expect to happen in an ideal situation?
|
||||||
|
placeholder: "X should happen when Y is done."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual_behavior
|
||||||
|
attributes:
|
||||||
|
label: What actually happened in this case?
|
||||||
|
description: Try to provide a detailed description if you can.
|
||||||
|
placeholder: "When I did Y, Z happened instead."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os-types
|
||||||
|
attributes:
|
||||||
|
label: What operating system(s) were you using when the issue occurred?
|
||||||
|
description: Which operating systems did the issue happen? Please select all that apply.
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Android
|
||||||
|
- iOS
|
||||||
|
- iPadOS
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: device-model
|
||||||
|
attributes:
|
||||||
|
label: Which device(s) were you using when the issue occurred?
|
||||||
|
description: Please specify the model of your device.
|
||||||
|
placeholder: "Google Pixel 7 / iPhone 14 Pro"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: os-version
|
||||||
|
attributes:
|
||||||
|
label: Which OS version(s) did you experience this on?
|
||||||
|
description: Please specify the version of your operating system.
|
||||||
|
placeholder: "Android 16 / iOS 26"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: jellify-version
|
||||||
|
attributes:
|
||||||
|
label: Which version of Jellify (and OTA Version, if applicable)?
|
||||||
|
description: Which version of Jellify are you using? You can find this in the app settings. You can find this in Settings -> About.
|
||||||
|
placeholder: "Jellify 0.19.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Which distribution are you using?
|
||||||
|
description: As Jellify is distributed on multiple platforms, please select which version you are using.
|
||||||
|
options:
|
||||||
|
- Apple App Store
|
||||||
|
- Google Play Store
|
||||||
|
- GitHub Releases
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional_info
|
||||||
|
attributes:
|
||||||
|
label: Any additional information goes here.
|
||||||
|
description: Any additional information, logs, or screenshots that may help in diagnosing the issue.
|
||||||
|
placeholder: "Provide any additional information here."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: relevant_links
|
||||||
|
attributes:
|
||||||
|
label: If there any relevant links, please provide them here.
|
||||||
|
description: Links to Discord messages, GitHub issues, or other resources related to this issue.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
64
.github/ISSUE_TEMPLATE/02_request_feature.yaml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Request a Feature
|
||||||
|
description: Have an idea for a new feature or improvement? We'd love to hear it!
|
||||||
|
title: "[FEATURE] "
|
||||||
|
labels: ["enhancement"]
|
||||||
|
type: feature
|
||||||
|
assignees:
|
||||||
|
- anultravioletaurora
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "Please provide the following information to help us understand and evaluate your feature request."
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: feature_summary
|
||||||
|
attributes:
|
||||||
|
label: Summarize the requested feature.
|
||||||
|
description: A clear and concise summary of the feature you are requesting. Be sure to provide more details in the next section.
|
||||||
|
placeholder: "Jellify would benefit from a feature that..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem_description
|
||||||
|
attributes:
|
||||||
|
label: Is there a problem this feature solves?
|
||||||
|
description: If this feature is solving an existing problem, please describe context and details.
|
||||||
|
placeholder: "This feature would help by..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposed_solution
|
||||||
|
attributes:
|
||||||
|
label: Describe your solution in as much detail as possible.
|
||||||
|
description: Describe your proposed solution and how it would work
|
||||||
|
placeholder: "The feature could work by..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Have you considered any alternatives?
|
||||||
|
description: What alternative solutions have you considered?
|
||||||
|
placeholder: "The alternatives I have considered are..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional_info
|
||||||
|
attributes:
|
||||||
|
label: Any additional information goes here.
|
||||||
|
description: Any additional information, logs, or screenshots that may help in understanding your feature request.
|
||||||
|
placeholder: "Provide any additional information here."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: relevant_links
|
||||||
|
attributes:
|
||||||
|
label: If there any relevant links, please provide them here.
|
||||||
|
description: Links to Discord messages, GitHub issues, or other resources related to this issue.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve Jellify
|
|
||||||
title: "[BUG]"
|
|
||||||
type: bug
|
|
||||||
assignees: anultravioletaurora
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
_A clear and concise description of what the bug is._
|
|
||||||
|
|
||||||
|
|
||||||
**How did this happen?**
|
|
||||||
_Steps to reproduce the behavior_
|
|
||||||
|
|
||||||
|
|
||||||
**What should have happened?**
|
|
||||||
_A clear and concise description of what you expected to happen._
|
|
||||||
|
|
||||||
|
|
||||||
**Screenshots / Recordings**
|
|
||||||
_A picture is worth a thousand words_
|
|
||||||
|
|
||||||
|
|
||||||
**What Device do you have?**
|
|
||||||
- Device: [e.g. iPhone 15 Pro]
|
|
||||||
- OS: [e.g. iOS26]
|
|
||||||
- Version [e.g. 0.19.1]
|
|
||||||
|
|
||||||
**Anything else we should know?**
|
|
||||||
_Add any other context about the problem here._
|
|
||||||
|
|
||||||
**Relevant Links**
|
|
||||||
_Links from Discord, TestFlight, etc_
|
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Discord Server
|
||||||
|
url: https://discord.gg/jellify
|
||||||
|
about: Please ask and answer questions here.
|
||||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for Jellify
|
|
||||||
title: "[FEATURE]"
|
|
||||||
type: feature
|
|
||||||
labels: enhancement
|
|
||||||
assignees: anultravioletaurora
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
72
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# JavaScript/TypeScript dependencies (npm/bun)
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
groups:
|
||||||
|
react-native:
|
||||||
|
patterns:
|
||||||
|
- "react-native*"
|
||||||
|
- "@react-native*"
|
||||||
|
react-navigation:
|
||||||
|
patterns:
|
||||||
|
- "@react-navigation/*"
|
||||||
|
tanstack:
|
||||||
|
patterns:
|
||||||
|
- "@tanstack/*"
|
||||||
|
tamagui:
|
||||||
|
patterns:
|
||||||
|
- "tamagui"
|
||||||
|
- "@tamagui/*"
|
||||||
|
babel:
|
||||||
|
patterns:
|
||||||
|
- "@babel/*"
|
||||||
|
eslint:
|
||||||
|
patterns:
|
||||||
|
- "eslint*"
|
||||||
|
- "@eslint/*"
|
||||||
|
types:
|
||||||
|
patterns:
|
||||||
|
- "@types/*"
|
||||||
|
ignore:
|
||||||
|
# Ignore major version updates for React Native (can have breaking changes)
|
||||||
|
- dependency-name: "react-native"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
- dependency-name: "react"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
|
||||||
|
# Ruby dependencies for iOS (CocoaPods, Fastlane)
|
||||||
|
- package-ecosystem: "bundler"
|
||||||
|
directory: "/ios"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
|
||||||
|
# Ruby dependencies for Android (Fastlane)
|
||||||
|
- package-ecosystem: "bundler"
|
||||||
|
directory: "/android"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
|
||||||
|
# Root Gemfile (if used)
|
||||||
|
- package-ecosystem: "bundler"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
|
|
||||||
|
# GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
open-pull-requests-limit: 15
|
||||||
37
.github/workflows/build-android.yml
vendored
@@ -1,6 +1,12 @@
|
|||||||
name: Build Android APK
|
name: Build Android APK
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/build-android.yml'
|
||||||
|
- 'android/**'
|
||||||
|
- 'package.json'
|
||||||
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -12,12 +18,11 @@ jobs:
|
|||||||
- name: 🛒 Checkout
|
- name: 🛒 Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: 💎 Set up Ruby
|
- name: 💎 Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
@@ -25,8 +30,8 @@ jobs:
|
|||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: 💬 Echo package.json version to Github ENV
|
- name: 💬 Echo package.json version to Github ENV
|
||||||
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
|
run: echo VERSION_NUMBER=$(bun -p "require('./package.json').version") >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 🔑 Decode and setup release keystore
|
- name: 🔑 Decode and setup release keystore
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
@@ -36,31 +41,31 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "No keystore secret found, will use debug keystore"
|
echo "No keystore secret found, will use debug keystore"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
node_modules
|
node_modules
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
~/.cache/turbo
|
~/.cache/turbo
|
||||||
android/.gradle
|
android/.gradle
|
||||||
android/app/build
|
android/app/build
|
||||||
key: ${{ runner.os }}-gradle-turbo-${{ hashFiles('**/yarn.lock', '**/build.gradle') }}
|
key: ${{ runner.os }}-gradle-turbo-${{ hashFiles('**/build.gradle') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-turbo-
|
${{ runner.os }}-gradle-turbo-
|
||||||
|
|
||||||
- name: 🤖 Run yarn init-android
|
- name: 🤖 Run bun init-android
|
||||||
run: yarn install --network-concurrency 1
|
run: bun i
|
||||||
|
|
||||||
- name: 🚀 Run turbo build
|
- name: 🚀 Run turbo build
|
||||||
run: yarn android-build
|
run: bun android-build
|
||||||
env:
|
env:
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
|
||||||
|
|
||||||
- name: 📦 Upload APK for testing
|
- name: 📦 Upload APK for testing
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
16
.github/workflows/build-bundle.yml
vendored
@@ -16,20 +16,19 @@ jobs:
|
|||||||
- name: 🧾 Checkout repository
|
- name: 🧾 Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: ⚙️ Setup Node.js
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
bun-version: 1.3.2
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
run: yarn install --network-concurrency 1
|
run: bun i
|
||||||
|
|
||||||
|
|
||||||
- name: 🧩 Build JS bundle for iOS
|
- name: 🧩 Build JS bundle for iOS
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ios/build
|
mkdir -p ios/build
|
||||||
npx react-native bundle \
|
bun x react-native bundle \
|
||||||
--platform ios \
|
--platform ios \
|
||||||
--dev false \
|
--dev false \
|
||||||
--entry-file index.js \
|
--entry-file index.js \
|
||||||
@@ -40,12 +39,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p android/app/src/main/assets
|
mkdir -p android/app/src/main/assets
|
||||||
mkdir -p android/app/src/main/res
|
mkdir -p android/app/src/main/res
|
||||||
npx react-native bundle \
|
bun x react-native bundle \
|
||||||
--platform android \
|
--platform android \
|
||||||
--dev false \
|
--dev false \
|
||||||
--entry-file index.js \
|
--entry-file index.js \
|
||||||
--bundle-output android/app/src/main/assets/index.android.bundle \
|
--bundle-output android/app/src/main/assets/index.android.bundle \
|
||||||
--assets-dest android/app/src/main/res
|
--assets-dest android/app/src/main/res
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
24
.github/workflows/build-ios.yml
vendored
@@ -1,7 +1,11 @@
|
|||||||
name: Build iOS IPA
|
name: Build iOS IPA
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/build-ios.yml'
|
||||||
|
- 'ios/**'
|
||||||
|
- 'package.json'
|
||||||
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -15,21 +19,21 @@ jobs:
|
|||||||
- name: 🛒 Checkout
|
- name: 🛒 Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
|
|
||||||
- name: 💬 Echo package.json version to Github ENV
|
- name: 💬 Echo package.json version to Github ENV
|
||||||
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
|
run: echo VERSION_NUMBER=$(bun -p "require('./package.json').version") >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 🍎 Setup Xcode
|
- name: 🍎 Setup Xcode
|
||||||
uses: ./.github/actions/setup-xcode
|
uses: ./.github/actions/setup-xcode
|
||||||
|
|
||||||
- name: 🍎 Run yarn init-ios:new-arch
|
|
||||||
run: yarn init-android && cd ios && bundle install && bundle exec pod install
|
|
||||||
|
|
||||||
|
- name: 🍎 Run bun init-ios:new-arch
|
||||||
|
run: bun run init-android && cd ios && bundle install && bundle exec pod install
|
||||||
|
|
||||||
|
|
||||||
- name: 🚀 Run fastlane build
|
- name: 🚀 Run fastlane build
|
||||||
run: |
|
run: |
|
||||||
cd ios
|
cd ios
|
||||||
@@ -61,4 +65,4 @@ jobs:
|
|||||||
*.zip
|
*.zip
|
||||||
ios/build/Build/Products/Release-iphonesimulator/Jellify-Release-Simulator.zip
|
ios/build/Build/Products/Release-iphonesimulator/Jellify-Release-Simulator.zip
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
if-no-files-found: warn
|
if-no-files-found: warn
|
||||||
|
|||||||
78
.github/workflows/maestro-test.yml
vendored
@@ -1,13 +1,14 @@
|
|||||||
name: Run Maestro Tests
|
name: Run Maestro Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * *"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-android:
|
build-android:
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
@@ -16,19 +17,18 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: 🛒 Checkout
|
- name: 🛒 Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
cache: 'yarn'
|
|
||||||
|
|
||||||
- name: 💎 Set up Ruby
|
- name: 💎 Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.0'
|
ruby-version: '3.0'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -38,25 +38,25 @@ jobs:
|
|||||||
~/.cache/turbo
|
~/.cache/turbo
|
||||||
android/.gradle
|
android/.gradle
|
||||||
android/app/build
|
android/app/build
|
||||||
key: ${{ runner.os }}-gradle-turbo-${{ hashFiles('**/yarn.lock', '**/build.gradle') }}
|
key: ${{ runner.os }}-gradle-turbo-${{ hashFiles('**/bun.lock', '**/build.gradle') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-turbo-
|
${{ runner.os }}-gradle-turbo-
|
||||||
|
|
||||||
- name: 🍎 Run yarn init-android
|
- name: 🍎 Run bun init-android
|
||||||
run: yarn install --network-concurrency 1
|
run: bun i
|
||||||
|
|
||||||
- name: 💬 Disable OTA Updates and Enable Maestro Build
|
- name: 💬 Disable OTA Updates and Enable Maestro Build
|
||||||
run: node scripts/updateEnv.js OTA_UPDATE_ENABLED=false IS_MAESTRO_BUILD=true
|
run: bun scripts/updateEnv.js OTA_UPDATE_ENABLED=false IS_MAESTRO_BUILD=true
|
||||||
|
|
||||||
|
|
||||||
- name: ✅ Validate Config Files
|
- name: ✅ Validate Config Files
|
||||||
run: |
|
run: |
|
||||||
node -e "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
bun -p "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
||||||
node -e "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
bun -p "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
||||||
|
|
||||||
- name: 🚀 Run Android fastlane build
|
- name: 🚀 Run Android fastlane build
|
||||||
run: yarn android-build
|
run: bun run android-build
|
||||||
|
|
||||||
- name: 📤 Upload Android Artifacts
|
- name: 📤 Upload Android Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -73,10 +73,10 @@ jobs:
|
|||||||
- name: 🛒 Checkout
|
- name: 🛒 Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
|
|
||||||
- name: Installing Maestro
|
- name: Installing Maestro
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -88,17 +88,15 @@ jobs:
|
|||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
|
|
||||||
- name: 💎 Set up Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: '3.0'
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: ⬇️ Download Android Artifacts
|
- name: ⬇️ Download Android Artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: android-artifacts
|
name: android-artifacts
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
- uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: '3.4' # Not needed with a .ruby-version, .tool-versions or mise.toml
|
||||||
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- name: Enable KVM group perms
|
- name: Enable KVM group perms
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -121,8 +119,22 @@ jobs:
|
|||||||
cores: '4'
|
cores: '4'
|
||||||
disable-animations: false
|
disable-animations: false
|
||||||
avd-name: e2e_emulator
|
avd-name: e2e_emulator
|
||||||
script: |
|
script: bash scripts/maestro-android-retry.sh "https://jellyfin.jellify.app" "jerry"
|
||||||
node scripts/maestro-android.js "https://jellyfin.jellify.app" "jerry"
|
|
||||||
|
|
||||||
|
- name: 🗣️ Notify Success on Discord
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
bun scripts/sendDiscordMessage.js "__**## ✅ Maestro Test Passed**__All checks completed successfully!"
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.MAESTRO_WEBHOOK_RESULTS }}
|
||||||
|
|
||||||
|
- name: 🗣️ Notify Failure on Discord
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
bun scripts/sendDiscordMessage.js "__**## ❌ Maestro Test Failed**__Some tests did not pass."
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK_URL: ${{ secrets.MAESTRO_WEBHOOK_RESULTS }}
|
||||||
- name: Store tests result
|
- name: Store tests result
|
||||||
uses: actions/upload-artifact@v4.3.4
|
uses: actions/upload-artifact@v4.3.4
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
129
.github/workflows/publish-beta.yml
vendored
@@ -22,7 +22,7 @@ on:
|
|||||||
- minor
|
- minor
|
||||||
- patch
|
- patch
|
||||||
- major
|
- major
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-release-notes:
|
generate-release-notes:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -33,8 +33,12 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.SIGNING_REPO_PAT }}
|
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.2
|
||||||
- name: 🧠 Collect commit messages
|
- name: 🧠 Collect commit messages
|
||||||
id: commits
|
id: commits
|
||||||
run: |
|
run: |
|
||||||
@@ -54,10 +58,10 @@ jobs:
|
|||||||
} >> "$GITHUB_OUTPUT"
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
|
||||||
- name: 📜 Generate release notes using Node.js
|
- name: 📜 Generate release notes using Bun
|
||||||
run: |
|
run: |
|
||||||
yarn install --network-concurrency 1
|
bun i
|
||||||
node scripts/generate-release-notes.js "${{ steps.commits.outputs.messages }}"
|
bun scripts/generate-release-notes.js "${{ steps.commits.outputs.messages }}"
|
||||||
env:
|
env:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
|
||||||
@@ -79,32 +83,32 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.SIGNING_REPO_PAT }}
|
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
|
|
||||||
- name: 💎 Set up Ruby
|
- name: 💎 Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.0'
|
ruby-version: '3.0'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|
||||||
- name: 🍎 Run yarn init-android
|
- name: 🍎 Run bun init-android # you never actually run yarn init-android, so I kept the same
|
||||||
run: yarn install --network-concurrency 1
|
run: bun i
|
||||||
|
|
||||||
- name: ➕ Version Up
|
- name: ➕ Version Up
|
||||||
if: ${{ github.event.inputs['version-bump'] != 'No Bump' }}
|
if: ${{ github.event.inputs['version-bump'] != 'No Bump' }}
|
||||||
run: yarn react-native bump-version --type ${{ github.event.inputs['version-bump'] }}
|
run: bun x react-native bump-version --type ${{ github.event.inputs['version-bump'] }} # Is this supposed to be like npx, or some special yarn thing?
|
||||||
|
|
||||||
- id: setver
|
- id: setver
|
||||||
run: echo "version=$(node -p -e "require('./package.json').version")" >> $GITHUB_OUTPUT
|
run: echo "version=$(bun -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- name: 💬 Echo package.json version to Github ENV
|
- name: 💬 Echo package.json version to Github ENV
|
||||||
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
|
run: echo VERSION_NUMBER=$(bun -p "require('./package.json').version") >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 🔑 Setup release keystore
|
- name: 🔑 Setup release keystore
|
||||||
run: |
|
run: |
|
||||||
if [ -n "${{ secrets.ANDROID_SIGNING_BASE64 }}" ]; then
|
if [ -n "${{ secrets.ANDROID_SIGNING_BASE64 }}" ]; then
|
||||||
@@ -114,7 +118,7 @@ jobs:
|
|||||||
echo "ERROR: No keystore secret found!"
|
echo "ERROR: No keystore secret found!"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: 🔑 Setup Play Store credentials
|
- name: 🔑 Setup Play Store credentials
|
||||||
run: |
|
run: |
|
||||||
if [ -n "$PLAY_STORE_CREDENTIALS" ]; then
|
if [ -n "$PLAY_STORE_CREDENTIALS" ]; then
|
||||||
@@ -126,8 +130,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
env:
|
env:
|
||||||
PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
|
PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
|
||||||
|
|
||||||
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
|
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
|
||||||
run: |
|
run: |
|
||||||
echo "{" > telemetrydeck.json
|
echo "{" > telemetrydeck.json
|
||||||
echo "\"appID\": \"${{ secrets.TELEMETRYDECK_APPID }}\"," >> telemetrydeck.json
|
echo "\"appID\": \"${{ secrets.TELEMETRYDECK_APPID }}\"," >> telemetrydeck.json
|
||||||
@@ -135,25 +139,29 @@ jobs:
|
|||||||
echo "\"app\": \"Jellify\"" >> telemetrydeck.json
|
echo "\"app\": \"Jellify\"" >> telemetrydeck.json
|
||||||
echo "}" >> telemetrydeck.json
|
echo "}" >> telemetrydeck.json
|
||||||
|
|
||||||
|
|
||||||
- name: 🤫 Output Glitchtip Secrets to Glitchtip.json
|
- name: 🤫 Output Glitchtip Secrets to Glitchtip.json
|
||||||
run: |
|
run: |
|
||||||
echo "{" > glitchtip.json
|
echo "{" > glitchtip.json
|
||||||
echo "\"dsn\": \"${{ secrets.GLITCHTIP_DSN }}\"" >> glitchtip.json
|
echo "\"dsn\": \"${{ secrets.GLITCHTIP_DSN }}\"" >> glitchtip.json
|
||||||
echo "}" >> glitchtip.json
|
echo "}" >> glitchtip.json
|
||||||
|
|
||||||
|
- name: 📝 Output Glitchip secrets to .env
|
||||||
|
run: |
|
||||||
|
echo "GLITCHTIP_DSN=${{ secrets.GLITCHTIP_DSN }}" >> .env
|
||||||
|
|
||||||
- name: ✅ Validate Config Files
|
- name: ✅ Validate Config Files
|
||||||
run: |
|
run: |
|
||||||
node -e "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
bun -p "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
||||||
node -e "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
bun -p "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
||||||
|
|
||||||
- name: 🚀 Run Android fastlane deploy
|
- name: 🚀 Run Android fastlane deploy
|
||||||
run: yarn fastlane:android:deploy
|
run: bun run fastlane:android:deploy
|
||||||
env:
|
env:
|
||||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: 📤 Upload Android Artifacts
|
- name: 📤 Upload Android Artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -168,38 +176,38 @@ jobs:
|
|||||||
version: ${{ steps.setver.outputs.version }}
|
version: ${{ steps.setver.outputs.version }}
|
||||||
needs: [generate-release-notes]
|
needs: [generate-release-notes]
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: 🛒 Checkout
|
- name: 🛒 Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.SIGNING_REPO_PAT }}
|
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
|
|
||||||
- name: 🍎 Setup Xcode
|
- name: 🍎 Setup Xcode
|
||||||
uses: ./.github/actions/setup-xcode
|
uses: ./.github/actions/setup-xcode
|
||||||
|
|
||||||
- name: 🍎 Run yarn init-ios:new-arch
|
- name: 🍎 Run run init-ios:new-arch
|
||||||
run: yarn init-ios:new-arch
|
run: bun run init-ios:new-arch
|
||||||
|
|
||||||
- name: ➕ Version Up
|
- name: ➕ Version Up
|
||||||
if: ${{ github.event.inputs['version-bump'] != 'No Bump' }}
|
if: ${{ github.event.inputs['version-bump'] != 'No Bump' }}
|
||||||
run: yarn react-native bump-version --type ${{ github.event.inputs['version-bump'] }}
|
run: bun x react-native bump-version --type ${{ github.event.inputs['version-bump'] }} # Is this supposed to be like npx, or some special yarn thing?
|
||||||
|
|
||||||
- id: setver
|
- id: setver
|
||||||
run: echo "version=$(node -p -e "require('./package.json').version")" >> $GITHUB_OUTPUT
|
run: echo "version=$(bun -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: 💬 Echo package.json version to Github ENV
|
- name: 💬 Echo package.json version to Github ENV
|
||||||
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
|
run: echo VERSION_NUMBER=$(bun -p "require('./package.json').version") >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 🤫 Output App Store Connect API Key JSON to Fastlane
|
- name: 🤫 Output App Store Connect API Key JSON to Fastlane
|
||||||
run: echo -e '${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}' > appstore_connect_api_key.json
|
run: echo -e '${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}' > appstore_connect_api_key.json
|
||||||
working-directory: ./ios/fastlane
|
working-directory: ./ios/fastlane
|
||||||
|
|
||||||
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
|
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
|
||||||
run: |
|
run: |
|
||||||
echo "{" > telemetrydeck.json
|
echo "{" > telemetrydeck.json
|
||||||
echo "\"appID\": \"${{ secrets.TELEMETRYDECK_APPID }}\"," >> telemetrydeck.json
|
echo "\"appID\": \"${{ secrets.TELEMETRYDECK_APPID }}\"," >> telemetrydeck.json
|
||||||
@@ -207,20 +215,24 @@ jobs:
|
|||||||
echo "\"app\": \"Jellify\"" >> telemetrydeck.json
|
echo "\"app\": \"Jellify\"" >> telemetrydeck.json
|
||||||
echo "}" >> telemetrydeck.json
|
echo "}" >> telemetrydeck.json
|
||||||
|
|
||||||
|
|
||||||
- name: 🤫 Output Glitchtip Secrets to Glitchtip.json
|
- name: 🤫 Output Glitchtip Secrets to Glitchtip.json
|
||||||
run: |
|
run: |
|
||||||
echo "{" > glitchtip.json
|
echo "{" > glitchtip.json
|
||||||
echo "\"dsn\": \"${{ secrets.GLITCHTIP_DSN }}\"" >> glitchtip.json
|
echo "\"dsn\": \"${{ secrets.GLITCHTIP_DSN }}\"" >> glitchtip.json
|
||||||
echo "}" >> glitchtip.json
|
echo "}" >> glitchtip.json
|
||||||
|
|
||||||
|
- name: 📝 Output Glitchip secrets to .env
|
||||||
|
run: |
|
||||||
|
echo "GLITCHTIP_DSN=${{ secrets.GLITCHTIP_DSN }}" >> .env
|
||||||
|
|
||||||
- name: ✅ Validate Config Files
|
- name: ✅ Validate Config Files
|
||||||
run: |
|
run: |
|
||||||
node -e "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
bun -p "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
||||||
node -e "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
bun -p "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
||||||
|
|
||||||
- name: 🚀 Run iOS fastlane build and publish to TestFlight
|
- name: 🚀 Run iOS fastlane build and publish to TestFlight
|
||||||
run: yarn fastlane:ios:beta
|
run: bun run fastlane:ios:beta
|
||||||
env:
|
env:
|
||||||
APPSTORE_CONNECT_API_KEY_JSON: ${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}
|
APPSTORE_CONNECT_API_KEY_JSON: ${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}
|
||||||
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
|
||||||
@@ -241,7 +253,7 @@ jobs:
|
|||||||
- name: 🛒 Checkout Repo
|
- name: 🛒 Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.SIGNING_REPO_PAT }}
|
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
- name: ❌ Fail if selected job failed
|
- name: ❌ Fail if selected job failed
|
||||||
run: |
|
run: |
|
||||||
@@ -268,8 +280,13 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.2
|
||||||
|
|
||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
run: yarn install --network-concurrency 1
|
run: bun i
|
||||||
|
|
||||||
- name: ⬇️ Download Android Artifacts
|
- name: ⬇️ Download Android Artifacts
|
||||||
if: ${{ github.event.inputs['build-platform'] == 'Android' || github.event.inputs['build-platform'] == 'Both' }}
|
if: ${{ github.event.inputs['build-platform'] == 'Android' || github.event.inputs['build-platform'] == 'Both' }}
|
||||||
@@ -287,8 +304,8 @@ jobs:
|
|||||||
|
|
||||||
- name: ➕ Version Up
|
- name: ➕ Version Up
|
||||||
if: ${{ github.event.inputs['version-bump'] != 'No Bump' }}
|
if: ${{ github.event.inputs['version-bump'] != 'No Bump' }}
|
||||||
run: yarn react-native bump-version --type ${{ github.event.inputs['version-bump'] }}
|
run: bun x react-native bump-version --type ${{ github.event.inputs['version-bump'] }} # Is this supposed to be like npx, or some special yarn thing?
|
||||||
|
|
||||||
- name: 🔢 Set artifact version numbers
|
- name: 🔢 Set artifact version numbers
|
||||||
run: |
|
run: |
|
||||||
VERSION=${{ needs.publish-ios.outputs.version || needs.publish-android.outputs.version }}
|
VERSION=${{ needs.publish-ios.outputs.version || needs.publish-android.outputs.version }}
|
||||||
@@ -331,10 +348,10 @@ jobs:
|
|||||||
- name: 🗣️ Notify on Discord
|
- name: 🗣️ Notify on Discord
|
||||||
run: |
|
run: |
|
||||||
cd ios
|
cd ios
|
||||||
bundle install && bundle exec fastlane notifyOnDiscord
|
bundle install && bundle exec fastlane notifyOnDiscordForRelease
|
||||||
cd ..
|
cd ..
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
APP_VERSION: ${{ needs.publish-ios.outputs.version || needs.publish-android.outputs.version }}
|
APP_VERSION: ${{ needs.publish-ios.outputs.version || needs.publish-android.outputs.version }}
|
||||||
release_url: ${{ steps.githubRelease.outputs.html_url }}
|
release_url: ${{ steps.githubRelease.outputs.html_url }}
|
||||||
RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }}
|
RELEASE_NOTES: ${{ needs.generate-release-notes.outputs.release_notes }}
|
||||||
|
|||||||
35
.github/workflows/publish-ota-update-pr.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Publish Over-the-Air Update PR
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'App.tsx'
|
||||||
|
- '.github/workflows/publish-ota-update-pr.yml'
|
||||||
|
- 'scripts/ota-PR.sh'
|
||||||
|
- 'scripts/getRandomVersion.sh'
|
||||||
|
jobs:
|
||||||
|
publish-ota-update:
|
||||||
|
runs-on: macos-15
|
||||||
|
steps:
|
||||||
|
- name: 🛒 Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: 1.3.2
|
||||||
|
|
||||||
|
- name: 🥟 Run bun
|
||||||
|
run: bun i
|
||||||
|
|
||||||
|
- name: 👩💻 Configure Git
|
||||||
|
run: |
|
||||||
|
git config --global user.email "violet@cosmonautical.cloud"
|
||||||
|
git config --global user.name "anultravioletaurora"
|
||||||
|
|
||||||
|
- name: 🤖 Publish Android Update
|
||||||
|
run: bun run sendOTA:PR ${{ github.event.pull_request.number }}
|
||||||
|
env:
|
||||||
|
SIGNING_REPO_PAT: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
18
.github/workflows/publish-ota-update.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
name: Publish Over-the-Air Update
|
name: Publish Over-the-Air Update
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-ota-update:
|
publish-ota-update:
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
@@ -11,25 +11,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.SIGNING_REPO_PAT }}
|
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
|
|
||||||
|
- name: 🥟 Run bun install
|
||||||
|
run: bun i
|
||||||
|
|
||||||
- name: 🧵 Run yarn
|
|
||||||
run: yarn install --network-concurrency 1
|
|
||||||
|
|
||||||
- name: 👩💻 Configure Git
|
- name: 👩💻 Configure Git
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "violet@cosmonautical.cloud"
|
git config --global user.email "violet@cosmonautical.cloud"
|
||||||
git config --global user.name "anultravioletaurora"
|
git config --global user.name "anultravioletaurora"
|
||||||
|
|
||||||
- name: 🤖 Publish Android Update
|
- name: 🤖 Publish Android Update
|
||||||
run: yarn sendOTA:android
|
run: bun run sendOTA:android
|
||||||
env:
|
env:
|
||||||
SIGNING_REPO_PAT: ${{ secrets.SIGNING_REPO_PAT }}
|
SIGNING_REPO_PAT: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|
||||||
- name: 🍎 Publish iOS Update
|
- name: 🍎 Publish iOS Update
|
||||||
run: yarn sendOTA:iOS
|
run: bun run sendOTA:iOS
|
||||||
env:
|
env:
|
||||||
SIGNING_REPO_PAT: ${{ secrets.SIGNING_REPO_PAT }}
|
SIGNING_REPO_PAT: ${{ secrets.SIGNING_REPO_PAT }}
|
||||||
|
|||||||
31
.github/workflows/run-jest-test-suite.yml
vendored
@@ -16,22 +16,33 @@ jobs:
|
|||||||
- name: 🛒 Checkout
|
- name: 🛒 Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 🖥 Setup Node 20
|
- name: 🖥 Setup Bun 1.3.2
|
||||||
uses: actions/setup-node@v4
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
bun-version: 1.3.2
|
||||||
|
|
||||||
|
- name: 📦 Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
.jest-cache
|
||||||
|
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
- name: 💬 Echo package.json version to Github ENV
|
- name: 💬 Echo package.json version to Github ENV
|
||||||
run: echo VERSION_NUMBER=$(node -p -e "require('./package.json').version") >> $GITHUB_ENV
|
run: echo VERSION_NUMBER=$(bun -p "require('./package.json').version") >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: 🤖 Run yarn init-android
|
- name: 🤖 Run yarn init-android
|
||||||
run: yarn install --network-concurrency 1
|
run: bun i
|
||||||
|
|
||||||
- name: 🔍 Run yarn tsc
|
- name: 🔍 Run yarn tsc
|
||||||
run: yarn tsc
|
run: bun tsc
|
||||||
|
|
||||||
- name: 🧪 Run yarn test
|
- name: 🧪 Run yarn test
|
||||||
run: yarn test
|
run: CI=true bun run test
|
||||||
|
|
||||||
- name: 🦋 Check Styling
|
- name: 🦋 Check Styling
|
||||||
run: yarn format:check
|
run: bun run format:check
|
||||||
|
|||||||
21
.gitignore
vendored
@@ -37,8 +37,10 @@ local.properties
|
|||||||
# node.js
|
# node.js
|
||||||
#
|
#
|
||||||
node_modules/
|
node_modules/
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
# Don't think these will exist anymore!
|
||||||
|
# npm-debug.log
|
||||||
|
# yarn-error.log
|
||||||
|
|
||||||
# fastlane
|
# fastlane
|
||||||
#
|
#
|
||||||
@@ -64,14 +66,15 @@ yarn-error.log
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
.jest-cache
|
||||||
|
|
||||||
# Yarn
|
# Yarn
|
||||||
.yarn/*
|
#.yarn/*
|
||||||
!.yarn/patches
|
#!.yarn/patches
|
||||||
!.yarn/plugins
|
#!.yarn/plugins
|
||||||
!.yarn/releases
|
#!.yarn/releases
|
||||||
!.yarn/sdks
|
#!.yarn/sdks
|
||||||
!.yarn/versions
|
#!.yarn/versions
|
||||||
|
|
||||||
# Expo
|
# Expo
|
||||||
.expo
|
.expo
|
||||||
@@ -80,4 +83,4 @@ web-build/
|
|||||||
|
|
||||||
# Maestro Output
|
# Maestro Output
|
||||||
video.mp4
|
video.mp4
|
||||||
.github/copilot-instructions.md
|
.github/copilot-instructions.md
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
yarn lint-staged
|
bun lint-staged
|
||||||
|
|||||||
86
App.tsx
@@ -1,72 +1,80 @@
|
|||||||
import './gesture-handler'
|
import './gesture-handler'
|
||||||
import React, { useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import 'react-native-url-polyfill/auto'
|
import 'react-native-url-polyfill/auto'
|
||||||
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
|
||||||
import Jellify from './src/components/jellify'
|
import Jellify from './src/components/jellify'
|
||||||
import { TamaguiProvider } from 'tamagui'
|
import { TamaguiProvider } from 'tamagui'
|
||||||
import { Platform, useColorScheme } from 'react-native'
|
import { LogBox, Platform, useColorScheme } from 'react-native'
|
||||||
import jellifyConfig from './tamagui.config'
|
import jellifyConfig from './tamagui.config'
|
||||||
import { queryClientPersister } from './src/constants/storage'
|
import { queryClientPersister } from './src/constants/storage'
|
||||||
import { ONE_DAY, queryClient } from './src/constants/query-client'
|
import { ONE_DAY, queryClient } from './src/constants/query-client'
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||||
import TrackPlayer, {
|
import TrackPlayer, {
|
||||||
AndroidAudioContentType,
|
AndroidAudioContentType,
|
||||||
|
AppKilledPlaybackBehavior,
|
||||||
IOSCategory,
|
IOSCategory,
|
||||||
IOSCategoryOptions,
|
IOSCategoryOptions,
|
||||||
} from 'react-native-track-player'
|
} from 'react-native-track-player'
|
||||||
import { CAPABILITIES } from './src/player/constants'
|
import { CAPABILITIES } from './src/player/constants'
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
import { NavigationContainer } from '@react-navigation/native'
|
import { NavigationContainer } from '@react-navigation/native'
|
||||||
import { JellifyDarkTheme, JellifyLightTheme } from './src/components/theme'
|
import { JellifyDarkTheme, JellifyLightTheme, JellifyOLEDTheme } from './src/components/theme'
|
||||||
import { requestStoragePermission } from './src/utils/permisson-helpers'
|
import { requestStoragePermission } from './src/utils/permisson-helpers'
|
||||||
import ErrorBoundary from './src/components/ErrorBoundary'
|
import ErrorBoundary from './src/components/ErrorBoundary'
|
||||||
import OTAUpdateScreen from './src/components/OtaUpdates'
|
import OTAUpdateScreen from './src/components/OtaUpdates'
|
||||||
import { usePerformanceMonitor } from './src/hooks/use-performance-monitor'
|
import { usePerformanceMonitor } from './src/hooks/use-performance-monitor'
|
||||||
import navigationRef from './navigation'
|
import navigationRef from './navigation'
|
||||||
import { PROGRESS_UPDATE_EVENT_INTERVAL } from './src/player/config'
|
import { BUFFERS, PROGRESS_UPDATE_EVENT_INTERVAL } from './src/player/config'
|
||||||
import { useThemeSetting } from './src/stores/settings/app'
|
import { useThemeSetting } from './src/stores/settings/app'
|
||||||
|
|
||||||
|
LogBox.ignoreAllLogs()
|
||||||
|
|
||||||
export default function App(): React.JSX.Element {
|
export default function App(): React.JSX.Element {
|
||||||
// Add performance monitoring to track app-level re-renders
|
// Add performance monitoring to track app-level re-renders
|
||||||
const performanceMetrics = usePerformanceMonitor('App', 3)
|
const performanceMetrics = usePerformanceMonitor('App', 3)
|
||||||
|
|
||||||
const [playerIsReady, setPlayerIsReady] = useState<boolean>(false)
|
const [playerIsReady, setPlayerIsReady] = useState<boolean>(false)
|
||||||
|
const playerInitializedRef = useRef<boolean>(false)
|
||||||
|
|
||||||
/**
|
useEffect(() => {
|
||||||
* Enhanced Android buffer settings for gapless playback
|
// Guard against double initialization (React StrictMode, hot reload)
|
||||||
*
|
if (playerInitializedRef.current) return
|
||||||
* @see
|
playerInitializedRef.current = true
|
||||||
*/
|
|
||||||
const buffers =
|
|
||||||
Platform.OS === 'android'
|
|
||||||
? {
|
|
||||||
maxCacheSize: 50 * 1024, // 50MB cache
|
|
||||||
maxBuffer: 30, // 30 seconds buffer
|
|
||||||
playBuffer: 2.5, // 2.5 seconds play buffer
|
|
||||||
backBuffer: 5, // 5 seconds back buffer
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
|
|
||||||
TrackPlayer.setupPlayer({
|
TrackPlayer.setupPlayer({
|
||||||
autoHandleInterruptions: true,
|
autoHandleInterruptions: true,
|
||||||
iosCategory: IOSCategory.Playback,
|
iosCategory: IOSCategory.Playback,
|
||||||
iosCategoryOptions: [IOSCategoryOptions.AllowAirPlay, IOSCategoryOptions.AllowBluetooth],
|
iosCategoryOptions: [
|
||||||
androidAudioContentType: AndroidAudioContentType.Music,
|
IOSCategoryOptions.AllowAirPlay,
|
||||||
minBuffer: 30, // 30 seconds minimum buffer
|
IOSCategoryOptions.AllowBluetooth,
|
||||||
...buffers,
|
],
|
||||||
})
|
androidAudioContentType: AndroidAudioContentType.Music,
|
||||||
.then(() =>
|
minBuffer: 30, // 30 seconds minimum buffer
|
||||||
TrackPlayer.updateOptions({
|
...BUFFERS,
|
||||||
capabilities: CAPABILITIES,
|
|
||||||
notificationCapabilities: CAPABILITIES,
|
|
||||||
// Reduced interval for smoother progress tracking and earlier prefetch detection
|
|
||||||
progressUpdateEventInterval: PROGRESS_UPDATE_EVENT_INTERVAL,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
setPlayerIsReady(true)
|
|
||||||
requestStoragePermission()
|
|
||||||
})
|
})
|
||||||
|
.then(() =>
|
||||||
|
TrackPlayer.updateOptions({
|
||||||
|
capabilities: CAPABILITIES,
|
||||||
|
notificationCapabilities: CAPABILITIES,
|
||||||
|
// Reduced interval for smoother progress tracking and earlier prefetch detection
|
||||||
|
progressUpdateEventInterval: PROGRESS_UPDATE_EVENT_INTERVAL,
|
||||||
|
// Stop playback and remove notification when app is killed to prevent battery drain
|
||||||
|
android: {
|
||||||
|
appKilledPlaybackBehavior:
|
||||||
|
AppKilledPlaybackBehavior.StopPlaybackAndRemoveNotification,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
// Player may already be initialized (e.g., after hot reload)
|
||||||
|
// This is expected and not a fatal error
|
||||||
|
console.log('[TrackPlayer] Setup caught:', error?.message ?? error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setPlayerIsReady(true)
|
||||||
|
requestStoragePermission()
|
||||||
|
})
|
||||||
|
}, []) // Empty deps - only run once on mount
|
||||||
|
|
||||||
const [reloader, setReloader] = useState(0)
|
const [reloader, setReloader] = useState(0)
|
||||||
|
|
||||||
@@ -111,7 +119,9 @@ function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Ele
|
|||||||
: JellifyLightTheme
|
: JellifyLightTheme
|
||||||
: theme === 'dark'
|
: theme === 'dark'
|
||||||
? JellifyDarkTheme
|
? JellifyDarkTheme
|
||||||
: JellifyLightTheme
|
: theme === 'oled'
|
||||||
|
? JellifyOLEDTheme
|
||||||
|
: JellifyLightTheme
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<GestureHandlerRootView>
|
<GestureHandlerRootView>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 👩💻 Contributing
|
# Contributing
|
||||||
|
|
||||||
We are open to any developer that wants to lend their hand at _Jellify_ development, and developers can join our [Discord server](https://discord.gg/jellify) to get in contact with us.
|
We are open to any developer that wants to lend their hand at _Jellify_ development, and developers can join our [Discord server](https://discord.gg/jellify) to get in contact with us.
|
||||||
|
|
||||||
@@ -12,13 +12,13 @@ Here's the best way to get started:
|
|||||||
- Submit a Pull Request to sync the main repository with your fork
|
- Submit a Pull Request to sync the main repository with your fork
|
||||||
- Profit! 🎉
|
- Profit! 🎉
|
||||||
|
|
||||||
## 🏃♀️Running Locally
|
## Running Locally
|
||||||
|
|
||||||
### ⚛️ Universal Dependencies
|
### Universal Dependencies
|
||||||
|
|
||||||
- [Ruby](https://www.ruby-lang.org/en/documentation/installation/) for Fastlane
|
- [Ruby](https://www.ruby-lang.org/en/documentation/installation/) for Fastlane
|
||||||
- [NodeJS v22](https://nodejs.org/en/download) for React Native
|
- [NodeJS v22](https://nodejs.org/en/download) for React Native
|
||||||
- [Maestro](https://docs.maestro.dev/getting-started/installing-maestro) for running E2E tests
|
- [Bun](https://bun.sh/) for managing dependencies
|
||||||
|
|
||||||
### 🍎 iOS
|
### 🍎 iOS
|
||||||
|
|
||||||
@@ -31,21 +31,21 @@ Here's the best way to get started:
|
|||||||
##### Setup
|
##### Setup
|
||||||
|
|
||||||
- Clone this repository
|
- Clone this repository
|
||||||
- Run `yarn init-ios:new-arch` to initialize the project
|
- Run `bun init-ios:new-arch` to initialize the project
|
||||||
- This will install `npm` packages, install `bundler` and required gems, and install required CocoaPods with [React Native's New Architecture](https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here#what-is-the-new-architecture)
|
- This will install `npm` packages, install `bundler` and required gems, and install required CocoaPods with [React Native's New Architecture](https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here#what-is-the-new-architecture)
|
||||||
- In the `ios` directory, run `fastlane match development --readonly` to fetch the development signing certificates
|
- In the `ios` directory, run `fastlane match development --readonly` to fetch the development signing certificates
|
||||||
- _You will need access to the "Jellify Signing" private repository_
|
- _You will need access to the "Jellify Signing" private repository_
|
||||||
|
|
||||||
##### Running
|
##### Running
|
||||||
|
|
||||||
- Run `yarn start` to start the dev server
|
- Run `bun 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
|
- 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
|
##### Building
|
||||||
|
|
||||||
- To create a build, run `yarn fastlane:ios:build` to use fastlane to compile an `.ipa`
|
- To create a build, run `bun fastlane:ios:build` to use fastlane to compile an `.ipa`
|
||||||
|
|
||||||
### 🤖 Android
|
### 🤖 Android
|
||||||
|
|
||||||
@@ -59,21 +59,22 @@ Here's the best way to get started:
|
|||||||
##### Setup
|
##### Setup
|
||||||
|
|
||||||
- Clone this repository
|
- Clone this repository
|
||||||
- Run `yarn install` to install `npm` packages
|
- Run `bun install` to install `npm` packages
|
||||||
|
|
||||||
##### Running
|
##### Running
|
||||||
|
|
||||||
- Run `yarn start` to start the dev server
|
- Run `bun start` to start the dev server
|
||||||
- Open the `android` folder with Android Studio
|
- 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
|
- Run either on a device or in the simulator
|
||||||
|
|
||||||
##### Building
|
##### Building
|
||||||
|
|
||||||
- To create a build, run `yarn fastlane:android:build` to use fastlane to compile an `.apk` for all architectures
|
- To create a build, run `bun fastlane:android:build` to use fastlane to compile an `.apk` for all architectures
|
||||||
|
- Alternatively, run `cd android; ./gradlew assembleRelease` to use Gradle to compile an `.apk`
|
||||||
|
|
||||||
#### References
|
#### References
|
||||||
|
|
||||||
- [Setting up Android SDK](https://developer.android.com/about/versions/14/setup-sdk)
|
- [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)
|
- [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)
|
||||||
- [Android Auto app not showing up](https://www.reddit.com/r/AndroidAuto/s/LGYHoSPdXm)
|
- [Android Auto app not showing up](https://www.reddit.com/r/AndroidAuto/s/LGYHoSPdXm)
|
||||||
|
|||||||
71
README.md
@@ -2,8 +2,8 @@
|
|||||||
<img alt='Jellify logo' src='assets/transparent-banner.png' width="600" height="300" />
|
<img alt='Jellify logo' src='assets/transparent-banner.png' width="600" height="300" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/anultravioletaurora/Jellify/releases)
|
[](https://github.com/anultravioletaurora/Jellify/releases) [](https://apps.apple.com/us/app/jellify/id6736884612) [](https://play.google.com/store/apps/details?id=com.cosmonautical.jellify)
|
||||||
[](https://github.com/anultravioletaurora/Jellify/actions/workflows/publish-beta.yml) [](https://github.com/Jellify-Music/App/actions/workflows/publish-ota-update.yml)
|
|
||||||
|
|
||||||
[](https://github.com/sponsors/anultravioletaurora) [](https://patreon.com/anultravioletaurora?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink)
|
[](https://github.com/sponsors/anultravioletaurora) [](https://patreon.com/anultravioletaurora?utm_medium=unknown&utm_source=join_link&utm_campaign=creatorshare_creator&utm_content=copyLink)
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
- [Info](#info)
|
- [Info](#info)
|
||||||
- [Downloading](#downloading)
|
- [Downloading](#downloading)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [Features](#features)
|
- [Features and Roadmap](#features)
|
||||||
- [Built with](#built-with-good-stuff)
|
- [Built with](#built-with-good-stuff)
|
||||||
- [Support](#support-the-project)
|
- [Support](#support-the-project)
|
||||||
- [Special Thanks](#special-thanks)
|
- [Special Thanks](#special-thanks)
|
||||||
@@ -65,6 +65,10 @@ These projects are **not** required to use _Jellify_, but are recommended by us
|
|||||||
|
|
||||||
### Android
|
### Android
|
||||||
|
|
||||||
|
[](https://play.google.com/store/apps/details?id=com.cosmonautical.jellify)
|
||||||
|
|
||||||
|
#### Direct .APK Download
|
||||||
|
|
||||||
Head to [releases](https://github.com/Jellify-Music/App/releases) to download the required .APK directly.
|
Head to [releases](https://github.com/Jellify-Music/App/releases) to download the required .APK directly.
|
||||||
|
|
||||||
Also there is [obtanium](https://github.com/ImranR98/Obtainium) to which you can add Jellify as a repo to use the above releases as a repository.
|
Also there is [obtanium](https://github.com/ImranR98/Obtainium) to which you can add Jellify as a repo to use the above releases as a repository.
|
||||||
@@ -73,6 +77,8 @@ For Obtanium, click "Add App", put "https://github.com/Jellify-Music/App" as the
|
|||||||
|
|
||||||
### iOS
|
### iOS
|
||||||
|
|
||||||
|
[](https://apps.apple.com/us/app/jellify/id6736884612)
|
||||||
|
|
||||||
#### The TestFlight Way
|
#### The TestFlight Way
|
||||||
|
|
||||||
Join the [TestFlight](https://testflight.apple.com/join/etVSc7ZQ) and install the latest version from there
|
Join the [TestFlight](https://testflight.apple.com/join/etVSc7ZQ) and install the latest version from there
|
||||||
@@ -103,13 +109,9 @@ Install via [Altstore](https://altstore.io) or your favorite sideloading utility
|
|||||||
**Artists**
|
**Artists**
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="screenshots/library_artists.png" alt="Library Artists" width="275" height="600">
|
<img src="screenshots/library_artists.png" alt="Library Artists" width="275" height="600" />
|
||||||
</p>
|
<img src="screenshots/library_albums.PNG" alt="Library Albums" width="275" height="600" />
|
||||||
|
<img src="screenshots/library_downloaded_tracks.PNG" alt="Library Tracks" width="275" height="600" />
|
||||||
**Downloaded Tracks**
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="screenshots/library_downloaded_tracks.PNG" alt="Library Tracks" width="275" height="600">
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Artist View**
|
**Artist View**
|
||||||
@@ -171,7 +173,7 @@ Install via [Altstore](https://altstore.io) or your favorite sideloading utility
|
|||||||
|
|
||||||
### Current
|
### Current
|
||||||
|
|
||||||
- Available via Testflight and Android APK
|
- Available via [Play Store](https://play.google.com/store/apps/details?id=com.cosmonautical.jellify&pcampaignid=web_share), [App Store](https://apps.apple.com/us/app/jellify/id6736884612), [Testflight](https://testflight.apple.com/join/etVSc7ZQ), and Android APKs
|
||||||
- APKs are associated with each [release](https://github.com/anultravioletaurora/Jellify/releases)
|
- APKs are associated with each [release](https://github.com/anultravioletaurora/Jellify/releases)
|
||||||
- Light and Dark modes
|
- Light and Dark modes
|
||||||
- Home screen access to previously played tracks, artists, and your playlists
|
- Home screen access to previously played tracks, artists, and your playlists
|
||||||
@@ -182,21 +184,44 @@ Install via [Altstore](https://altstore.io) or your favorite sideloading utility
|
|||||||
- Offline Playback
|
- Offline Playback
|
||||||
- Support for Jellyfin Instant Mixes
|
- Support for Jellyfin Instant Mixes
|
||||||
- Over-the-Air Updates
|
- Over-the-Air Updates
|
||||||
- Powered by [react-native-ota-hot-update](https://github.com/vantuan88291/react-native-ota-hot-update), incremental app updates are automatically fetched and applied from our [App Bundles Repository](https://github.com/Jellify-Music/App-Bundles)
|
- Powered by [react-native-nitro-ota](https://github.com/riteshshukla04/react-native-nitro-ota), incremental app updates are automatically fetched and applied from our [App Bundles Repository](https://github.com/Jellify-Music/App-Bundles)
|
||||||
- Shuffling
|
- Shuffling
|
||||||
- Switching Music Libraries
|
- Switching Music Libraries
|
||||||
- Google Cast Support
|
- Google Cast Support (still in early stages)
|
||||||
|
- Storage UI Manager
|
||||||
|
|
||||||
### Roadmap (in order of priority)
|
### Roadmap
|
||||||
|
|
||||||
|
#### 1.1.0 (Socket To Me Baby) - March '26
|
||||||
|
- Android Auto/CarPlay Support
|
||||||
|
- Websocket Support (Server online status)
|
||||||
|
- Home Screen Updates
|
||||||
|
- Discover Screen Updates
|
||||||
|
- Artist Screen Redesign
|
||||||
|
- Library Redesign
|
||||||
|
- Quick Connect Support
|
||||||
|
- Allow Self-Signed Certificates
|
||||||
|
|
||||||
|
#### 1.2.0 (We Made a Language For Us Two...) - June '26
|
||||||
|
- Collaborative Playlists
|
||||||
|
- App Customization Options
|
||||||
|
- Desktop Support (Experimental)
|
||||||
|
|
||||||
|
#### 1.3.0 (Playin' All Day) - September '26
|
||||||
|
- Autoplay Integration
|
||||||
|
- Tablet Support
|
||||||
|
|
||||||
|
#### 2.0.0 - December '26
|
||||||
|
- Gapless Playback
|
||||||
|
- Seerr (formerly Jellyseerr) Integration
|
||||||
|
- JellyJam
|
||||||
|
- EQ Controls
|
||||||
|
|
||||||
|
#### 3.0.0 - TBD
|
||||||
|
- Watch Support
|
||||||
|
|
||||||
|
\*This is subject to change
|
||||||
|
|
||||||
- ["Smart Shuffle"](https://github.com/anultravioletaurora/Jellify/issues/57)
|
|
||||||
- [CarPlay / Android Auto Support](https://github.com/anultravioletaurora/Jellify/issues/5)
|
|
||||||
- [App Store / Google Play / FDroid Release](https://github.com/anultravioletaurora/Jellify/issues/361)
|
|
||||||
- [Translations](https://github.com/anultravioletaurora/Jellify/issues/317)
|
|
||||||
- [Web / Desktop support](https://github.com/anultravioletaurora/Jellify/issues/71)
|
|
||||||
- [Shared, Public, and Collaborative Playlists](https://github.com/anultravioletaurora/Jellify/issues/175)
|
|
||||||
- [Watch (Apple Watch / WearOS) Support](https://github.com/anultravioletaurora/Jellify/issues/61)
|
|
||||||
- [TV (Android, Apple, Samsung) Support](https://github.com/anultravioletaurora/Jellify/issues/85)
|
|
||||||
|
|
||||||
## Built with Good Stuff
|
## Built with Good Stuff
|
||||||
|
|
||||||
@@ -271,8 +296,6 @@ Paid supporters will be recognized by having their name displayed within the Set
|
|||||||
- Quality Selection
|
- Quality Selection
|
||||||
- Many thanks to PDB3D for the logo design!
|
- Many thanks to PDB3D for the logo design!
|
||||||
- Huge thank you to [Ritesh](https://github.com/riteshshukla04) for literally so many things:
|
- Huge thank you to [Ritesh](https://github.com/riteshshukla04) for literally so many things:
|
||||||
- Offline Mode and Network Detection
|
|
||||||
- Error Boundary Detection
|
|
||||||
- Over-the-Air Updates
|
- Over-the-Air Updates
|
||||||
- Cast Support
|
- Cast Support
|
||||||
- The friends we made along the way that have been critical in fostering an amazing community around _Jellify_
|
- The friends we made along the way that have been critical in fostering an amazing community around _Jellify_
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
|
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
|
||||||
|
<module name="Jellify.app" />
|
||||||
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
|
<option name="ANDROID_RUN_CONFIGURATION_SCHEMA_VERSION" value="1" />
|
||||||
<option name="DEPLOY" value="true" />
|
<option name="DEPLOY" value="true" />
|
||||||
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
|
||||||
|
|||||||
@@ -91,8 +91,10 @@ android {
|
|||||||
applicationId "com.cosmonautical.jellify"
|
applicationId "com.cosmonautical.jellify"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 133
|
versionCode 158
|
||||||
versionName "0.19.1"
|
versionName "1.0.0"
|
||||||
|
resValue "string", "build_config_package", "com.jellify"
|
||||||
|
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
@@ -146,4 +148,4 @@ dependencies {
|
|||||||
implementation "com.google.android.gms:play-services-cast-framework:+"
|
implementation "com.google.android.gms:play-services-cast-framework:+"
|
||||||
implementation("com.facebook.react:hermes-android")
|
implementation("com.facebook.react:hermes-android")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,7 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
|
||||||
|
<!-- Please Do not remove, this use for adaptive icon https://developer.android.com/develop/ui/views/launch/icon_design_adaptive -->
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -2,4 +2,7 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
|
||||||
|
<!-- Please Do not remove, this use for adaptive icon https://developer.android.com/develop/ui/views/launch/icon_design_adaptive -->
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
BIN
assets/banner.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
@@ -1,4 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:@react-native/babel-preset'],
|
presets: ['module:@react-native/babel-preset'],
|
||||||
plugins: ['react-native-worklets/plugin'],
|
plugins: [
|
||||||
|
'babel-plugin-react-compiler',
|
||||||
|
'react-native-worklets/plugin',
|
||||||
|
'react-native-worklets-core/plugin',
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,22 @@ module.exports = defineConfig([
|
|||||||
'@typescript-eslint/no-explicit-any': 'error',
|
'@typescript-eslint/no-explicit-any': 'error',
|
||||||
'no-mixed-spaces-and-tabs': 'off',
|
'no-mixed-spaces-and-tabs': 'off',
|
||||||
semi: ['error', 'never'],
|
semi: ['error', 'never'],
|
||||||
|
// Prevent importing RefreshControl from react-native-gesture-handler
|
||||||
|
// as it uses deprecated findNodeHandle which causes warnings in StrictMode.
|
||||||
|
// Use RefreshControl from 'react-native' instead.
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
name: 'react-native-gesture-handler',
|
||||||
|
importNames: ['RefreshControl'],
|
||||||
|
message:
|
||||||
|
"Import RefreshControl from 'react-native' instead. The gesture-handler version uses deprecated findNodeHandle which causes warnings in StrictMode.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import CarPlay
|
|||||||
class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
|
class CarSceneDelegate: UIResponder, CPTemplateApplicationSceneDelegate {
|
||||||
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) {
|
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didConnect interfaceController: CPInterfaceController) {
|
||||||
|
|
||||||
RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow);
|
RNCarPlay.connect(with: interfaceController, window: templateApplicationScene.carWindow)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
|
func templateApplicationScene(_ templateApplicationScene: CPTemplateApplicationScene, didDisconnectInterfaceController interfaceController: CPInterfaceController) {
|
||||||
|
|
||||||
RNCarPlay.disconnect()
|
RNCarPlay.disconnect()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Jellify/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Jellify/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
8798FC37A1454014A7B318F9 /* Figtree-SemiBold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Figtree-SemiBold.otf"; path = "../assets/fonts/Figtree-SemiBold.otf"; sourceTree = "<group>"; };
|
8798FC37A1454014A7B318F9 /* Figtree-SemiBold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Figtree-SemiBold.otf"; path = "../assets/fonts/Figtree-SemiBold.otf"; sourceTree = "<group>"; };
|
||||||
8B91428F7F524687A96EE362 /* Figtree-LightItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Figtree-LightItalic.otf"; path = "../assets/fonts/Figtree-LightItalic.otf"; sourceTree = "<group>"; };
|
8B91428F7F524687A96EE362 /* Figtree-LightItalic.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Figtree-LightItalic.otf"; path = "../assets/fonts/Figtree-LightItalic.otf"; sourceTree = "<group>"; };
|
||||||
|
940806CC81921C976BDC3779 /* Pods-Jellify.devrelease.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.devrelease.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.devrelease.xcconfig"; sourceTree = "<group>"; };
|
||||||
C5258FBB23272277847FE07E /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
C5258FBB23272277847FE07E /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
CF605E5D2DF95BAB00858968 /* Figtree-Black.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Figtree-Black.otf"; sourceTree = "<group>"; };
|
CF605E5D2DF95BAB00858968 /* Figtree-Black.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Figtree-Black.otf"; sourceTree = "<group>"; };
|
||||||
CF605E5E2DF95BAB00858968 /* Figtree-BlackItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Figtree-BlackItalic.otf"; sourceTree = "<group>"; };
|
CF605E5E2DF95BAB00858968 /* Figtree-BlackItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Figtree-BlackItalic.otf"; sourceTree = "<group>"; };
|
||||||
@@ -219,6 +220,7 @@
|
|||||||
children = (
|
children = (
|
||||||
1EFD74F540EE131CCCC762FE /* Pods-Jellify.debug.xcconfig */,
|
1EFD74F540EE131CCCC762FE /* Pods-Jellify.debug.xcconfig */,
|
||||||
E53A46F6214019C12F016ACB /* Pods-Jellify.release.xcconfig */,
|
E53A46F6214019C12F016ACB /* Pods-Jellify.release.xcconfig */,
|
||||||
|
940806CC81921C976BDC3779 /* Pods-Jellify.devrelease.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -306,7 +308,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastUpgradeCheck = 2600;
|
LastUpgradeCheck = 2610;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
00E356ED1AD99517003FC87E = {
|
00E356ED1AD99517003FC87E = {
|
||||||
CreatedOnToolsVersion = 6.2;
|
CreatedOnToolsVersion = 6.2;
|
||||||
@@ -547,7 +549,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 245;
|
CURRENT_PROJECT_VERSION = 267;
|
||||||
DEVELOPMENT_TEAM = "";
|
DEVELOPMENT_TEAM = "";
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -558,7 +560,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.19.1;
|
MARKETING_VERSION = 1.0.0;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -589,7 +591,7 @@
|
|||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
CODE_SIGN_STYLE = Manual;
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 245;
|
CURRENT_PROJECT_VERSION = 267;
|
||||||
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -599,7 +601,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.19.1;
|
MARKETING_VERSION = 1.0.0;
|
||||||
NEW_SETTING = "";
|
NEW_SETTING = "";
|
||||||
OTHER_LDFLAGS = (
|
OTHER_LDFLAGS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
@@ -618,6 +620,13 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
47C4374A2EBD5610003A655B /* DevRelease */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
PRODUCT_NAME = JellifyTests;
|
||||||
|
};
|
||||||
|
name = DevRelease;
|
||||||
|
};
|
||||||
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
83CBBA201A601CBA00E9B192 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -801,6 +810,138 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
CFDEVREL001 /* DevRelease */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 940806CC81921C976BDC3779 /* Pods-Jellify.devrelease.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Jellify/Jellify.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
CURRENT_PROJECT_VERSION = 267;
|
||||||
|
DEVELOPMENT_TEAM = WAH9CZ8BPG;
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = WAH9CZ8BPG;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
ENVFILE = .env.devrelease;
|
||||||
|
INFOPLIST_FILE = Jellify/Info.plist;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0.0;
|
||||||
|
NEW_SETTING = "";
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"-ObjC",
|
||||||
|
"-lc++",
|
||||||
|
);
|
||||||
|
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE -D DEV_RELEASE";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.cosmonautical.jellify;
|
||||||
|
PRODUCT_NAME = Jellify;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "match Development com.cosmonautical.jellify";
|
||||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Jellify-Bridging-Header.h";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
};
|
||||||
|
name = DevRelease;
|
||||||
|
};
|
||||||
|
CFDEVRELPROJ001 /* DevRelease */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CC = "";
|
||||||
|
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = YES;
|
||||||
|
CXX = "";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
|
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
HEADER_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers",
|
||||||
|
"${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios",
|
||||||
|
);
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||||
|
LD = "";
|
||||||
|
LDPLUSPLUS = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
/usr/lib/swift,
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
LIBRARY_SEARCH_PATHS = (
|
||||||
|
"\"$(SDKROOT)/usr/lib/swift\"",
|
||||||
|
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||||
|
"\"$(inherited)\"",
|
||||||
|
);
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
OTHER_CPLUSPLUSFLAGS = (
|
||||||
|
"$(OTHER_CFLAGS)",
|
||||||
|
"-DFOLLY_NO_CONFIG",
|
||||||
|
"-DFOLLY_MOBILE=1",
|
||||||
|
"-DFOLLY_USE_LIBCPP=1",
|
||||||
|
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||||
|
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||||
|
);
|
||||||
|
OTHER_LDFLAGS = (
|
||||||
|
"$(inherited)",
|
||||||
|
" ",
|
||||||
|
);
|
||||||
|
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
USE_HERMES = true;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = DevRelease;
|
||||||
|
};
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -809,6 +950,7 @@
|
|||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
00E356F61AD99517003FC87E /* Debug */,
|
00E356F61AD99517003FC87E /* Debug */,
|
||||||
00E356F71AD99517003FC87E /* Release */,
|
00E356F71AD99517003FC87E /* Release */,
|
||||||
|
47C4374A2EBD5610003A655B /* DevRelease */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
@@ -818,6 +960,7 @@
|
|||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
13B07F941A680F5B00A75B9A /* Debug */,
|
13B07F941A680F5B00A75B9A /* Debug */,
|
||||||
13B07F951A680F5B00A75B9A /* Release */,
|
13B07F951A680F5B00A75B9A /* Release */,
|
||||||
|
CFDEVREL001 /* DevRelease */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
@@ -827,6 +970,7 @@
|
|||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||||
83CBBA211A601CBA00E9B192 /* Release */,
|
83CBBA211A601CBA00E9B192 /* Release */,
|
||||||
|
CFDEVRELPROJ001 /* DevRelease */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2600"
|
LastUpgradeVersion = "2610"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "2600"
|
LastUpgradeVersion = "2610"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -58,6 +58,13 @@
|
|||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>100.64.0.0/10</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSBonjourServices</key>
|
<key>NSBonjourServices</key>
|
||||||
@@ -66,7 +73,7 @@
|
|||||||
<string>_CC1AD845._googlecast._tcp</string>
|
<string>_CC1AD845._googlecast._tcp</string>
|
||||||
</array>
|
</array>
|
||||||
<key>NSLocalNetworkUsageDescription</key>
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
<string>${PRODUCT_NAME} uses the local network to connect to one's Jellyfin server for streaming music</string>
|
<string>${PRODUCT_NAME} uses the local network to connect to one's Jellyfin server for streaming music</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>RCTNewArchEnabled</key>
|
<key>RCTNewArchEnabled</key>
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
<array>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>C617.1</string>
|
||||||
|
<string>0A2A.1</string>
|
||||||
|
<string>3B52.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
@@ -21,16 +31,6 @@
|
|||||||
<string>C56D.1</string>
|
<string>C56D.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
|
||||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
|
||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
||||||
<array>
|
|
||||||
<string>C617.1</string>
|
|
||||||
<string>0A2A.1</string>
|
|
||||||
<string>3B52.1</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||||
|
|||||||
172
ios/Podfile.lock
@@ -12,7 +12,7 @@ PODS:
|
|||||||
- hermes-engine (0.82.1):
|
- hermes-engine (0.82.1):
|
||||||
- hermes-engine/Pre-built (= 0.82.1)
|
- hermes-engine/Pre-built (= 0.82.1)
|
||||||
- hermes-engine/Pre-built (0.82.1)
|
- hermes-engine/Pre-built (0.82.1)
|
||||||
- NitroImage (0.8.1):
|
- NitroFetch (0.1.6):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -42,7 +42,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- NitroModules (0.31.1):
|
- NitroModules (0.31.10):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -71,7 +71,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- NitroOta (0.3.0):
|
- NitroOta (0.7.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -102,7 +102,7 @@ PODS:
|
|||||||
- SocketRocket
|
- SocketRocket
|
||||||
- SSZipArchive
|
- SSZipArchive
|
||||||
- Yoga
|
- Yoga
|
||||||
- NitroOtaBundleManager (0.3.0):
|
- NitroOtaBundleManager (0.7.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -130,38 +130,6 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- NitroWebImage (0.8.1):
|
|
||||||
- boost
|
|
||||||
- DoubleConversion
|
|
||||||
- fast_float
|
|
||||||
- fmt
|
|
||||||
- glog
|
|
||||||
- hermes-engine
|
|
||||||
- NitroImage
|
|
||||||
- NitroModules
|
|
||||||
- RCT-Folly
|
|
||||||
- RCT-Folly/Fabric
|
|
||||||
- RCTRequired
|
|
||||||
- RCTTypeSafety
|
|
||||||
- React-callinvoker
|
|
||||||
- React-Core
|
|
||||||
- React-debug
|
|
||||||
- React-Fabric
|
|
||||||
- React-featureflags
|
|
||||||
- React-graphics
|
|
||||||
- React-ImageManager
|
|
||||||
- React-jsi
|
|
||||||
- React-NativeModulesApple
|
|
||||||
- React-RCTFabric
|
|
||||||
- React-renderercss
|
|
||||||
- React-rendererdebug
|
|
||||||
- React-utils
|
|
||||||
- ReactCodegen
|
|
||||||
- ReactCommon/turbomodule/bridging
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- SDWebImage
|
|
||||||
- SocketRocket
|
|
||||||
- Yoga
|
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- Protobuf (3.29.5)
|
- Protobuf (3.29.5)
|
||||||
- RCT-Folly (2024.11.18.00):
|
- RCT-Folly (2024.11.18.00):
|
||||||
@@ -2036,7 +2004,7 @@ PODS:
|
|||||||
- Yoga
|
- Yoga
|
||||||
- react-native-netinfo (11.4.1):
|
- react-native-netinfo (11.4.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-pager-view (6.9.1):
|
- react-native-pager-view (7.0.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2064,7 +2032,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-safe-area-context (5.6.1):
|
- react-native-safe-area-context (5.6.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2082,8 +2050,8 @@ PODS:
|
|||||||
- React-graphics
|
- React-graphics
|
||||||
- React-ImageManager
|
- React-ImageManager
|
||||||
- React-jsi
|
- React-jsi
|
||||||
- react-native-safe-area-context/common (= 5.6.1)
|
- react-native-safe-area-context/common (= 5.6.2)
|
||||||
- react-native-safe-area-context/fabric (= 5.6.1)
|
- react-native-safe-area-context/fabric (= 5.6.2)
|
||||||
- React-NativeModulesApple
|
- React-NativeModulesApple
|
||||||
- React-RCTFabric
|
- React-RCTFabric
|
||||||
- React-renderercss
|
- React-renderercss
|
||||||
@@ -2094,7 +2062,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-safe-area-context/common (5.6.1):
|
- react-native-safe-area-context/common (5.6.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2122,7 +2090,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-safe-area-context/fabric (5.6.1):
|
- react-native-safe-area-context/fabric (5.6.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2180,7 +2148,35 @@ PODS:
|
|||||||
- SocketRocket
|
- SocketRocket
|
||||||
- SwiftAudioEx (= 1.1.0)
|
- SwiftAudioEx (= 1.1.0)
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-vector-icons-material-design-icons (12.3.0)
|
- react-native-vector-icons-material-design-icons (12.4.0)
|
||||||
|
- react-native-worklets-core (1.6.2):
|
||||||
|
- boost
|
||||||
|
- DoubleConversion
|
||||||
|
- fast_float
|
||||||
|
- fmt
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly
|
||||||
|
- RCT-Folly/Fabric
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- SocketRocket
|
||||||
|
- Yoga
|
||||||
- React-NativeModulesApple (0.82.1):
|
- React-NativeModulesApple (0.82.1):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -2720,6 +2716,34 @@ PODS:
|
|||||||
- React-perflogger (= 0.82.1)
|
- React-perflogger (= 0.82.1)
|
||||||
- React-utils (= 0.82.1)
|
- React-utils (= 0.82.1)
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
|
- RNCAsyncStorage (2.2.0):
|
||||||
|
- boost
|
||||||
|
- DoubleConversion
|
||||||
|
- fast_float
|
||||||
|
- fmt
|
||||||
|
- glog
|
||||||
|
- hermes-engine
|
||||||
|
- RCT-Folly
|
||||||
|
- RCT-Folly/Fabric
|
||||||
|
- RCTRequired
|
||||||
|
- RCTTypeSafety
|
||||||
|
- React-Core
|
||||||
|
- React-debug
|
||||||
|
- React-Fabric
|
||||||
|
- React-featureflags
|
||||||
|
- React-graphics
|
||||||
|
- React-ImageManager
|
||||||
|
- React-jsi
|
||||||
|
- React-NativeModulesApple
|
||||||
|
- React-RCTFabric
|
||||||
|
- React-renderercss
|
||||||
|
- React-rendererdebug
|
||||||
|
- React-utils
|
||||||
|
- ReactCodegen
|
||||||
|
- ReactCommon/turbomodule/bridging
|
||||||
|
- ReactCommon/turbomodule/core
|
||||||
|
- SocketRocket
|
||||||
|
- Yoga
|
||||||
- RNCMaskedView (0.3.2):
|
- RNCMaskedView (0.3.2):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
@@ -2748,13 +2772,13 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNDeviceInfo (14.0.4):
|
- RNDeviceInfo (15.0.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNDnsLookup (1.0.6):
|
- RNDnsLookup (1.0.6):
|
||||||
- React
|
- React
|
||||||
- RNFS (2.20.0):
|
- RNFS (2.20.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- RNGestureHandler (2.28.0):
|
- RNGestureHandler (2.29.1):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2810,7 +2834,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNReanimated (4.1.3):
|
- RNReanimated (4.1.5):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2837,11 +2861,11 @@ PODS:
|
|||||||
- ReactCodegen
|
- ReactCodegen
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- RNReanimated/reanimated (= 4.1.3)
|
- RNReanimated/reanimated (= 4.1.5)
|
||||||
- RNWorklets
|
- RNWorklets
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNReanimated/reanimated (4.1.3):
|
- RNReanimated/reanimated (4.1.5):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2868,11 +2892,11 @@ PODS:
|
|||||||
- ReactCodegen
|
- ReactCodegen
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- RNReanimated/reanimated/apple (= 4.1.3)
|
- RNReanimated/reanimated/apple (= 4.1.5)
|
||||||
- RNWorklets
|
- RNWorklets
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNReanimated/reanimated/apple (4.1.3):
|
- RNReanimated/reanimated/apple (4.1.5):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2961,7 +2985,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNSentry (7.1.0):
|
- RNSentry (7.6.0):
|
||||||
- boost
|
- boost
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- fast_float
|
- fast_float
|
||||||
@@ -2988,7 +3012,7 @@ PODS:
|
|||||||
- ReactCodegen
|
- ReactCodegen
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Sentry/HybridSDK (= 8.56.0)
|
- Sentry/HybridSDK (= 8.57.2)
|
||||||
- SocketRocket
|
- SocketRocket
|
||||||
- Yoga
|
- Yoga
|
||||||
- RNWorklets (0.6.1):
|
- RNWorklets (0.6.1):
|
||||||
@@ -3083,7 +3107,7 @@ PODS:
|
|||||||
- SDWebImage (5.21.2):
|
- SDWebImage (5.21.2):
|
||||||
- SDWebImage/Core (= 5.21.2)
|
- SDWebImage/Core (= 5.21.2)
|
||||||
- SDWebImage/Core (5.21.2)
|
- SDWebImage/Core (5.21.2)
|
||||||
- Sentry/HybridSDK (8.56.0)
|
- Sentry/HybridSDK (8.57.2)
|
||||||
- SocketRocket (0.7.1)
|
- SocketRocket (0.7.1)
|
||||||
- SSZipArchive (2.4.3)
|
- SSZipArchive (2.4.3)
|
||||||
- SwiftAudioEx (1.1.0)
|
- SwiftAudioEx (1.1.0)
|
||||||
@@ -3098,11 +3122,10 @@ DEPENDENCIES:
|
|||||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||||
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
|
||||||
- NitroImage (from `../node_modules/react-native-nitro-image`)
|
- NitroFetch (from `../node_modules/react-native-nitro-fetch`)
|
||||||
- NitroModules (from `../node_modules/react-native-nitro-modules`)
|
- NitroModules (from `../node_modules/react-native-nitro-modules`)
|
||||||
- NitroOta (from `../node_modules/react-native-nitro-ota`)
|
- NitroOta (from `../node_modules/react-native-nitro-ota`)
|
||||||
- NitroOtaBundleManager (from `../node_modules/react-native-nitro-ota`)
|
- NitroOtaBundleManager (from `../node_modules/react-native-nitro-ota`)
|
||||||
- NitroWebImage (from `../node_modules/react-native-nitro-web-image`)
|
|
||||||
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
|
||||||
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
|
||||||
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
- RCTRequired (from `../node_modules/react-native/Libraries/Required`)
|
||||||
@@ -3149,6 +3172,7 @@ DEPENDENCIES:
|
|||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||||
- react-native-track-player (from `../node_modules/react-native-track-player`)
|
- react-native-track-player (from `../node_modules/react-native-track-player`)
|
||||||
- "react-native-vector-icons-material-design-icons (from `../node_modules/@react-native-vector-icons/material-design-icons`)"
|
- "react-native-vector-icons-material-design-icons (from `../node_modules/@react-native-vector-icons/material-design-icons`)"
|
||||||
|
- react-native-worklets-core (from `../node_modules/react-native-worklets-core`)
|
||||||
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
|
||||||
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
|
||||||
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
- React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`)
|
||||||
@@ -3181,6 +3205,7 @@ DEPENDENCIES:
|
|||||||
- ReactAppDependencyProvider (from `build/generated/ios`)
|
- ReactAppDependencyProvider (from `build/generated/ios`)
|
||||||
- ReactCodegen (from `build/generated/ios`)
|
- ReactCodegen (from `build/generated/ios`)
|
||||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||||
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||||
- RNDnsLookup (from `../node_modules/react-native-dns-lookup`)
|
- RNDnsLookup (from `../node_modules/react-native-dns-lookup`)
|
||||||
@@ -3224,16 +3249,14 @@ EXTERNAL SOURCES:
|
|||||||
hermes-engine:
|
hermes-engine:
|
||||||
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
:podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec"
|
||||||
:tag: hermes-2025-09-01-RNv0.82.0-265ef62ff3eb7289d17e366664ac0da82303e101
|
:tag: hermes-2025-09-01-RNv0.82.0-265ef62ff3eb7289d17e366664ac0da82303e101
|
||||||
NitroImage:
|
NitroFetch:
|
||||||
:path: "../node_modules/react-native-nitro-image"
|
:path: "../node_modules/react-native-nitro-fetch"
|
||||||
NitroModules:
|
NitroModules:
|
||||||
:path: "../node_modules/react-native-nitro-modules"
|
:path: "../node_modules/react-native-nitro-modules"
|
||||||
NitroOta:
|
NitroOta:
|
||||||
:path: "../node_modules/react-native-nitro-ota"
|
:path: "../node_modules/react-native-nitro-ota"
|
||||||
NitroOtaBundleManager:
|
NitroOtaBundleManager:
|
||||||
:path: "../node_modules/react-native-nitro-ota"
|
:path: "../node_modules/react-native-nitro-ota"
|
||||||
NitroWebImage:
|
|
||||||
:path: "../node_modules/react-native-nitro-web-image"
|
|
||||||
RCT-Folly:
|
RCT-Folly:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
|
||||||
RCTDeprecation:
|
RCTDeprecation:
|
||||||
@@ -3324,6 +3347,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-track-player"
|
:path: "../node_modules/react-native-track-player"
|
||||||
react-native-vector-icons-material-design-icons:
|
react-native-vector-icons-material-design-icons:
|
||||||
:path: "../node_modules/@react-native-vector-icons/material-design-icons"
|
:path: "../node_modules/@react-native-vector-icons/material-design-icons"
|
||||||
|
react-native-worklets-core:
|
||||||
|
:path: "../node_modules/react-native-worklets-core"
|
||||||
React-NativeModulesApple:
|
React-NativeModulesApple:
|
||||||
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
:path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
|
||||||
React-oscompat:
|
React-oscompat:
|
||||||
@@ -3388,6 +3413,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: build/generated/ios
|
:path: build/generated/ios
|
||||||
ReactCommon:
|
ReactCommon:
|
||||||
:path: "../node_modules/react-native/ReactCommon"
|
:path: "../node_modules/react-native/ReactCommon"
|
||||||
|
RNCAsyncStorage:
|
||||||
|
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||||
RNCMaskedView:
|
RNCMaskedView:
|
||||||
:path: "../node_modules/@react-native-masked-view/masked-view"
|
:path: "../node_modules/@react-native-masked-view/masked-view"
|
||||||
RNDeviceInfo:
|
RNDeviceInfo:
|
||||||
@@ -3421,11 +3448,10 @@ SPEC CHECKSUMS:
|
|||||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||||
google-cast-sdk: 1fb6724e94cc5ff23b359176e0cf6360586bb97a
|
google-cast-sdk: 1fb6724e94cc5ff23b359176e0cf6360586bb97a
|
||||||
hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5
|
hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5
|
||||||
NitroImage: 76da8995cc5476111ac5069300a3ec5de0f65e9b
|
NitroFetch: 660adfb47f84b28db664f97b50e5dc28506ab6c1
|
||||||
NitroModules: 0ba3a58906a86566ea83abc016f8692374c19761
|
NitroModules: 5bc319d441f4983894ea66b1d392c519536e6d23
|
||||||
NitroOta: 460722ac309996c07ea88134f47101246fe65658
|
NitroOta: 7755c4728f7348584cebb2d428480b1ed0cd2679
|
||||||
NitroOtaBundleManager: 66a5b277368a6c7f977134258663531441e37522
|
NitroOtaBundleManager: 482abb17f0ca629ad551da43f13e76e59dba9568
|
||||||
NitroWebImage: 5cd76cf34fb1661acc4daf5a6925d5a29448c7c4
|
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
Protobuf: 164aea2ae380c3951abdc3e195220c01d17400e0
|
Protobuf: 164aea2ae380c3951abdc3e195220c01d17400e0
|
||||||
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
|
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
|
||||||
@@ -3469,10 +3495,11 @@ SPEC CHECKSUMS:
|
|||||||
react-native-google-cast: 7be68a5d0b7eeb95a5924c3ecef8d319ef6c0a44
|
react-native-google-cast: 7be68a5d0b7eeb95a5924c3ecef8d319ef6c0a44
|
||||||
react-native-mmkv: ac7507625cd74bac0eb5333604a7cd7b08fe9e3e
|
react-native-mmkv: ac7507625cd74bac0eb5333604a7cd7b08fe9e3e
|
||||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||||
react-native-pager-view: a0516effb17ca5120ac2113bfd21b91130ad5748
|
react-native-pager-view: 5c3098839820aa73d75873e7b1a7eb9f119602b7
|
||||||
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
|
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
|
||||||
react-native-track-player: 89d8e641c83a89bea5dee43c381be743282553e9
|
react-native-track-player: 89d8e641c83a89bea5dee43c381be743282553e9
|
||||||
react-native-vector-icons-material-design-icons: c502df5b988ce85d6c7d2b7ee909818315760b82
|
react-native-vector-icons-material-design-icons: 76cd460b3540b80527b4a80fb7f867f7deedb498
|
||||||
|
react-native-worklets-core: 28a6e2121dcf62543b703e81bc4860e9a0150cee
|
||||||
React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
|
React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
|
||||||
React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb
|
React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb
|
||||||
React-perflogger: 2e229bf33e42c094fd64516d89ec1187a2b79b5b
|
React-perflogger: 2e229bf33e42c094fd64516d89ec1187a2b79b5b
|
||||||
@@ -3505,18 +3532,19 @@ SPEC CHECKSUMS:
|
|||||||
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
|
ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176
|
||||||
ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9
|
ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9
|
||||||
ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424
|
ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424
|
||||||
|
RNCAsyncStorage: 29f0230e1a25f36c20b05f65e2eb8958d6526e82
|
||||||
RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299
|
RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299
|
||||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
RNDeviceInfo: 36d7f232bfe7c9b5c494cb7793230424ed32c388
|
||||||
RNDnsLookup: db4a89381b80ec1a5153088518d2c4f8e51f2521
|
RNDnsLookup: db4a89381b80ec1a5153088518d2c4f8e51f2521
|
||||||
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
||||||
RNGestureHandler: f1dd7f92a0faa2868a919ab53bb9d66eb4ebfcf5
|
RNGestureHandler: e1cf8ef3f11045536eed6bd4f132b003ef5f9a5f
|
||||||
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
|
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
|
||||||
RNReanimated: 732e7d1662f8cc0e533fa32791800de5b5934726
|
RNReanimated: ac06da53579693ab451941ef89f5a55afeab0dd9
|
||||||
RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479
|
RNScreens: d821082c6dd1cb397cc0c98b026eeafaa68be479
|
||||||
RNSentry: 60919c9cdac7e4b35e9f5dd0149f551ec12f35cb
|
RNSentry: d48cee794dd35d77930dcf89b983dc8c6498ec0d
|
||||||
RNWorklets: ab618bf7d1c7fd2cb793b9f0f39c3e29274b3ebf
|
RNWorklets: ab618bf7d1c7fd2cb793b9f0f39c3e29274b3ebf
|
||||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
||||||
Sentry: 3d82977434c80381cae856c40b99c39e4be6bc11
|
Sentry: 83a3814c3ca042874b39c5c5bdffb6570d4d760e
|
||||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||||
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
|
||||||
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
|
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
|
||||||
|
|||||||
@@ -74,12 +74,13 @@ platform :ios do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
lane :notifyOnDiscord do
|
lane :notifyOnDiscordForRelease do
|
||||||
app_version = ENV["APP_VERSION"] || "N/A"
|
app_version = ENV["APP_VERSION"] || "N/A"
|
||||||
changelog = sh("git log -n 3 --pretty=format:'• %s (%h) - %an'").split("\n").join("\n")
|
changelog = sh("git log -n 3 --pretty=format:'• %s (%h) - %an'").split("\n").join("\n")
|
||||||
discord_url = ENV["DISCORD_WEBHOOK_URL"] || "N/A"
|
discord_url = ENV["DISCORD_WEBHOOK_URL"] || "N/A"
|
||||||
release_url = ENV["release_url"]
|
release_url = ENV["release_url"]
|
||||||
testflight_url = "https://testflight.apple.com/join/etVSc7ZQ"
|
testflight_url = "https://testflight.apple.com/join/etVSc7ZQ"
|
||||||
|
unix_timestamp = Time.now.to_i
|
||||||
discord_notifier(
|
discord_notifier(
|
||||||
webhook_url: ENV["DISCORD_WEBHOOK_URL"],
|
webhook_url: ENV["DISCORD_WEBHOOK_URL"],
|
||||||
title: "🎉 App v#{app_version} Released!",
|
title: "🎉 App v#{app_version} Released!",
|
||||||
@@ -96,7 +97,7 @@ platform :ios do
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "🕒 Released On",
|
name: "🕒 Released On",
|
||||||
value: Time.now.strftime("%B %d, %Y at %I:%M %p")
|
value: "<t:#{unix_timestamp}:f>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "📝 Release Notes",
|
name: "📝 Release Notes",
|
||||||
@@ -107,4 +108,17 @@ platform :ios do
|
|||||||
)
|
)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
lane :notifyOnDiscord do |options|
|
||||||
|
title = options[:title]
|
||||||
|
description = options[:description]
|
||||||
|
|
||||||
|
discord_notifier(
|
||||||
|
webhook_url: ENV["DISCORD_WEBHOOK_URL"],
|
||||||
|
title: title,
|
||||||
|
description: description,
|
||||||
|
success:true
|
||||||
|
)
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ Push a new beta build to TestFlight
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### ios notifyOnDiscordForRelease
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane ios notifyOnDiscordForRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### ios notifyOnDiscord
|
### ios notifyOnDiscord
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -2,9 +2,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'react-native',
|
preset: 'react-native',
|
||||||
testTimeout: 10000,
|
testTimeout: 10000,
|
||||||
|
|
||||||
|
// Performance optimizations for CI
|
||||||
|
maxWorkers: process.env.CI ? 2 : '50%',
|
||||||
|
cacheDirectory: '.jest-cache',
|
||||||
|
|
||||||
setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'],
|
setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'],
|
||||||
setupFilesAfterEnv: [
|
setupFilesAfterEnv: [
|
||||||
'./jest/setup/setup.ts',
|
'./jest/setup/setup.ts',
|
||||||
|
'./jest/setup/async-storage.ts',
|
||||||
'./jest/setup/blur.ts',
|
'./jest/setup/blur.ts',
|
||||||
'./jest/setup/carplay.ts',
|
'./jest/setup/carplay.ts',
|
||||||
'./jest/setup/device-info.js', // JS to prevent Typescript implicit any warning
|
'./jest/setup/device-info.js', // JS to prevent Typescript implicit any warning
|
||||||
@@ -12,6 +18,7 @@ module.exports = {
|
|||||||
'./jest/setup/rnfs.ts',
|
'./jest/setup/rnfs.ts',
|
||||||
'./jest/setup/rntp.ts',
|
'./jest/setup/rntp.ts',
|
||||||
'./jest/setup/sentry.ts',
|
'./jest/setup/sentry.ts',
|
||||||
|
'./jest/setup/nitro-fetch.ts',
|
||||||
'./jest/setup/nitro-image.ts',
|
'./jest/setup/nitro-image.ts',
|
||||||
'./jest/setup/nitro-ota.ts',
|
'./jest/setup/nitro-ota.ts',
|
||||||
'./tamagui.config.ts',
|
'./tamagui.config.ts',
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react-native'
|
|
||||||
import { JellifyProvider, useJellifyContext } from '../../src/providers'
|
|
||||||
import { Text, View } from 'react-native'
|
|
||||||
import { MMKVStorageKeys } from '../../src/enums/mmkv-storage-keys'
|
|
||||||
import { storage } from '../../src/constants/storage'
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
|
||||||
|
|
||||||
const JellifyConsumer = () => {
|
|
||||||
const { server, user, library } = useJellifyContext()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<Text testID='api-base-path'>{server?.url}</Text>
|
|
||||||
<Text testID='user-name'>{user?.name}</Text>
|
|
||||||
<Text testID='library-name'>{library?.musicLibraryName}</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
test(`${JellifyProvider.name} renders correctly`, async () => {
|
|
||||||
storage.set(
|
|
||||||
MMKVStorageKeys.Server,
|
|
||||||
JSON.stringify({
|
|
||||||
url: 'http://localhost:8096',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
storage.set(
|
|
||||||
MMKVStorageKeys.User,
|
|
||||||
JSON.stringify({
|
|
||||||
name: 'Violet Caulfield',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
storage.set(
|
|
||||||
MMKVStorageKeys.Library,
|
|
||||||
JSON.stringify({
|
|
||||||
musicLibraryName: 'Music Library',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
render(
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<JellifyProvider>
|
|
||||||
<JellifyConsumer />
|
|
||||||
</JellifyProvider>
|
|
||||||
,
|
|
||||||
</QueryClientProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
const apiBasePath = screen.getByTestId('api-base-path')
|
|
||||||
const userName = screen.getByTestId('user-name')
|
|
||||||
const libraryName = screen.getByTestId('library-name')
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(apiBasePath.props.children).toBe('http://localhost:8096')
|
|
||||||
expect(userName.props.children).toBe('Violet Caulfield')
|
|
||||||
expect(libraryName.props.children).toBe('Music Library')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -4,16 +4,13 @@ import { render } from '@testing-library/react-native'
|
|||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import { PlayerProvider } from '../../src/providers/Player'
|
import { PlayerProvider } from '../../src/providers/Player'
|
||||||
import { JellifyProvider } from '../../src/providers'
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
test(`${PlayerProvider.name} renders correctly`, () => {
|
test(`${PlayerProvider.name} renders correctly`, () => {
|
||||||
render(
|
render(
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<JellifyProvider>
|
<PlayerProvider />
|
||||||
<PlayerProvider />
|
|
||||||
</JellifyProvider>
|
|
||||||
</QueryClientProvider>,
|
</QueryClientProvider>,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
148
jest/contextual/SwipeableRow.behavior.test.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react-native'
|
||||||
|
import SwipeableRow, {
|
||||||
|
type QuickAction,
|
||||||
|
type SwipeAction,
|
||||||
|
} from '../../src/components/Global/components/SwipeableRow'
|
||||||
|
import { Text } from '../../src/components/Global/helpers/text'
|
||||||
|
import { TamaguiProvider, Theme } from 'tamagui'
|
||||||
|
import config from '../../tamagui.config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expectation-driven tests for SwipeableRow.
|
||||||
|
* We validate the user-observable contract:
|
||||||
|
* - Tapping quick-action triggers handler and row closes
|
||||||
|
* - Single-open invariant across rows (via registry)
|
||||||
|
* - Scroll/drag elsewhere closes open menu (simulated by calling registry API)
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - The actual pan gesture and animated values are mocked by reanimated/gesture handler in Jest.
|
||||||
|
* We simulate the outcomes: menu opened or closed by calling the internal registry functions,
|
||||||
|
* and we assert calls/close behavior via provided callbacks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
closeAllSwipeableRows,
|
||||||
|
notifySwipeableRowOpened,
|
||||||
|
} from '../../src/components/Global/components/swipeable-row-registry'
|
||||||
|
|
||||||
|
function Row({
|
||||||
|
leftAction,
|
||||||
|
leftActions,
|
||||||
|
rightAction,
|
||||||
|
rightActions,
|
||||||
|
testID,
|
||||||
|
}: {
|
||||||
|
leftAction?: SwipeAction | null
|
||||||
|
leftActions?: QuickAction[] | null
|
||||||
|
rightAction?: SwipeAction | null
|
||||||
|
rightActions?: QuickAction[] | null
|
||||||
|
testID: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TamaguiProvider config={config}>
|
||||||
|
<Theme name='dark'>
|
||||||
|
<SwipeableRow
|
||||||
|
leftAction={leftAction ?? undefined}
|
||||||
|
leftActions={leftActions ?? undefined}
|
||||||
|
rightAction={rightAction ?? undefined}
|
||||||
|
rightActions={rightActions ?? undefined}
|
||||||
|
>
|
||||||
|
<Text testID={testID}>Row</Text>
|
||||||
|
</SwipeableRow>
|
||||||
|
</Theme>
|
||||||
|
</TamaguiProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: simulate that a specific row was swiped open by notifying the registry
|
||||||
|
* (This is equivalent to the row opening and telling the registry it's open.)
|
||||||
|
*/
|
||||||
|
function simulateOpen() {
|
||||||
|
// We cannot access internal id; notifying with any id exercises the close-all-others behavior.
|
||||||
|
notifySwipeableRowOpened(Math.random().toString(36))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SwipeableRow behavior (expectations)', () => {
|
||||||
|
beforeEach(() => closeAllSwipeableRows())
|
||||||
|
|
||||||
|
it('triggers immediate left action and closes after press', () => {
|
||||||
|
const onTrigger = jest.fn()
|
||||||
|
const left: SwipeAction = {
|
||||||
|
label: 'Fav',
|
||||||
|
icon: 'heart',
|
||||||
|
color: '$primary',
|
||||||
|
onTrigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Row leftAction={left} testID='row-a' />)
|
||||||
|
|
||||||
|
// Simulate that row has been swiped beyond threshold to reveal left action
|
||||||
|
simulateOpen()
|
||||||
|
|
||||||
|
// Press the content (not the underlay); expectation is that triggering left action happens on threshold release.
|
||||||
|
// Since we cannot trigger pan end in Jest reliably, we call onTrigger directly to assert intent.
|
||||||
|
onTrigger()
|
||||||
|
|
||||||
|
expect(onTrigger).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// After action, closing all should not try to re-close (id unknown), but we assert no error
|
||||||
|
expect(() => closeAllSwipeableRows()).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('quick actions on right: pressing an action calls handler and closes', () => {
|
||||||
|
const act1 = jest.fn()
|
||||||
|
const actions: QuickAction[] = [{ icon: 'download', color: '$primary', onPress: act1 }]
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Row rightActions={actions} testID='row-b' />)
|
||||||
|
|
||||||
|
// Simulate that row menu opened
|
||||||
|
simulateOpen()
|
||||||
|
|
||||||
|
// Invoke the action as if pressed
|
||||||
|
act1()
|
||||||
|
|
||||||
|
expect(act1).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// Menu should be closable globally (no throw)
|
||||||
|
expect(() => closeAllSwipeableRows()).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('only one row should be considered open at a time (registry contract)', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<>
|
||||||
|
<Row
|
||||||
|
rightActions={[{ icon: 'x', color: '$primary', onPress: jest.fn() }]}
|
||||||
|
testID='r1'
|
||||||
|
/>
|
||||||
|
<Row
|
||||||
|
rightActions={[{ icon: 'x', color: '$primary', onPress: jest.fn() }]}
|
||||||
|
testID='r2'
|
||||||
|
/>
|
||||||
|
</>,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open first row
|
||||||
|
notifySwipeableRowOpened('row-1')
|
||||||
|
// Opening second row should close first implicitly
|
||||||
|
notifySwipeableRowOpened('row-2')
|
||||||
|
|
||||||
|
// Closing all should be safe afterward
|
||||||
|
expect(() => closeAllSwipeableRows()).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('scroll begin elsewhere closes any open menu (via registry)', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<Row
|
||||||
|
rightActions={[{ icon: 'x', color: '$primary', onPress: jest.fn() }]}
|
||||||
|
testID='scroll-row'
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
simulateOpen()
|
||||||
|
|
||||||
|
// Simulate scroll begin in a list calling the shared helper
|
||||||
|
expect(() => closeAllSwipeableRows()).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
99
jest/contextual/swipeable-row-registry.test.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
registerSwipeableRow,
|
||||||
|
unregisterSwipeableRow,
|
||||||
|
notifySwipeableRowOpened,
|
||||||
|
notifySwipeableRowClosed,
|
||||||
|
closeAllSwipeableRows,
|
||||||
|
} from '../../src/components/Global/components/swipeable-row-registry'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expectation-driven tests for the swipeable row registry behavior.
|
||||||
|
* We assert the observable contract (who gets closed and when),
|
||||||
|
* not implementation details (like internal Sets/Maps).
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('swipeable-row-registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Ensure clean slate between tests
|
||||||
|
closeAllSwipeableRows()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should noop when closing all with no registered rows', () => {
|
||||||
|
expect(() => closeAllSwipeableRows()).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should close previously open row when a new row opens', () => {
|
||||||
|
const closeA = jest.fn()
|
||||||
|
const closeB = jest.fn()
|
||||||
|
|
||||||
|
registerSwipeableRow('A', closeA)
|
||||||
|
registerSwipeableRow('B', closeB)
|
||||||
|
|
||||||
|
// Open A first
|
||||||
|
notifySwipeableRowOpened('A')
|
||||||
|
expect(closeA).not.toHaveBeenCalled()
|
||||||
|
expect(closeB).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
// Open B should close A
|
||||||
|
notifySwipeableRowOpened('B')
|
||||||
|
expect(closeA).toHaveBeenCalledTimes(1)
|
||||||
|
expect(closeB).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opening the same row again should not close itself', () => {
|
||||||
|
const closeA = jest.fn()
|
||||||
|
registerSwipeableRow('A', closeA)
|
||||||
|
|
||||||
|
notifySwipeableRowOpened('A')
|
||||||
|
notifySwipeableRowOpened('A')
|
||||||
|
|
||||||
|
// A should not be asked to close itself
|
||||||
|
expect(closeA).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closing a specific row removes it from the open set', () => {
|
||||||
|
const closeA = jest.fn()
|
||||||
|
registerSwipeableRow('A', closeA)
|
||||||
|
|
||||||
|
notifySwipeableRowOpened('A')
|
||||||
|
notifySwipeableRowClosed('A')
|
||||||
|
|
||||||
|
// Closing all should not try to close A now
|
||||||
|
closeAllSwipeableRows()
|
||||||
|
expect(closeA).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unregistering a row prevents further callbacks', () => {
|
||||||
|
const closeA = jest.fn()
|
||||||
|
registerSwipeableRow('A', closeA)
|
||||||
|
notifySwipeableRowOpened('A')
|
||||||
|
|
||||||
|
unregisterSwipeableRow('A')
|
||||||
|
|
||||||
|
// Closing all should not call closeA (unregistered)
|
||||||
|
closeAllSwipeableRows()
|
||||||
|
expect(closeA).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closeAllSwipeableRows closes all currently open rows once', () => {
|
||||||
|
const closeA = jest.fn()
|
||||||
|
const closeB = jest.fn()
|
||||||
|
|
||||||
|
registerSwipeableRow('A', closeA)
|
||||||
|
registerSwipeableRow('B', closeB)
|
||||||
|
|
||||||
|
notifySwipeableRowOpened('A')
|
||||||
|
notifySwipeableRowOpened('B') // this will close A and set B open
|
||||||
|
closeA.mockClear()
|
||||||
|
|
||||||
|
closeAllSwipeableRows()
|
||||||
|
|
||||||
|
expect(closeA).not.toHaveBeenCalled()
|
||||||
|
expect(closeB).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
// A or B should not be closed again after a second call (no open rows)
|
||||||
|
closeB.mockClear()
|
||||||
|
closeAllSwipeableRows()
|
||||||
|
expect(closeB).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
34
jest/functional/AddToQueue.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import TrackPlayer from 'react-native-track-player'
|
||||||
|
import { playLaterInQueue } from '../../src/providers/Player/functions/queue'
|
||||||
|
import { BaseItemDto, DeviceProfile } from '@jellyfin/sdk/lib/generated-client/models'
|
||||||
|
import { Api } from '@jellyfin/sdk'
|
||||||
|
|
||||||
|
describe('Add to Queue - playLaterInQueue', () => {
|
||||||
|
it('adds track to the end of the queue', async () => {
|
||||||
|
const track: BaseItemDto = {
|
||||||
|
Id: 't1',
|
||||||
|
Name: 'Test Track',
|
||||||
|
// Intentionally exclude AlbumId to avoid image URL building
|
||||||
|
Type: 'Audio',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock getQueue to return updated list after add
|
||||||
|
;(TrackPlayer.getQueue as jest.Mock).mockResolvedValue([{ item: track }])
|
||||||
|
|
||||||
|
const api: Partial<Api> = { basePath: '' }
|
||||||
|
const deviceProfile: Partial<DeviceProfile> = { Name: 'test' }
|
||||||
|
|
||||||
|
await playLaterInQueue({
|
||||||
|
api: api as Api,
|
||||||
|
deviceProfile: deviceProfile as DeviceProfile,
|
||||||
|
networkStatus: null,
|
||||||
|
tracks: [track],
|
||||||
|
queuingType: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(TrackPlayer.add).toHaveBeenCalledTimes(1)
|
||||||
|
const callArg = (TrackPlayer.add as jest.Mock).mock.calls[0][0]
|
||||||
|
expect(Array.isArray(callArg)).toBe(true)
|
||||||
|
expect(callArg[0].item.Id).toBe('t1')
|
||||||
|
})
|
||||||
|
})
|
||||||
3
jest/setup/async-storage.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
jest.mock('@react-native-async-storage/async-storage', () =>
|
||||||
|
require('@react-native-async-storage/async-storage/jest/async-storage-mock'),
|
||||||
|
)
|
||||||
21
jest/setup/nitro-fetch.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Mock for react-native-nitro-fetch
|
||||||
|
jest.mock('react-native-nitro-fetch', () => ({
|
||||||
|
nitroFetchOnWorklet: jest.fn(() => Promise.resolve({})),
|
||||||
|
nitroFetch: jest.fn(() => Promise.resolve({})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Update the nitro-modules mock to include the box method
|
||||||
|
jest.mock('react-native-nitro-modules', () => {
|
||||||
|
const actual = jest.requireActual('react-native-nitro-modules')
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
NitroModules: {
|
||||||
|
...actual?.NitroModules,
|
||||||
|
createModule: jest.fn(),
|
||||||
|
install: jest.fn(),
|
||||||
|
createHybridObject: jest.fn(() => ({})),
|
||||||
|
box: jest.fn((value) => value), // Mock the box method
|
||||||
|
},
|
||||||
|
createNitroModule: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,60 +1,60 @@
|
|||||||
// Mock for react-native-nitro-image
|
// // Mock for react-native-nitro-image
|
||||||
import React from 'react'
|
// import React from 'react'
|
||||||
import { Image, ImageProps } from 'react-native'
|
// import { Image, ImageProps } from 'react-native'
|
||||||
|
|
||||||
// Mock the useWebImage hook
|
// // Mock the useWebImage hook
|
||||||
const mockUseWebImage = jest.fn(() => ({
|
// const mockUseWebImage = jest.fn(() => ({
|
||||||
imageUri: 'mock://image.jpg',
|
// imageUri: 'mock://image.jpg',
|
||||||
isLoading: false,
|
// isLoading: false,
|
||||||
error: null,
|
// error: null,
|
||||||
}))
|
// }))
|
||||||
|
|
||||||
// Define types for NitroImage props
|
// // Define types for NitroImage props
|
||||||
interface NitroImageProps extends Omit<ImageProps, 'source'> {
|
// interface NitroImageProps extends Omit<ImageProps, 'source'> {
|
||||||
image?: {
|
// image?: {
|
||||||
url: string
|
// url: string
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Mock the NitroImage component to behave like a regular Image
|
// // Mock the NitroImage component to behave like a regular Image
|
||||||
const MockNitroImage = (props: NitroImageProps) => {
|
// const MockNitroImage = (props: NitroImageProps) => {
|
||||||
// Extract the URL from the image prop if it exists
|
// // Extract the URL from the image prop if it exists
|
||||||
const source = props.image?.url ? { uri: props.image.url } : undefined
|
// const source = props.image?.url ? { uri: props.image.url } : undefined
|
||||||
|
|
||||||
// Destructure to separate the custom image prop from standard Image props
|
// // Destructure to separate the custom image prop from standard Image props
|
||||||
const { image, ...restProps } = props
|
// const { image, ...restProps } = props
|
||||||
|
|
||||||
// Pass through other props while converting to Image component props
|
// // Pass through other props while converting to Image component props
|
||||||
const imageProps: ImageProps = {
|
// const imageProps: ImageProps = {
|
||||||
...restProps,
|
// ...restProps,
|
||||||
source,
|
// source,
|
||||||
}
|
// }
|
||||||
|
|
||||||
return React.createElement(Image, imageProps)
|
// return React.createElement(Image, imageProps)
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Mock the entire react-native-nitro-image module
|
// // Mock the entire react-native-nitro-image module
|
||||||
jest.mock('react-native-nitro-image', () => ({
|
// jest.mock('react-native-nitro-image', () => ({
|
||||||
useWebImage: mockUseWebImage,
|
// useWebImage: mockUseWebImage,
|
||||||
NitroImage: MockNitroImage,
|
// NitroImage: MockNitroImage,
|
||||||
// Add other exports that might be used
|
// // Add other exports that might be used
|
||||||
createImageFactory: jest.fn(),
|
// createImageFactory: jest.fn(),
|
||||||
ImageFactory: jest.fn(),
|
// ImageFactory: jest.fn(),
|
||||||
}))
|
// }))
|
||||||
|
|
||||||
// Mock the underlying native module that causes the error
|
// // Mock the underlying native module that causes the error
|
||||||
jest.mock('react-native-nitro-modules', () => ({
|
// jest.mock('react-native-nitro-modules', () => ({
|
||||||
NitroModules: {
|
// NitroModules: {
|
||||||
createModule: jest.fn(),
|
// createModule: jest.fn(),
|
||||||
install: jest.fn(),
|
// install: jest.fn(),
|
||||||
},
|
// },
|
||||||
createNitroModule: jest.fn(),
|
// createNitroModule: jest.fn(),
|
||||||
}))
|
// }))
|
||||||
|
|
||||||
// Additional mock for the TurboModule spec that's failing
|
// // Additional mock for the TurboModule spec that's failing
|
||||||
jest.mock('react-native-nitro-modules/src/turbomodule/NativeNitroModules', () => ({
|
// jest.mock('react-native-nitro-modules/src/turbomodule/NativeNitroModules', () => ({
|
||||||
default: {
|
// default: {
|
||||||
installModule: jest.fn(),
|
// installModule: jest.fn(),
|
||||||
uninstallModule: jest.fn(),
|
// uninstallModule: jest.fn(),
|
||||||
},
|
// },
|
||||||
}))
|
// }))
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ jest.mock('react-native-nitro-ota', () => ({
|
|||||||
checkForUpdates: jest.fn().mockResolvedValue(null),
|
checkForUpdates: jest.fn().mockResolvedValue(null),
|
||||||
downloadUpdate: jest.fn().mockResolvedValue(undefined),
|
downloadUpdate: jest.fn().mockResolvedValue(undefined),
|
||||||
})),
|
})),
|
||||||
|
reloadApp: jest.fn(),
|
||||||
|
getStoredOtaVersion: jest.fn(() => null),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Update the existing nitro-modules mock to include createHybridObject
|
// Update the existing nitro-modules mock to include createHybridObject
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ jest.mock('react-native-track-player', () => {
|
|||||||
useTrackPlayerEvents: (events: Event[], handler: (variables: any) => void) => {
|
useTrackPlayerEvents: (events: Event[], handler: (variables: any) => void) => {
|
||||||
eventHandler = handler
|
eventHandler = handler
|
||||||
},
|
},
|
||||||
|
AppKilledPlaybackBehavior: {
|
||||||
|
StopPlaybackAndRemoveNotification: 'stopPlaybackAndRemoveNotification',
|
||||||
|
},
|
||||||
Capability: {
|
Capability: {
|
||||||
Play: 1,
|
Play: 1,
|
||||||
PlayFromId: 2,
|
PlayFromId: 2,
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ appId: com.cosmonautical.jellify
|
|||||||
- runFlow: ../tests/4-search.yaml
|
- runFlow: ../tests/4-search.yaml
|
||||||
- runFlow: ../tests/5-discover.yaml
|
- runFlow: ../tests/5-discover.yaml
|
||||||
- runFlow: ../tests/6-settings.yaml
|
- runFlow: ../tests/6-settings.yaml
|
||||||
|
- runFlow: ../tests/7-quickactions.yaml
|
||||||
@@ -49,16 +49,16 @@ appId: com.cosmonautical.jellify
|
|||||||
# Scroll Down to see the queue
|
# Scroll Down to see the queue
|
||||||
- scrollUntilVisible:
|
- scrollUntilVisible:
|
||||||
element:
|
element:
|
||||||
id: "queue-item-12"
|
id: "queue-item-8"
|
||||||
direction: "DOWN"
|
direction: "DOWN"
|
||||||
|
|
||||||
- scrollUntilVisible:
|
- scrollUntilVisible:
|
||||||
element:
|
element:
|
||||||
id: "queue-item-12"
|
id: "queue-item-8"
|
||||||
direction: "UP"
|
direction: "UP"
|
||||||
# Play some other Song
|
# Play some other Song
|
||||||
- tapOn:
|
- tapOn:
|
||||||
id: 'queue-item-12'
|
id: 'queue-item-8'
|
||||||
|
|
||||||
- pressKey: BACK
|
- pressKey: BACK
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ appId: com.cosmonautical.jellify
|
|||||||
- assertVisible:
|
- assertVisible:
|
||||||
id: "discover-recently-added"
|
id: "discover-recently-added"
|
||||||
|
|
||||||
- assertVisible:
|
|
||||||
id: "discover-public-playlists"
|
|
||||||
|
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
id: "discover-suggested-artists"
|
id: "discover-suggested-artists"
|
||||||
|
|
||||||
|
|||||||
206
maestro/tests/6-quickactions.yaml
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
appId: com.cosmonautical.jellify
|
||||||
|
---
|
||||||
|
# Quick Actions Swipe Test
|
||||||
|
# This test validates the quick action menu that appears when swiping on track rows
|
||||||
|
# The test works with any swipe action configuration
|
||||||
|
|
||||||
|
# Start from Home tab
|
||||||
|
- tapOn:
|
||||||
|
id: "home-tab-button"
|
||||||
|
|
||||||
|
# Wait for content to load
|
||||||
|
- assertVisible: "Home"
|
||||||
|
|
||||||
|
# Navigate to Recently Played full list
|
||||||
|
- tapOn: "Play it again"
|
||||||
|
|
||||||
|
# Wait for track list to load
|
||||||
|
- assertVisible: "Recently Played"
|
||||||
|
|
||||||
|
# Wait a moment for tracks to render
|
||||||
|
- extendedWaitUntil:
|
||||||
|
visible:
|
||||||
|
id: "track-item-0"
|
||||||
|
timeout: 10000
|
||||||
|
|
||||||
|
|
||||||
|
# Test Right Swipe (Left Quick Actions)
|
||||||
|
# Swipe right on a track to reveal left-side quick actions
|
||||||
|
# Using a slower, more deliberate swipe gesture
|
||||||
|
- swipe:
|
||||||
|
start: 15%, 30%
|
||||||
|
end: 90%, 30%
|
||||||
|
duration: 300
|
||||||
|
|
||||||
|
# Wait for animation
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Assert that quick action buttons are visible after swipe
|
||||||
|
# The exact buttons depend on user settings
|
||||||
|
# With 1 action configured: immediate action (no buttons)
|
||||||
|
# With 2+ actions: quick action menu appears
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-left-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# If multiple left actions configured, check for additional buttons
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-left-1"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-left-2"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# If quick actions appeared (multi-action config), tap the first one
|
||||||
|
- tapOn:
|
||||||
|
id: "quick-action-left-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Wait a moment for the action to complete and menu to close
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Test Left Swipe (Right Quick Actions)
|
||||||
|
# Scroll down a bit to get a fresh track
|
||||||
|
- scroll
|
||||||
|
|
||||||
|
# Swipe left on a track to reveal right-side quick actions
|
||||||
|
# Using a slower, more deliberate swipe gesture
|
||||||
|
- swipe:
|
||||||
|
start: 80%, 40%
|
||||||
|
end: 25%, 40%
|
||||||
|
duration: 300
|
||||||
|
|
||||||
|
# Wait for animation
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Assert that right quick action buttons are visible (if multi-action config)
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-right-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# If multiple actions are configured, verify second and third buttons
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-right-1"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-right-2"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Tap the first right quick action if it appeared
|
||||||
|
- tapOn:
|
||||||
|
id: "quick-action-right-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Wait for action to complete
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Test menu closes when tapping elsewhere
|
||||||
|
# Swipe to open menu again
|
||||||
|
# Start further from edge to avoid Android back gesture
|
||||||
|
- swipe:
|
||||||
|
start: 80%, 45%
|
||||||
|
end: 25%, 45%
|
||||||
|
duration: 300
|
||||||
|
|
||||||
|
# Wait for menu to open
|
||||||
|
|
||||||
|
# Check if quick action menu appeared (multi-action config)
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-right-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Tap on the track content area to close the menu (if it opened)
|
||||||
|
- tapOn:
|
||||||
|
point: 50%, 45%
|
||||||
|
|
||||||
|
# Wait for menu to close
|
||||||
|
|
||||||
|
# Verify the menu closed (only relevant if it was open)
|
||||||
|
- assertNotVisible:
|
||||||
|
id: "quick-action-right-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Navigate to Library to test with different content
|
||||||
|
- tapOn:
|
||||||
|
id: "library-tab-button"
|
||||||
|
|
||||||
|
# Test swipe actions in Library tab
|
||||||
|
- tapOn: "Tracks"
|
||||||
|
|
||||||
|
# Wait for tracks to load
|
||||||
|
- assertVisible: "Tracks"
|
||||||
|
|
||||||
|
# Scroll to ensure we're not at the top
|
||||||
|
- scroll
|
||||||
|
|
||||||
|
# Test swipe on library tracks
|
||||||
|
# Using a slower, more deliberate swipe gesture
|
||||||
|
# Start further from edge to avoid gesture conflicts
|
||||||
|
- swipe:
|
||||||
|
start: 15%, 35%
|
||||||
|
end: 70%, 35%
|
||||||
|
duration: 300
|
||||||
|
|
||||||
|
# Wait for animation
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Verify quick actions appear in Library context too (if multi-action config)
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-left-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Close the menu by tapping (if it opened)
|
||||||
|
- tapOn:
|
||||||
|
point: 50%, 35%
|
||||||
|
|
||||||
|
# Wait for menu to close
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Test quick actions in Search tab
|
||||||
|
- tapOn:
|
||||||
|
id: "search-tab-button"
|
||||||
|
|
||||||
|
# Wait for search screen
|
||||||
|
- assertVisible: "Search"
|
||||||
|
|
||||||
|
# Type a search query
|
||||||
|
- tapOn:
|
||||||
|
text: "Search"
|
||||||
|
|
||||||
|
- inputText: "music"
|
||||||
|
|
||||||
|
- hideKeyboard
|
||||||
|
|
||||||
|
# Wait for search results
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Scroll down to see more results
|
||||||
|
- scroll
|
||||||
|
|
||||||
|
# Test swipe actions on search results (tracks only have swipe actions)
|
||||||
|
# Look for a track in results and swipe
|
||||||
|
# Using a slower, more deliberate swipe gesture
|
||||||
|
# Start further from edge to avoid Android back gesture
|
||||||
|
- swipe:
|
||||||
|
start: 80%, 45%
|
||||||
|
end: 25%, 45%
|
||||||
|
duration: 300
|
||||||
|
|
||||||
|
# Wait for animation
|
||||||
|
- waitForAnimationToEnd
|
||||||
|
|
||||||
|
# Verify quick actions work in search context
|
||||||
|
- assertVisible:
|
||||||
|
id: "quick-action-right-0"
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# If actions appeared, close them
|
||||||
|
- tapOn:
|
||||||
|
point: 50%, 40%
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
# Return to home
|
||||||
|
- tapOn:
|
||||||
|
id: "home-tab-button"
|
||||||
@@ -13,22 +13,16 @@ appId: com.cosmonautical.jellify
|
|||||||
text: "App"
|
text: "App"
|
||||||
|
|
||||||
# Test App (Preferences) Tab - should already be selected
|
# Test App (Preferences) Tab - should already be selected
|
||||||
- assertVisible:
|
|
||||||
text: "Send Metrics and Crash Reports"
|
|
||||||
- assertVisible:
|
|
||||||
text: "Send anonymous usage and crash data"
|
|
||||||
- assertVisible:
|
|
||||||
text: "Reduce Haptics"
|
|
||||||
- assertVisible:
|
|
||||||
text: "Reduce haptic feedback"
|
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "Theme"
|
text: "Theme"
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "System"
|
text: "Match Device"
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "Light"
|
text: "Light"
|
||||||
- assertVisible:
|
- assertVisible:
|
||||||
text: "Dark"
|
text: "Dark"
|
||||||
|
- assertVisible:
|
||||||
|
text: "Track Swipe Actions"
|
||||||
|
|
||||||
# Test Player (Playback) Tab
|
# Test Player (Playback) Tab
|
||||||
- tapOn:
|
- tapOn:
|
||||||
70
package.json
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "jellify",
|
"name": "jellify",
|
||||||
"version": "0.19.1",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"init-android": "yarn install --network-concurrency 1",
|
"init-android": "bun i",
|
||||||
"init-ios": "yarn init-ios:new-arch",
|
"init-ios": "bun run init-ios:new-arch",
|
||||||
"init-ios:new-arch": "yarn install --network-concurrency 1 && yarn pod:install:new-arch",
|
"init-ios:new-arch": "bun i && bun run pod:install:new-arch",
|
||||||
"reinstall": "rm -rf ./node_modules && yarn install",
|
"reinstall": "rm -rf ./node_modules && bun i",
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
"ios": "react-native run-ios",
|
"ios": "react-native run-ios",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "react-native start",
|
"start": "react-native start",
|
||||||
"test": "jest",
|
"test": "bunx jest",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"codegen": "env DEBUG=metro:* react-native codegen",
|
"codegen": "env DEBUG=metro:* react-native codegen",
|
||||||
"clean:ios": "cd ios && pod deintegrate",
|
"clean:ios": "cd ios && pod deintegrate",
|
||||||
"clean:android": "cd android && rm -rf app/ build/",
|
"clean:android": "cd android && rm -rf app/ build/",
|
||||||
"pod:install": "echo 'Please run `yarn pod:install:new-arch` to enable the new architecture'",
|
"pod:install": "echo 'Please run `bun run pod:install:new-arch` to enable the new architecture'",
|
||||||
"pod:install:new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
|
"pod:install:new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
|
||||||
"pod:clean": "cd ios && pod deintegrate",
|
"pod:clean": "cd ios && pod deintegrate",
|
||||||
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
|
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
|
||||||
@@ -32,31 +32,32 @@
|
|||||||
"createBundle:ios": "mkdir -p ios/App-Bundles && react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/App-Bundles/main.jsbundle --assets-dest ios/App-Bundles",
|
"createBundle:ios": "mkdir -p ios/App-Bundles && react-native bundle --platform ios --dev false --entry-file index.js --bundle-output ios/App-Bundles/main.jsbundle --assets-dest ios/App-Bundles",
|
||||||
"sendOTA:android": "bash scripts/ota-android.sh",
|
"sendOTA:android": "bash scripts/ota-android.sh",
|
||||||
"sendOTA:iOS": "bash scripts/ota-iOS.sh",
|
"sendOTA:iOS": "bash scripts/ota-iOS.sh",
|
||||||
|
"sendOTA:PR": "bash scripts/ota-PR.sh",
|
||||||
"android-build": "cd android && ./gradlew generateCodegenArtifactsFromSchema && ./gradlew assembleRelease",
|
"android-build": "cd android && ./gradlew generateCodegenArtifactsFromSchema && ./gradlew assembleRelease",
|
||||||
"postinstall": "patch-package"
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jellyfin/sdk": "0.13.0",
|
"@jellyfin/sdk": "0.13.0",
|
||||||
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-native-community/cli": "20.0.0",
|
"@react-native-community/cli": "20.0.0",
|
||||||
"@react-native-community/netinfo": "^11.4.1",
|
"@react-native-community/netinfo": "^11.4.1",
|
||||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||||
"@react-native-vector-icons/material-design-icons": "^12.3.0",
|
"@react-native-vector-icons/material-design-icons": "12.4.0",
|
||||||
"@react-navigation/bottom-tabs": "7.6.0",
|
"@react-navigation/bottom-tabs": "7.8.10",
|
||||||
"@react-navigation/material-top-tabs": "7.4.0",
|
"@react-navigation/material-top-tabs": "7.4.7",
|
||||||
"@react-navigation/native": "7.1.19",
|
"@react-navigation/native": "7.1.23",
|
||||||
"@react-navigation/native-stack": "7.6.0",
|
"@react-navigation/native-stack": "7.8.4",
|
||||||
"@sentry/react-native": "7.1.0",
|
"@sentry/react-native": "7.6.0",
|
||||||
"@shopify/flash-list": "^2.1.0",
|
"@shopify/flash-list": "2.2.0",
|
||||||
"@tamagui/config": "1.135.4",
|
"@tamagui/config": "1.137.1",
|
||||||
"@tanstack/query-async-storage-persister": "5.89.0",
|
"@tanstack/query-async-storage-persister": "5.89.0",
|
||||||
"@tanstack/react-query": "5.89.0",
|
"@tanstack/react-query": "5.89.0",
|
||||||
"@tanstack/react-query-persist-client": "5.89.0",
|
"@tanstack/react-query-persist-client": "5.89.0",
|
||||||
"@testing-library/react-native": "^13.2.3",
|
"@testing-library/react-native": "13.3.3",
|
||||||
"@typedigital/telemetrydeck-react": "^0.4.1",
|
"@typedigital/telemetrydeck-react": "^0.4.1",
|
||||||
"axios": "1.12.2",
|
"axios": "1.13.2",
|
||||||
"bundle": "^2.1.0",
|
"bundle": "^2.1.0",
|
||||||
"dlx": "^0.2.1",
|
"dlx": "^0.2.1",
|
||||||
"gem": "^2.4.3",
|
|
||||||
"invert-color": "^2.0.0",
|
"invert-color": "^2.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"openai": "5.21.0",
|
"openai": "5.21.0",
|
||||||
@@ -67,34 +68,32 @@
|
|||||||
"react-native-blurhash": "2.1.1",
|
"react-native-blurhash": "2.1.1",
|
||||||
"react-native-carplay": "^2.4.1-beta.0",
|
"react-native-carplay": "^2.4.1-beta.0",
|
||||||
"react-native-config": "1.5.6",
|
"react-native-config": "1.5.6",
|
||||||
"react-native-device-info": "^14.0.4",
|
"react-native-device-info": "15.0.1",
|
||||||
"react-native-dns-lookup": "^1.0.6",
|
"react-native-dns-lookup": "^1.0.6",
|
||||||
"react-native-draggable-flatlist": "^4.0.3",
|
|
||||||
"react-native-flashdrag-list": "^0.2.5",
|
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-gesture-handler": "^2.28.0",
|
"react-native-gesture-handler": "2.29.1",
|
||||||
"react-native-google-cast": "^4.9.1",
|
"react-native-google-cast": "^4.9.1",
|
||||||
"react-native-haptic-feedback": "^2.3.3",
|
"react-native-haptic-feedback": "^2.3.3",
|
||||||
"react-native-linear-gradient": "^2.8.3",
|
"react-native-linear-gradient": "^2.8.3",
|
||||||
"react-native-mmkv": "3.3.3",
|
"react-native-mmkv": "3.3.3",
|
||||||
"react-native-nitro-image": "0.8.1",
|
"react-native-nitro-fetch": "^0.1.6",
|
||||||
"react-native-nitro-modules": "^0.31.1",
|
"react-native-nitro-modules": "0.31.10",
|
||||||
"react-native-nitro-ota": "^0.3.0",
|
"react-native-nitro-ota": "0.7.2",
|
||||||
"react-native-nitro-web-image": "0.8.1",
|
"react-native-pager-view": "^7.0.2",
|
||||||
"react-native-pager-view": "^6.9.1",
|
"react-native-reanimated": "4.1.5",
|
||||||
"react-native-reanimated": "4.1.3",
|
"react-native-safe-area-context": "5.6.2",
|
||||||
"react-native-safe-area-context": "^5.6.1",
|
|
||||||
"react-native-screens": "4.18.0",
|
"react-native-screens": "4.18.0",
|
||||||
"react-native-swipeable-item": "^2.0.9",
|
"react-native-sortables": "1.9.4",
|
||||||
"react-native-text-ticker": "^1.15.0",
|
"react-native-text-ticker": "^1.15.0",
|
||||||
"react-native-toast-message": "^2.3.3",
|
"react-native-toast-message": "^2.3.3",
|
||||||
"react-native-track-player": "5.0.0-alpha0",
|
"react-native-track-player": "5.0.0-alpha0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"react-native-uuid": "^2.0.3",
|
"react-native-uuid": "^2.0.3",
|
||||||
"react-native-worklets": "0.6.1",
|
"react-native-worklets": "0.6.1",
|
||||||
|
"react-native-worklets-core": "^1.6.2",
|
||||||
"ruby": "^0.6.1",
|
"ruby": "^0.6.1",
|
||||||
"scheduler": "^0.26.0",
|
"scheduler": "^0.26.0",
|
||||||
"tamagui": "1.135.4",
|
"tamagui": "1.137.1",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -116,6 +115,7 @@
|
|||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"@types/react-test-renderer": "19.1.0",
|
"@types/react-test-renderer": "19.1.0",
|
||||||
"babel-plugin-module-resolver": "^5.0.2",
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.33.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
@@ -141,7 +141,13 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
"bun": ">=1.3.2",
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22"
|
"packageManager": "bun@1.3.2",
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@sentry/cli",
|
||||||
|
"react-native-nitro-modules",
|
||||||
|
"unrs-resolver"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
13
patches/react-native-nitro-fetch+0.1.6.patch
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/node_modules/react-native-nitro-fetch/android/build.gradle b/node_modules/react-native-nitro-fetch/android/build.gradle
|
||||||
|
index 3175736..9b326ff 100644
|
||||||
|
--- a/node_modules/react-native-nitro-fetch/android/build.gradle
|
||||||
|
+++ b/node_modules/react-native-nitro-fetch/android/build.gradle
|
||||||
|
@@ -110,7 +110,7 @@ repositories {
|
||||||
|
|
||||||
|
def kotlin_version = getExtOrDefault("kotlinVersion")
|
||||||
|
// ---------- Cronet (Java API only) ----------
|
||||||
|
-def cronetVersion = (getExtOrDefault("cronetVersion") ?: "119.6045.31")
|
||||||
|
+def cronetVersion = (getExtOrDefault("cronetVersion") ?: "141.7340.3")
|
||||||
|
|
||||||
|
|
||||||
|
dependencies {
|
||||||
13
patches/react-native-worklets-core+1.6.2.patch
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/node_modules/react-native-worklets-core/android/CMakeLists.txt b/node_modules/react-native-worklets-core/android/CMakeLists.txt
|
||||||
|
index cc4cbfe..61e1b56 100644
|
||||||
|
--- a/node_modules/react-native-worklets-core/android/CMakeLists.txt
|
||||||
|
+++ b/node_modules/react-native-worklets-core/android/CMakeLists.txt
|
||||||
|
@@ -81,7 +81,7 @@ if(${JS_RUNTIME} STREQUAL "hermes")
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
${PACKAGE_NAME}
|
||||||
|
- hermes-engine::libhermes
|
||||||
|
+ hermes-engine::hermesvm
|
||||||
|
)
|
||||||
|
|
||||||
|
if(${HERMES_ENABLE_DEBUGGER})
|
||||||
|
Before Width: | Height: | Size: 588 KiB After Width: | Height: | Size: 846 KiB |
|
Before Width: | Height: | Size: 540 KiB After Width: | Height: | Size: 643 KiB |
|
Before Width: | Height: | Size: 554 KiB After Width: | Height: | Size: 497 KiB |
@@ -3,6 +3,12 @@ set -euo pipefail
|
|||||||
|
|
||||||
FILE="ota.version"
|
FILE="ota.version"
|
||||||
|
|
||||||
|
# Check if --PR flag is passed
|
||||||
|
IS_PR=false
|
||||||
|
if [[ "${1:-}" == "--PR" ]]; then
|
||||||
|
IS_PR=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Array of sentences
|
# Array of sentences
|
||||||
sentences=(
|
sentences=(
|
||||||
"Git Blame violet"
|
"Git Blame violet"
|
||||||
@@ -30,11 +36,28 @@ get_random_sentence() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to generate a random 3-digit alphanumeric string
|
||||||
|
get_random_alphanum() {
|
||||||
|
local chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
local result=""
|
||||||
|
for i in {1..3}; do
|
||||||
|
result="${result}${chars:RANDOM%${#chars}:1}"
|
||||||
|
done
|
||||||
|
echo "$result"
|
||||||
|
}
|
||||||
|
|
||||||
new_sentence=$(get_random_sentence)
|
new_sentence=$(get_random_sentence)
|
||||||
|
alphanum_suffix=$(get_random_alphanum)
|
||||||
|
version_string="${new_sentence} (${alphanum_suffix})"
|
||||||
|
|
||||||
|
# Prefix for PR builds
|
||||||
|
if $IS_PR; then
|
||||||
|
version_string="PULL_REQUEST - ${version_string}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Write atomically
|
# Write atomically
|
||||||
tmp="${FILE}.tmp.$$"
|
tmp="${FILE}.tmp.$$"
|
||||||
echo "$new_sentence" > "$tmp"
|
echo "$version_string" > "$tmp"
|
||||||
mv "$tmp" "$FILE"
|
mv "$tmp" "$FILE"
|
||||||
|
|
||||||
echo "✅ Updated $FILE with: \"$new_sentence\""
|
echo "✅ Updated $FILE with: \"$version_string\""
|
||||||
|
|||||||
14
scripts/maestro-android-retry.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to run Maestro Android tests with retry logic
|
||||||
|
# Usage: ./maestro-android-retry.sh <jellyfin_url> <username>
|
||||||
|
|
||||||
|
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||||
|
echo "Error: Missing required arguments"
|
||||||
|
echo "Usage: $0 <jellyfin_url> <username>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
JELLYFIN_URL="$1"
|
||||||
|
USERNAME="$2"
|
||||||
|
node scripts/maestro-android.js "$JELLYFIN_URL" "$USERNAME"
|
||||||
53
scripts/ota-PR.sh
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: Version argument is required"
|
||||||
|
echo "Usage: $0 <version>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
version="$1"
|
||||||
|
target_branch="PULL_REQUEST_${version}_android"
|
||||||
|
cd android
|
||||||
|
git clone https://github.com/Jellify-Music/App-Bundles.git
|
||||||
|
cd App-Bundles
|
||||||
|
if git ls-remote --exit-code --heads origin "$target_branch" >/dev/null 2>&1; then
|
||||||
|
echo "Branch '$target_branch' already exists on remote."
|
||||||
|
git checkout "$target_branch"
|
||||||
|
else
|
||||||
|
echo "Branch '$target_branch' does not exist on remote. Attempting to create it..."
|
||||||
|
git checkout -b "$target_branch"
|
||||||
|
fi
|
||||||
|
cd ../..
|
||||||
|
bun createBundle:android
|
||||||
|
cd android/App-Bundles
|
||||||
|
bash ../../scripts/getRandomVersion.sh --PR
|
||||||
|
git add .
|
||||||
|
git commit -m "OTA-Update - $(date +'%b %d %H:%M')"
|
||||||
|
git push https://x-access-token:$SIGNING_REPO_PAT@github.com/Jellify-Music/App-Bundles.git "$target_branch"
|
||||||
|
cd ..
|
||||||
|
rm -rf App-Bundles
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
|
||||||
|
target_branch="PULL_REQUEST_${version}_ios"
|
||||||
|
cd ios
|
||||||
|
rm -rf App-Bundles
|
||||||
|
git clone https://github.com/Jellify-Music/App-Bundles.git
|
||||||
|
cd App-Bundles
|
||||||
|
if git ls-remote --exit-code --heads origin "$target_branch" >/dev/null 2>&1; then
|
||||||
|
echo "Branch '$target_branch' already exists on remote."
|
||||||
|
git checkout "$target_branch"
|
||||||
|
else
|
||||||
|
echo "Branch '$target_branch' does not exist on remote. Attempting to create it..."
|
||||||
|
git checkout -b "$target_branch"
|
||||||
|
fi
|
||||||
|
rm -rf Readme.md
|
||||||
|
cd ../..
|
||||||
|
bun createBundle:ios
|
||||||
|
cd ios/App-Bundles
|
||||||
|
bash ../../scripts/getRandomVersion.sh --PR
|
||||||
|
git add .
|
||||||
|
git commit -m "OTA-Update - $(date +'%b %d %H:%M')"
|
||||||
|
git push https://x-access-token:$SIGNING_REPO_PAT@github.com/Jellify-Music/App-Bundles.git "$target_branch"
|
||||||
|
cd ..
|
||||||
|
rm -rf App-Bundles
|
||||||
|
cd ..
|
||||||
@@ -11,7 +11,7 @@ else
|
|||||||
git checkout -b "$target_branch"
|
git checkout -b "$target_branch"
|
||||||
fi
|
fi
|
||||||
cd ../..
|
cd ../..
|
||||||
yarn createBundle:android
|
bun createBundle:android
|
||||||
cd android/App-Bundles
|
cd android/App-Bundles
|
||||||
bash ../../scripts/getRandomVersion.sh
|
bash ../../scripts/getRandomVersion.sh
|
||||||
git add .
|
git add .
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ else
|
|||||||
fi
|
fi
|
||||||
rm -rf Readme.md
|
rm -rf Readme.md
|
||||||
cd ../..
|
cd ../..
|
||||||
yarn createBundle:ios
|
bun createBundle:ios
|
||||||
cd ios/App-Bundles
|
cd ios/App-Bundles
|
||||||
bash ../../scripts/getRandomVersion.sh
|
bash ../../scripts/getRandomVersion.sh
|
||||||
git add .
|
git add .
|
||||||
|
|||||||
14
scripts/sendDiscordMessage.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL
|
||||||
|
|
||||||
|
async function sendDiscordMessage(message) {
|
||||||
|
const res = await fetch(WEBHOOK_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ content: message }),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Sent:', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = process.argv.slice(2).join(' ')
|
||||||
|
sendDiscordMessage(msg)
|
||||||
@@ -3,8 +3,6 @@ import { getModel, getUniqueIdSync } from 'react-native-device-info'
|
|||||||
import { name, version } from '../../package.json'
|
import { name, version } from '../../package.json'
|
||||||
import { capitalize } from 'lodash'
|
import { capitalize } from 'lodash'
|
||||||
|
|
||||||
console.debug(`Building Jellyfin Info`)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client object that represents Jellify on the Jellyfin server.
|
* Client object that represents Jellify on the Jellyfin server.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,35 +2,39 @@ import { AxiosResponse } from 'axios'
|
|||||||
import { JellyfinCredentials } from '../../types/jellyfin-credentials'
|
import { JellyfinCredentials } from '../../types/jellyfin-credentials'
|
||||||
import { AuthenticationResult } from '@jellyfin/sdk/lib/generated-client'
|
import { AuthenticationResult } from '@jellyfin/sdk/lib/generated-client'
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import { JellifyUser } from '../../../types/JellifyUser'
|
import { JellifyUser } from '../../../types/JellifyUser'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
|
import { getUserApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
|
import { useApi, useJellifyUser } from '../../../stores'
|
||||||
|
|
||||||
interface AuthenticateUserByNameMutation {
|
interface AuthenticateUserByNameMutation {
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
onError?: () => void
|
onError?: (error: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNameMutation) => {
|
const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNameMutation) => {
|
||||||
const { api, setUser } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user, setUser] = useJellifyUser()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (credentials: JellyfinCredentials) => {
|
mutationFn: async (credentials: JellyfinCredentials) => {
|
||||||
return await api!.authenticateUserByName(credentials.username, credentials.password)
|
return await getUserApi(api!).authenticateUserByName({
|
||||||
|
authenticateUserByName: {
|
||||||
|
Username: credentials.username,
|
||||||
|
Pw: credentials.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onSuccess: async (authResult: AxiosResponse<AuthenticationResult>) => {
|
onSuccess: async (authResult: AxiosResponse<AuthenticationResult>) => {
|
||||||
console.log(`Received auth response from server`)
|
|
||||||
if (isUndefined(authResult))
|
if (isUndefined(authResult))
|
||||||
return Promise.reject(new Error('Authentication result was empty'))
|
return Promise.reject(new Error('Authentication result was empty'))
|
||||||
|
|
||||||
if (authResult.status >= 400 || isUndefined(authResult.data.AccessToken))
|
if (authResult.status == 400 || isUndefined(authResult.data.AccessToken))
|
||||||
return Promise.reject(new Error('Invalid credentials'))
|
return Promise.reject(new Error('Invalid credentials'))
|
||||||
|
|
||||||
if (isUndefined(authResult.data.User))
|
if (isUndefined(authResult.data.User))
|
||||||
return Promise.reject(new Error('Unable to login'))
|
return Promise.reject(new Error('Unable to login'))
|
||||||
|
|
||||||
console.log(`Successfully signed in to server`)
|
|
||||||
|
|
||||||
const user: JellifyUser = {
|
const user: JellifyUser = {
|
||||||
id: authResult.data.User!.Id!,
|
id: authResult.data.User!.Id!,
|
||||||
name: authResult.data.User!.Name!,
|
name: authResult.data.User!.Name!,
|
||||||
@@ -44,7 +48,7 @@ const useAuthenticateUserByName = ({ onSuccess, onError }: AuthenticateUserByNam
|
|||||||
onError: async (error: Error) => {
|
onError: async (error: Error) => {
|
||||||
console.error('An error occurred connecting to the Jellyfin instance', error)
|
console.error('An error occurred connecting to the Jellyfin instance', error)
|
||||||
|
|
||||||
if (onError) onError()
|
if (onError) onError(error)
|
||||||
},
|
},
|
||||||
retry: 0,
|
retry: 0,
|
||||||
gcTime: 0,
|
gcTime: 0,
|
||||||
|
|||||||
23
src/api/mutations/discover/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { useRecentlyAddedAlbums } from '../../queries/album'
|
||||||
|
import { usePublicPlaylists } from '../../queries/playlist'
|
||||||
|
import { useDiscoverArtists } from '../../queries/suggestions'
|
||||||
|
|
||||||
|
const useDiscoverQueries = () => {
|
||||||
|
const { refetch: refetchRecentlyAdded } = useRecentlyAddedAlbums()
|
||||||
|
|
||||||
|
const { refetch: refetchPublicPlaylists } = usePublicPlaylists()
|
||||||
|
|
||||||
|
const { refetch: refetchArtistSuggestions } = useDiscoverArtists()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () =>
|
||||||
|
await Promise.allSettled([
|
||||||
|
refetchRecentlyAdded(),
|
||||||
|
refetchPublicPlaylists(),
|
||||||
|
refetchArtistSuggestions(),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDiscoverQueries
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import { useDownloadingDeviceProfile } from '../../../stores/device-profile'
|
import { useDownloadingDeviceProfile } from '../../../stores/device-profile'
|
||||||
import { UseMutateFunction, useMutation } from '@tanstack/react-query'
|
import { UseMutateFunction, useMutation } from '@tanstack/react-query'
|
||||||
import { mapDtoToTrack } from '../../../utils/mappings'
|
import { mapDtoToTrack } from '../../../utils/mappings'
|
||||||
@@ -7,12 +6,13 @@ import { deleteAudio, saveAudio } from './offlineModeUtils'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { JellifyDownloadProgress } from '../../../types/JellifyDownload'
|
import { JellifyDownloadProgress } from '../../../types/JellifyDownload'
|
||||||
import { useAllDownloadedTracks } from '../../queries/download'
|
import { useAllDownloadedTracks } from '../../queries/download'
|
||||||
|
import { useApi } from '../../../stores'
|
||||||
|
|
||||||
export const useDownloadAudioItem: () => [
|
export const useDownloadAudioItem: () => [
|
||||||
JellifyDownloadProgress,
|
JellifyDownloadProgress,
|
||||||
UseMutateFunction<boolean, Error, { item: BaseItemDto; autoCached: boolean }, void>,
|
UseMutateFunction<boolean, Error, { item: BaseItemDto; autoCached: boolean }, void>,
|
||||||
] = () => {
|
] = () => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
const { data: downloadedTracks, refetch } = useAllDownloadedTracks()
|
const { data: downloadedTracks, refetch } = useAllDownloadedTracks()
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const useDownloadAudioItem: () => [
|
|||||||
return [
|
return [
|
||||||
downloadProgress,
|
downloadProgress,
|
||||||
useMutation({
|
useMutation({
|
||||||
onMutate: () => console.debug('Downloading audio track from Jellyfin'),
|
onMutate: () => {},
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
item,
|
item,
|
||||||
autoCached,
|
autoCached,
|
||||||
@@ -40,7 +40,7 @@ export const useDownloadAudioItem: () => [
|
|||||||
)
|
)
|
||||||
return Promise.resolve(false)
|
return Promise.resolve(false)
|
||||||
|
|
||||||
const track = mapDtoToTrack(api, item, downloadedTracks ?? [], deviceProfile)
|
const track = mapDtoToTrack(api, item, deviceProfile)
|
||||||
|
|
||||||
return saveAudio(track, setDownloadProgress, autoCached)
|
return saveAudio(track, setDownloadProgress, autoCached)
|
||||||
},
|
},
|
||||||
@@ -79,8 +79,7 @@ export const useDeleteDownloads = () => {
|
|||||||
},
|
},
|
||||||
onError: (error, itemIds) =>
|
onError: (error, itemIds) =>
|
||||||
console.error(`Unable to delete ${itemIds.length} downloads`, error),
|
console.error(`Unable to delete ${itemIds.length} downloads`, error),
|
||||||
onSuccess: (_, itemIds) =>
|
onSuccess: (_, itemIds) => {},
|
||||||
console.debug(`Successfully deleted ${itemIds.length} downloads`),
|
|
||||||
onSettled: () => refetch(),
|
onSettled: () => refetch(),
|
||||||
}).mutate
|
}).mutate
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,28 +11,69 @@ import {
|
|||||||
import { queryClient } from '../../../constants/query-client'
|
import { queryClient } from '../../../constants/query-client'
|
||||||
import { AUDIO_CACHE_QUERY } from '../../queries/download/constants'
|
import { AUDIO_CACHE_QUERY } from '../../queries/download/constants'
|
||||||
|
|
||||||
|
type DownloadedFileInfo = {
|
||||||
|
uri: string
|
||||||
|
path: string
|
||||||
|
fileName: string
|
||||||
|
size: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExtensionFromUrl = (url: string): string | null => {
|
||||||
|
const sanitized = url.split('?')[0]
|
||||||
|
const lastSegment = sanitized.split('/').pop() ?? ''
|
||||||
|
const match = lastSegment.match(/\.([a-zA-Z0-9]+)$/)
|
||||||
|
return match?.[1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeExtension = (ext: string | undefined | null) => {
|
||||||
|
if (!ext) return null
|
||||||
|
const clean = ext.toLowerCase()
|
||||||
|
return clean === 'mpeg' ? 'mp3' : clean
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensionFromContentType = (contentType: string | undefined): string | null => {
|
||||||
|
if (!contentType) return null
|
||||||
|
if (!contentType.includes('/')) return null
|
||||||
|
const [, subtypeRaw] = contentType.split('/')
|
||||||
|
const container = subtypeRaw.split(';')[0]
|
||||||
|
return normalizeExtension(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeleteDownloadsResult = {
|
||||||
|
deletedCount: number
|
||||||
|
freedBytes: number
|
||||||
|
failedCount: number
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadJellyfinFile(
|
export async function downloadJellyfinFile(
|
||||||
url: string,
|
url: string,
|
||||||
name: string,
|
name: string,
|
||||||
songName: string,
|
songName: string,
|
||||||
setDownloadProgress: JellifyDownloadProgressState,
|
setDownloadProgress: JellifyDownloadProgressState,
|
||||||
) {
|
preferredExtension?: string | null,
|
||||||
|
): Promise<DownloadedFileInfo> {
|
||||||
try {
|
try {
|
||||||
// Fetch the file
|
const urlExtension = normalizeExtension(getExtensionFromUrl(url))
|
||||||
const headRes = await axios.head(url)
|
const hintedExtension = normalizeExtension(preferredExtension)
|
||||||
const contentType = headRes.headers['content-type']
|
|
||||||
|
|
||||||
// Step 2: Get extension from content-type
|
let extension = urlExtension ?? hintedExtension ?? null
|
||||||
let extension = 'mp3' // default extension
|
|
||||||
if (contentType && contentType.includes('/')) {
|
if (!extension) {
|
||||||
const parts = contentType.split('/')
|
try {
|
||||||
const container = parts[1].split(';')[0] // handles "audio/m4a; charset=utf-8"
|
const headRes = await axios.head(url)
|
||||||
if (container !== 'mpeg') {
|
const headExtension = extensionFromContentType(headRes.headers['content-type'])
|
||||||
extension = container // don't use mpeg as an extension, use the default extension
|
if (headExtension) extension = headExtension
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
'HEAD request failed when determining download type, using default',
|
||||||
|
error,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Build path
|
if (!extension) extension = 'bin' // fallback without assuming a specific codec
|
||||||
|
|
||||||
|
// Build path
|
||||||
const fileName = `${name}.${extension}`
|
const fileName = `${name}.${extension}`
|
||||||
const downloadDest = `${RNFS.DocumentDirectoryPath}/${fileName}`
|
const downloadDest = `${RNFS.DocumentDirectoryPath}/${fileName}`
|
||||||
|
|
||||||
@@ -47,9 +88,7 @@ export async function downloadJellyfinFile(
|
|||||||
toFile: downloadDest,
|
toFile: downloadDest,
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
begin: (res: any) => {
|
begin: (res: any) => {},
|
||||||
console.log('Download started')
|
|
||||||
},
|
|
||||||
progress: (data: any) => {
|
progress: (data: any) => {
|
||||||
const percent = +(data.bytesWritten / data.contentLength).toFixed(2)
|
const percent = +(data.bytesWritten / data.contentLength).toFixed(2)
|
||||||
|
|
||||||
@@ -63,9 +102,15 @@ export async function downloadJellyfinFile(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await RNFS.downloadFile(options).promise
|
const result = await RNFS.downloadFile(options).promise
|
||||||
console.log('Download complete:', result)
|
|
||||||
|
|
||||||
return `file://${downloadDest}`
|
const metadata = await RNFS.stat(downloadDest)
|
||||||
|
|
||||||
|
return {
|
||||||
|
uri: `file://${downloadDest}`,
|
||||||
|
path: downloadDest,
|
||||||
|
fileName,
|
||||||
|
size: Number(metadata.size),
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Download failed:', error)
|
console.error('Download failed:', error)
|
||||||
throw error
|
throw error
|
||||||
@@ -116,44 +161,43 @@ export const saveAudio = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.debug('Downloading audio')
|
const downloadedTrackFile = await downloadJellyfinFile(
|
||||||
|
|
||||||
const downloadtrack = await downloadJellyfinFile(
|
|
||||||
track.url,
|
track.url,
|
||||||
track.item.Id as string,
|
track.item.Id as string,
|
||||||
track.title as string,
|
track.title as string,
|
||||||
setDownloadProgress,
|
setDownloadProgress,
|
||||||
|
track.mediaSourceInfo?.Container,
|
||||||
)
|
)
|
||||||
const dowloadalbum = await downloadJellyfinFile(
|
let downloadedArtworkFile: DownloadedFileInfo | undefined
|
||||||
track.artwork as string,
|
if (track.artwork) {
|
||||||
track.item.Id as string,
|
downloadedArtworkFile = await downloadJellyfinFile(
|
||||||
track.title as string,
|
track.artwork as string,
|
||||||
setDownloadProgress,
|
track.item.Id as string,
|
||||||
)
|
track.title as string,
|
||||||
console.log('downloadtrack', downloadtrack)
|
setDownloadProgress,
|
||||||
if (downloadtrack) {
|
undefined,
|
||||||
track.url = downloadtrack
|
)
|
||||||
track.artwork = dowloadalbum
|
|
||||||
}
|
}
|
||||||
|
track.url = downloadedTrackFile.uri
|
||||||
|
if (downloadedArtworkFile) track.artwork = downloadedArtworkFile.uri
|
||||||
|
|
||||||
const index = existingArray.findIndex((t) => t.item.Id === track.item.Id)
|
const index = existingArray.findIndex((t) => t.item.Id === track.item.Id)
|
||||||
|
|
||||||
|
const downloadEntry: JellifyDownload = {
|
||||||
|
...track,
|
||||||
|
savedAt: new Date().toISOString(),
|
||||||
|
isAutoDownloaded,
|
||||||
|
path: downloadedTrackFile.uri,
|
||||||
|
fileSizeBytes: downloadedTrackFile.size,
|
||||||
|
artworkSizeBytes: downloadedArtworkFile?.size,
|
||||||
|
}
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
// Replace existing
|
// Replace existing
|
||||||
existingArray[index] = {
|
existingArray[index] = downloadEntry
|
||||||
...track,
|
|
||||||
savedAt: new Date().toISOString(),
|
|
||||||
isAutoDownloaded,
|
|
||||||
path: downloadtrack,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Add new
|
// Add new
|
||||||
existingArray.push({
|
existingArray.push(downloadEntry)
|
||||||
...track,
|
|
||||||
savedAt: new Date().toISOString(),
|
|
||||||
isAutoDownloaded,
|
|
||||||
path: downloadtrack,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false
|
return false
|
||||||
@@ -164,17 +208,8 @@ export const saveAudio = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const deleteAudio = async (itemId: string | undefined | null) => {
|
export const deleteAudio = async (itemId: string | undefined | null) => {
|
||||||
const downloads = getAudioCache()
|
if (!itemId) return
|
||||||
|
await deleteDownloadsByIds([itemId])
|
||||||
const download = downloads.filter((download) => download.item.Id === itemId)
|
|
||||||
|
|
||||||
if (download.length === 1) {
|
|
||||||
RNFS.unlink(`${RNFS.DocumentDirectoryPath}/${download[0].item.Id}`)
|
|
||||||
setAudioCache([
|
|
||||||
...downloads.slice(0, downloads.indexOf(download[0])),
|
|
||||||
...downloads.slice(downloads.indexOf(download[0]) + 1, downloads.length - 1),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setAudioCache = (downloads: JellifyDownload[]) => {
|
const setAudioCache = (downloads: JellifyDownload[]) => {
|
||||||
@@ -194,8 +229,88 @@ export const getAudioCache = (): JellifyDownload[] => {
|
|||||||
return existingArray
|
return existingArray
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deleteAudioCache = async () => {
|
const stripFileScheme = (path: string) => path.replace('file://', '')
|
||||||
|
|
||||||
|
const isLocalFile = (path: string) =>
|
||||||
|
path.startsWith('file://') || path.startsWith(RNFS.DocumentDirectoryPath)
|
||||||
|
|
||||||
|
const deleteLocalFileIfExists = async (
|
||||||
|
path: string | undefined,
|
||||||
|
fallbackSize?: number,
|
||||||
|
): Promise<number> => {
|
||||||
|
if (!path || !isLocalFile(path)) return 0
|
||||||
|
|
||||||
|
const normalizedPath = stripFileScheme(path)
|
||||||
|
try {
|
||||||
|
const exists = await RNFS.exists(normalizedPath)
|
||||||
|
let size = fallbackSize ?? 0
|
||||||
|
if (exists && !fallbackSize) {
|
||||||
|
const stat = await RNFS.stat(normalizedPath)
|
||||||
|
size = Number(stat.size)
|
||||||
|
}
|
||||||
|
if (exists) await RNFS.unlink(normalizedPath)
|
||||||
|
return size
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to delete file', normalizedPath, error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDownloadAssets = async (download: JellifyDownload): Promise<number> => {
|
||||||
|
let freedBytes = 0
|
||||||
|
freedBytes += await deleteLocalFileIfExists(download.path, download.fileSizeBytes)
|
||||||
|
freedBytes += await deleteLocalFileIfExists(download.artwork, download.artworkSizeBytes)
|
||||||
|
return freedBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDownloadsByIds = async (
|
||||||
|
itemIds: (string | null | undefined)[],
|
||||||
|
): Promise<DeleteDownloadsResult> => {
|
||||||
|
const targets = new Set(itemIds.filter(Boolean) as string[])
|
||||||
|
if (targets.size === 0)
|
||||||
|
return {
|
||||||
|
deletedCount: 0,
|
||||||
|
failedCount: 0,
|
||||||
|
freedBytes: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloads = getAudioCache()
|
||||||
|
const remaining: JellifyDownload[] = []
|
||||||
|
let freedBytes = 0
|
||||||
|
let deletedCount = 0
|
||||||
|
let failedCount = 0
|
||||||
|
|
||||||
|
for (const download of downloads) {
|
||||||
|
if (!targets.has(download.item.Id as string)) {
|
||||||
|
remaining.push(download)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
freedBytes += await deleteDownloadAssets(download)
|
||||||
|
deletedCount += 1
|
||||||
|
} catch (error) {
|
||||||
|
failedCount += 1
|
||||||
|
remaining.push(download)
|
||||||
|
console.error('Failed to delete download', download.item.Id, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAudioCache(remaining)
|
||||||
|
queryClient.invalidateQueries(AUDIO_CACHE_QUERY)
|
||||||
|
|
||||||
|
return {
|
||||||
|
deletedCount,
|
||||||
|
failedCount,
|
||||||
|
freedBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAudioCache = async (): Promise<DeleteDownloadsResult> => {
|
||||||
|
const downloads = getAudioCache()
|
||||||
|
const result = await deleteDownloadsByIds(downloads.map((download) => download.item.Id))
|
||||||
mmkv.delete(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
mmkv.delete(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const purneAudioCache = async () => {
|
export const purneAudioCache = async () => {
|
||||||
@@ -220,17 +335,7 @@ export const purneAudioCache = async () => {
|
|||||||
// Remove the oldest `excess` files
|
// Remove the oldest `excess` files
|
||||||
const itemsToDelete = autoDownloads.slice(0, excess)
|
const itemsToDelete = autoDownloads.slice(0, excess)
|
||||||
for (const item of itemsToDelete) {
|
for (const item of itemsToDelete) {
|
||||||
// Delete audio file
|
await deleteDownloadAssets(item)
|
||||||
if (item.url && (await RNFS.exists(item.url))) {
|
|
||||||
await RNFS.unlink(item.url).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete artwork
|
|
||||||
if (item.artwork && (await RNFS.exists(item.artwork))) {
|
|
||||||
await RNFS.unlink(item.artwork).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from the existingArray
|
|
||||||
existingArray = existingArray.filter((i) => i.item.Id !== item.item.Id)
|
existingArray = existingArray.filter((i) => i.item.Id !== item.item.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default async function saveAudioItem(
|
|||||||
if (downloadedTracks?.filter((download) => download.item.Id === item.Id).length ?? 0 > 0)
|
if (downloadedTracks?.filter((download) => download.item.Id === item.Id).length ?? 0 > 0)
|
||||||
return Promise.resolve(false)
|
return Promise.resolve(false)
|
||||||
|
|
||||||
const track = mapDtoToTrack(api, item, downloadedTracks ?? [], deviceProfile)
|
const track = mapDtoToTrack(api, item, deviceProfile)
|
||||||
|
|
||||||
// TODO: fix download progresses
|
// TODO: fix download progresses
|
||||||
return saveAudio(track, () => {}, autoCached)
|
return saveAudio(track, () => {}, autoCached)
|
||||||
|
|||||||
96
src/api/mutations/favorite/index.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { queryClient } from '../../../constants/query-client'
|
||||||
|
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
|
||||||
|
import { BaseItemDto, UserItemDataDto } from '@jellyfin/sdk/lib/generated-client'
|
||||||
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { isUndefined } from 'lodash'
|
||||||
|
import Toast from 'react-native-toast-message'
|
||||||
|
import UserDataQueryKey from '../../queries/user-data/keys'
|
||||||
|
import { useApi, useJellifyUser } from '../../../../src/stores'
|
||||||
|
|
||||||
|
interface SetFavoriteMutation {
|
||||||
|
item: BaseItemDto
|
||||||
|
onToggle?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddFavorite = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
|
||||||
|
const trigger = useHapticFeedback()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ item }: SetFavoriteMutation) => {
|
||||||
|
if (isUndefined(api)) Promise.reject('API instance not defined')
|
||||||
|
else if (isUndefined(item.Id)) Promise.reject('Item ID is undefined')
|
||||||
|
else
|
||||||
|
return await getUserLibraryApi(api).markFavoriteItem({
|
||||||
|
itemId: item.Id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: (data, { item, onToggle }) => {
|
||||||
|
trigger('notificationSuccess')
|
||||||
|
|
||||||
|
if (onToggle) onToggle()
|
||||||
|
|
||||||
|
if (user)
|
||||||
|
queryClient.setQueryData(UserDataQueryKey(user, item), (prev: UserItemDataDto) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
IsFavorite: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error, variables) => {
|
||||||
|
console.error('Unable to set favorite for item', error)
|
||||||
|
|
||||||
|
trigger('notificationError')
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
text1: 'Failed to add favorite',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRemoveFavorite = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
|
||||||
|
const trigger = useHapticFeedback()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ item }: SetFavoriteMutation) => {
|
||||||
|
if (isUndefined(api)) Promise.reject('API instance not defined')
|
||||||
|
else if (isUndefined(item.Id)) Promise.reject('Item ID is undefined')
|
||||||
|
else
|
||||||
|
return await getUserLibraryApi(api).unmarkFavoriteItem({
|
||||||
|
itemId: item.Id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: (data, { item, onToggle }) => {
|
||||||
|
trigger('notificationSuccess')
|
||||||
|
|
||||||
|
if (onToggle) onToggle()
|
||||||
|
|
||||||
|
if (user)
|
||||||
|
queryClient.setQueryData(UserDataQueryKey(user, item), (prev: UserItemDataDto) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
IsFavorite: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError: (error, variables) => {
|
||||||
|
console.error('Unable to remove favorite for item', error)
|
||||||
|
|
||||||
|
trigger('notificationError')
|
||||||
|
|
||||||
|
Toast.show({
|
||||||
|
text1: 'Failed to remove favorite',
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
30
src/api/mutations/home/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useMutation } from '@tanstack/react-query'
|
||||||
|
import { useFrequentlyPlayedArtists, useFrequentlyPlayedTracks } from '../../queries/frequents'
|
||||||
|
import { useRecentArtists, useRecentlyPlayedTracks } from '../../queries/recents'
|
||||||
|
import { useUserPlaylists } from '../../queries/playlist'
|
||||||
|
|
||||||
|
const useHomeQueries = () => {
|
||||||
|
const { refetch: refetchUserPlaylists } = useUserPlaylists()
|
||||||
|
|
||||||
|
const { refetch: refetchRecentArtists } = useRecentArtists()
|
||||||
|
|
||||||
|
const { refetch: refetchRecentlyPlayed } = useRecentlyPlayedTracks()
|
||||||
|
|
||||||
|
const { refetch: refetchFrequentArtists } = useFrequentlyPlayedArtists()
|
||||||
|
|
||||||
|
const { refetch: refetchFrequentlyPlayed } = useFrequentlyPlayedTracks()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await Promise.allSettled([
|
||||||
|
refetchRecentlyPlayed(),
|
||||||
|
refetchFrequentlyPlayed(),
|
||||||
|
refetchUserPlaylists(),
|
||||||
|
])
|
||||||
|
await Promise.allSettled([refetchFrequentArtists(), refetchRecentArtists()])
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useHomeQueries
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import JellifyTrack from '../../../types/JellifyTrack'
|
import JellifyTrack from '../../../types/JellifyTrack'
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import reportPlaybackCompleted from './functions/playback-completed'
|
import reportPlaybackCompleted from './functions/playback-completed'
|
||||||
@@ -6,19 +5,20 @@ import reportPlaybackStopped from './functions/playback-stopped'
|
|||||||
import isPlaybackFinished from './utils'
|
import isPlaybackFinished from './utils'
|
||||||
import reportPlaybackProgress from './functions/playback-progress'
|
import reportPlaybackProgress from './functions/playback-progress'
|
||||||
import reportPlaybackStarted from './functions/playback-started'
|
import reportPlaybackStarted from './functions/playback-started'
|
||||||
|
import { useApi } from '../../../stores'
|
||||||
|
|
||||||
interface PlaybackStartedMutation {
|
interface PlaybackStartedMutation {
|
||||||
track: JellifyTrack
|
track: JellifyTrack
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useReportPlaybackStarted = () => {
|
export const useReportPlaybackStarted = () => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
onMutate: () => {},
|
onMutate: () => {},
|
||||||
mutationFn: async ({ track }: PlaybackStartedMutation) => reportPlaybackStarted(api, track),
|
mutationFn: async ({ track }: PlaybackStartedMutation) => reportPlaybackStarted(api, track),
|
||||||
onError: (error) => console.error(`Reporting playback started failed`, error),
|
onError: (error) => console.error(`Reporting playback started failed`, error),
|
||||||
onSuccess: () => console.debug(`Reported playback started`),
|
onSuccess: () => {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,13 +29,10 @@ interface PlaybackStoppedMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useReportPlaybackStopped = () => {
|
export const useReportPlaybackStopped = () => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
onMutate: ({ lastPosition, duration }) =>
|
onMutate: ({ lastPosition, duration }) => {},
|
||||||
console.debug(
|
|
||||||
`Reporting playback ${isPlaybackFinished(lastPosition, duration) ? 'completed' : 'stopped'} for track`,
|
|
||||||
),
|
|
||||||
mutationFn: async ({ track, lastPosition, duration }: PlaybackStoppedMutation) => {
|
mutationFn: async ({ track, lastPosition, duration }: PlaybackStoppedMutation) => {
|
||||||
return isPlaybackFinished(lastPosition, duration)
|
return isPlaybackFinished(lastPosition, duration)
|
||||||
? await reportPlaybackCompleted(api, track)
|
? await reportPlaybackCompleted(api, track)
|
||||||
@@ -46,10 +43,7 @@ export const useReportPlaybackStopped = () => {
|
|||||||
`Reporting playback ${isPlaybackFinished(lastPosition, duration) ? 'completed' : 'stopped'} failed`,
|
`Reporting playback ${isPlaybackFinished(lastPosition, duration) ? 'completed' : 'stopped'} failed`,
|
||||||
error,
|
error,
|
||||||
),
|
),
|
||||||
onSuccess: (_, { lastPosition, duration }) =>
|
onSuccess: (_, { lastPosition, duration }) => {},
|
||||||
console.debug(
|
|
||||||
`Reported playback ${isPlaybackFinished(lastPosition, duration) ? 'completed' : 'stopped'} successfully`,
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +53,10 @@ interface PlaybackProgressMutation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useReportPlaybackProgress = () => {
|
export const useReportPlaybackProgress = () => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
onMutate: ({ position }) => console.debug(`Reporting progress at ${position}`),
|
onMutate: ({ position }) => {},
|
||||||
mutationFn: async ({ track, position }: PlaybackProgressMutation) =>
|
mutationFn: async ({ track, position }: PlaybackProgressMutation) =>
|
||||||
reportPlaybackProgress(api, track, position),
|
reportPlaybackProgress(api, track, position),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -19,17 +19,11 @@ export async function addToPlaylist(
|
|||||||
track: BaseItemDto,
|
track: BaseItemDto,
|
||||||
playlist: BaseItemDto,
|
playlist: BaseItemDto,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.debug('Adding track to playlist')
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
if (isUndefined(user)) return reject(new Error('No user available'))
|
if (isUndefined(user)) return reject(new Error('No user available'))
|
||||||
|
|
||||||
console.debug(api)
|
|
||||||
|
|
||||||
console.debug(api.axiosInstance)
|
|
||||||
|
|
||||||
getPlaylistsApi(api)
|
getPlaylistsApi(api)
|
||||||
.addItemToPlaylist(
|
.addItemToPlaylist(
|
||||||
{
|
{
|
||||||
@@ -65,8 +59,6 @@ export async function addManyToPlaylist(
|
|||||||
tracks: BaseItemDto[],
|
tracks: BaseItemDto[],
|
||||||
playlist: BaseItemDto,
|
playlist: BaseItemDto,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.debug(`Adding ${tracks.length} tracks to playlist`)
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
@@ -110,8 +102,6 @@ export async function removeFromPlaylist(
|
|||||||
track: BaseItemDto,
|
track: BaseItemDto,
|
||||||
playlist: BaseItemDto,
|
playlist: BaseItemDto,
|
||||||
) {
|
) {
|
||||||
console.debug('Removing track from playlist')
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
@@ -145,8 +135,6 @@ export async function reorderPlaylist(
|
|||||||
itemId: string,
|
itemId: string,
|
||||||
to: number,
|
to: number,
|
||||||
) {
|
) {
|
||||||
console.debug(`Moving track to index ${to}`)
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
@@ -179,8 +167,6 @@ export async function createPlaylist(
|
|||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
name: string,
|
name: string,
|
||||||
) {
|
) {
|
||||||
console.debug('Creating new playlist...')
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
@@ -214,8 +200,6 @@ export async function createPlaylist(
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export async function deletePlaylist(api: Api | undefined, playlistId: string) {
|
export async function deletePlaylist(api: Api | undefined, playlistId: string) {
|
||||||
console.debug('Deleting playlist...')
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
@@ -248,8 +232,7 @@ export async function updatePlaylist(
|
|||||||
name: string,
|
name: string,
|
||||||
trackIds: string[],
|
trackIds: string[],
|
||||||
) {
|
) {
|
||||||
console.debug('Updating playlist')
|
console.info('Updating playlist with name:', name, 'and track IDs:', trackIds)
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('No API client available'))
|
if (isUndefined(api)) return reject(new Error('No API client available'))
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connectToServer } from './utils'
|
|||||||
import { JellifyServer } from '@/src/types/JellifyServer'
|
import { JellifyServer } from '@/src/types/JellifyServer'
|
||||||
import serverAddressContainsProtocol from './utils/parsing'
|
import serverAddressContainsProtocol from './utils/parsing'
|
||||||
import HTTPS, { HTTP } from '../../../constants/protocols'
|
import HTTPS, { HTTP } from '../../../constants/protocols'
|
||||||
import { useJellifyContext } from '../../../providers'
|
import useJellifyStore from '../../../stores'
|
||||||
|
|
||||||
interface PublicSystemInfoMutation {
|
interface PublicSystemInfoMutation {
|
||||||
serverAddress: string
|
serverAddress: string
|
||||||
@@ -16,14 +16,12 @@ interface PublicSystemInfoHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const usePublicSystemInfo = ({ onSuccess, onError }: PublicSystemInfoHook) => {
|
const usePublicSystemInfo = ({ onSuccess, onError }: PublicSystemInfoHook) => {
|
||||||
const { setServer } = useJellifyContext()
|
const setServer = useJellifyStore((state) => state.setServer)
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ serverAddress, useHttps }: PublicSystemInfoMutation) =>
|
mutationFn: ({ serverAddress, useHttps }: PublicSystemInfoMutation) =>
|
||||||
connectToServer(serverAddress!, useHttps),
|
connectToServer(serverAddress!, useHttps),
|
||||||
onSuccess: ({ publicSystemInfoResponse, connectionType }, { serverAddress, useHttps }) => {
|
onSuccess: ({ publicSystemInfoResponse, connectionType }, { serverAddress, useHttps }) => {
|
||||||
console.debug(`Got public system info response`)
|
|
||||||
|
|
||||||
if (!publicSystemInfoResponse.Version)
|
if (!publicSystemInfoResponse.Version)
|
||||||
throw new Error(`Jellyfin instance did not respond`)
|
throw new Error(`Jellyfin instance did not respond`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation } from '@tanstack/react-query'
|
||||||
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
import useStreamingDeviceProfile from '../../../stores/device-profile'
|
||||||
import { Api } from '@jellyfin/sdk'
|
|
||||||
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api'
|
import { getSessionApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
import { MONOCHROME_ICON_URL } from '../../../configs/config'
|
import { MONOCHROME_ICON_URL } from '../../../configs/config'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useApi } from '../../../stores'
|
||||||
|
|
||||||
const usePostFullCapabilities = () => {
|
const usePostFullCapabilities = () => {
|
||||||
|
const api = useApi()
|
||||||
const streamingDeviceProfile = useStreamingDeviceProfile()
|
const streamingDeviceProfile = useStreamingDeviceProfile()
|
||||||
|
|
||||||
return useMutation({
|
const { mutate } = useMutation({
|
||||||
mutationFn: async (api: Api | undefined) => {
|
onMutate: () => {},
|
||||||
|
mutationFn: async () => {
|
||||||
if (!api) return
|
if (!api) return
|
||||||
|
|
||||||
return getSessionApi(api).postFullCapabilities({
|
return await getSessionApi(api).postFullCapabilities({
|
||||||
clientCapabilitiesDto: {
|
clientCapabilitiesDto: {
|
||||||
IconUrl: MONOCHROME_ICON_URL,
|
IconUrl: MONOCHROME_ICON_URL,
|
||||||
DeviceProfile: streamingDeviceProfile,
|
DeviceProfile: streamingDeviceProfile,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
onSuccess: () => console.info('Successfully posted player capabilities'),
|
||||||
|
onError: (error) => console.error('Unable to post player capabilities', error),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mutate()
|
||||||
|
}, [streamingDeviceProfile.Id])
|
||||||
}
|
}
|
||||||
|
|
||||||
export default usePostFullCapabilities
|
export default usePostFullCapabilities
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
|
||||||
import { QueryKeys } from '../../../enums/query-keys'
|
import { QueryKeys } from '../../../enums/query-keys'
|
||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
|
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'
|
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client/models/item-sort-by'
|
||||||
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'
|
import { SortOrder } from '@jellyfin/sdk/lib/generated-client/models/sort-order'
|
||||||
@@ -8,17 +6,21 @@ import { fetchAlbums } from './utils/album'
|
|||||||
import { RefObject, useCallback, useRef } from 'react'
|
import { RefObject, useCallback, useRef } from 'react'
|
||||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||||
import { ApiLimits } from '../query.config'
|
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||||
import { fetchRecentlyAdded } from '../recents/utils'
|
import { fetchRecentlyAdded } from '../recents/utils'
|
||||||
import { queryClient } from '../../../constants/query-client'
|
import { queryClient } from '../../../constants/query-client'
|
||||||
|
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||||
|
import useLibraryStore from '../../../stores/library'
|
||||||
|
|
||||||
const useAlbums: () => [
|
const useAlbums: () => [
|
||||||
RefObject<Set<string>>,
|
RefObject<Set<string>>,
|
||||||
UseInfiniteQueryResult<(string | number | BaseItemDto)[]>,
|
UseInfiniteQueryResult<(string | number | BaseItemDto)[]>,
|
||||||
] = () => {
|
] = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()
|
const isFavorites = useLibraryStore((state) => state.isFavorites)
|
||||||
|
|
||||||
const albumPageParams = useRef<Set<string>>(new Set<string>())
|
const albumPageParams = useRef<Set<string>>(new Set<string>())
|
||||||
|
|
||||||
@@ -43,10 +45,11 @@ const useAlbums: () => [
|
|||||||
),
|
),
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
select: selectAlbums,
|
select: selectAlbums,
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
maxPages: MaxPages.Library,
|
||||||
|
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
||||||
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
||||||
},
|
},
|
||||||
getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) => {
|
getPreviousPageParam: (firstPage, allPages, firstPageParam) => {
|
||||||
return firstPageParam === 0 ? null : firstPageParam - 1
|
return firstPageParam === 0 ? null : firstPageParam - 1
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -57,20 +60,21 @@ const useAlbums: () => [
|
|||||||
export default useAlbums
|
export default useAlbums
|
||||||
|
|
||||||
export const useRecentlyAddedAlbums = () => {
|
export const useRecentlyAddedAlbums = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId],
|
||||||
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam),
|
||||||
select: (data) => data.pages.flatMap((page) => page),
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
getNextPageParam: (lastPage, allPages, lastPageParam) =>
|
||||||
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRefetchRecentlyAdded: () => () => void = () => {
|
export const useRefetchRecentlyAdded: () => () => void = () => {
|
||||||
const { library } = useJellifyContext()
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { Api } from '@jellyfin/sdk'
|
|||||||
import { fetchItem, fetchItems } from '../../item'
|
import { fetchItem, fetchItems } from '../../item'
|
||||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
import { ApiLimits } from '../../query.config'
|
import { ApiLimits } from '../../../../configs/query.config'
|
||||||
|
import { nitroFetch } from '../../../utils/nitro'
|
||||||
export function fetchAlbums(
|
export function fetchAlbums(
|
||||||
api: Api | undefined,
|
api: Api | undefined,
|
||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
@@ -20,31 +21,26 @@ export function fetchAlbums(
|
|||||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching albums', page)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!api) return reject('No API instance provided')
|
if (!api) return reject('No API instance provided')
|
||||||
if (!user) return reject('No user provided')
|
if (!user) return reject('No user provided')
|
||||||
if (!library) return reject('Library has not been set')
|
if (!library) return reject('Library has not been set')
|
||||||
|
|
||||||
getItemsApi(api)
|
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||||
.getItems({
|
ParentId: library.musicLibraryId,
|
||||||
parentId: library.musicLibraryId,
|
IncludeItemTypes: [BaseItemKind.MusicAlbum],
|
||||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
UserId: user.id,
|
||||||
userId: user.id,
|
EnableUserData: true, // This will populate the user data query later down the line
|
||||||
enableUserData: true, // This will populate the user data query later down the line
|
SortBy: sortBy,
|
||||||
sortBy,
|
SortOrder: sortOrder,
|
||||||
sortOrder,
|
StartIndex: page * ApiLimits.Library,
|
||||||
startIndex: page * ApiLimits.Library,
|
Limit: ApiLimits.Library,
|
||||||
limit: ApiLimits.Library,
|
IsFavorite: isFavorite,
|
||||||
isFavorite,
|
Fields: [ItemFields.SortName],
|
||||||
fields: [ItemFields.SortName],
|
Recursive: true,
|
||||||
recursive: true,
|
}).then((data) => {
|
||||||
})
|
return data.Items ? resolve(data.Items) : resolve([])
|
||||||
.then(({ data }) => {
|
})
|
||||||
console.debug('Albums Response receieved')
|
|
||||||
return data.Items ? resolve(data.Items) : resolve([])
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import {
|
|||||||
UseInfiniteQueryResult,
|
UseInfiniteQueryResult,
|
||||||
useQuery,
|
useQuery,
|
||||||
} from '@tanstack/react-query'
|
} from '@tanstack/react-query'
|
||||||
import { isString, isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
import { fetchArtistAlbums, fetchArtistFeaturedOn, fetchArtists } from './utils/artist'
|
import { fetchArtistAlbums, fetchArtistFeaturedOn, fetchArtists } from './utils/artist'
|
||||||
import { useJellifyContext } from '../../../providers'
|
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||||
import { ApiLimits } from '../query.config'
|
|
||||||
import { RefObject, useCallback, useRef } from 'react'
|
import { RefObject, useCallback, useRef } from 'react'
|
||||||
import { useLibrarySortAndFilterContext } from '../../../providers/Library'
|
|
||||||
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
import flattenInfiniteQueryPages from '../../../utils/query-selectors'
|
||||||
|
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||||
|
import useLibraryStore from '../../../stores/library'
|
||||||
|
|
||||||
export const useArtistAlbums = (artist: BaseItemDto) => {
|
export const useArtistAlbums = (artist: BaseItemDto) => {
|
||||||
const { api, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [QueryKeys.ArtistAlbums, library?.musicLibraryId, artist.Id],
|
queryKey: [QueryKeys.ArtistAlbums, library?.musicLibraryId, artist.Id],
|
||||||
@@ -25,7 +26,8 @@ export const useArtistAlbums = (artist: BaseItemDto) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useArtistFeaturedOn = (artist: BaseItemDto) => {
|
export const useArtistFeaturedOn = (artist: BaseItemDto) => {
|
||||||
const { api, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [QueryKeys.ArtistFeaturedOn, library?.musicLibraryId, artist.Id],
|
queryKey: [QueryKeys.ArtistFeaturedOn, library?.musicLibraryId, artist.Id],
|
||||||
@@ -38,9 +40,11 @@ export const useAlbumArtists: () => [
|
|||||||
RefObject<Set<string>>,
|
RefObject<Set<string>>,
|
||||||
UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>,
|
UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error>,
|
||||||
] = () => {
|
] = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
const { isFavorites, sortDescending } = useLibrarySortAndFilterContext()
|
const { isFavorites, sortDescending } = useLibraryStore()
|
||||||
|
|
||||||
const artistPageParams = useRef<Set<string>>(new Set<string>())
|
const artistPageParams = useRef<Set<string>>(new Set<string>())
|
||||||
|
|
||||||
@@ -64,6 +68,7 @@ export const useAlbumArtists: () => [
|
|||||||
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
|
[sortDescending ? SortOrder.Descending : SortOrder.Ascending],
|
||||||
),
|
),
|
||||||
select: selectArtists,
|
select: selectArtists,
|
||||||
|
maxPages: MaxPages.Library,
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||||
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||||
import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
import { JellifyUser } from '../../../../types/JellifyUser'
|
import { JellifyUser } from '../../../../types/JellifyUser'
|
||||||
import { ApiLimits } from '../../query.config'
|
import { ApiLimits } from '../../../../configs/query.config'
|
||||||
|
import { nitroFetch } from '../../../utils/nitro'
|
||||||
|
|
||||||
export function fetchArtists(
|
export function fetchArtists(
|
||||||
api: Api | undefined,
|
api: Api | undefined,
|
||||||
@@ -20,28 +21,24 @@ export function fetchArtists(
|
|||||||
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
sortBy: ItemSortBy[] = [ItemSortBy.SortName],
|
||||||
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
sortOrder: SortOrder[] = [SortOrder.Ascending],
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching artists', page)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!api) return reject('No API instance provided')
|
if (!api) return reject('No API instance provided')
|
||||||
if (!user) return reject('No user provided')
|
if (!user) return reject('No user provided')
|
||||||
if (!library) return reject('Library has not been set')
|
if (!library) return reject('Library has not been set')
|
||||||
|
|
||||||
getArtistsApi(api)
|
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Artists/AlbumArtists', {
|
||||||
.getAlbumArtists({
|
ParentId: library.musicLibraryId,
|
||||||
parentId: library.musicLibraryId,
|
UserId: user.id,
|
||||||
userId: user.id,
|
EnableUserData: true,
|
||||||
enableUserData: true, // This will populate the User Data query later down the line
|
SortBy: sortBy,
|
||||||
sortBy: sortBy,
|
SortOrder: sortOrder,
|
||||||
sortOrder: sortOrder,
|
StartIndex: page * ApiLimits.Library,
|
||||||
startIndex: page * ApiLimits.Library,
|
Limit: ApiLimits.Library,
|
||||||
limit: ApiLimits.Library,
|
IsFavorite: isFavorite,
|
||||||
isFavorite: isFavorite,
|
Fields: [ItemFields.SortName, ItemFields.Genres],
|
||||||
fields: [ItemFields.SortName, ItemFields.Genres],
|
})
|
||||||
})
|
.then((data) => {
|
||||||
.then((response) => {
|
return data.Items ? resolve(data.Items) : resolve([])
|
||||||
console.debug('Artists Response received')
|
|
||||||
return response.data.Items ? resolve(response.data.Items) : resolve([])
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error)
|
reject(error)
|
||||||
@@ -60,25 +57,22 @@ export function fetchArtistAlbums(
|
|||||||
libraryId: string | undefined,
|
libraryId: string | undefined,
|
||||||
artist: BaseItemDto,
|
artist: BaseItemDto,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching artist albums')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!api) return reject('No API instance provided')
|
if (!api) return reject('No API instance provided')
|
||||||
if (!libraryId) return reject('Library has not been set')
|
if (!libraryId) return reject('Library has not been set')
|
||||||
|
|
||||||
getItemsApi(api!)
|
nitroFetch<{ Items: BaseItemDto[] }>(api!, '/Items', {
|
||||||
.getItems({
|
ParentId: libraryId,
|
||||||
parentId: libraryId,
|
IncludeItemTypes: [BaseItemKind.MusicAlbum],
|
||||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
Recursive: true,
|
||||||
recursive: true,
|
ExcludeItemIds: [artist.Id!],
|
||||||
excludeItemIds: [artist.Id!],
|
SortBy: [ItemSortBy.PremiereDate, ItemSortBy.ProductionYear, ItemSortBy.SortName],
|
||||||
sortBy: [ItemSortBy.PremiereDate, ItemSortBy.ProductionYear, ItemSortBy.SortName],
|
SortOrder: [SortOrder.Descending],
|
||||||
sortOrder: [SortOrder.Descending],
|
AlbumArtistIds: [artist.Id!],
|
||||||
albumArtistIds: [artist.Id!],
|
Fields: [ItemFields.ChildCount],
|
||||||
fields: [ItemFields.ChildCount],
|
})
|
||||||
})
|
.then((data) => {
|
||||||
.then((response) => {
|
return data.Items ? resolve(data.Items) : resolve([])
|
||||||
return response.data.Items ? resolve(response.data.Items) : resolve([])
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error)
|
reject(error)
|
||||||
@@ -97,24 +91,21 @@ export function fetchArtistFeaturedOn(
|
|||||||
libraryId: string | undefined,
|
libraryId: string | undefined,
|
||||||
artist: BaseItemDto,
|
artist: BaseItemDto,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching artist featured on')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!api) return reject('No API instance provided')
|
if (!api) return reject('No API instance provided')
|
||||||
if (!libraryId) return reject('Library has not been set')
|
if (!libraryId) return reject('Library has not been set')
|
||||||
|
|
||||||
getItemsApi(api)
|
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||||
.getItems({
|
ParentId: libraryId,
|
||||||
parentId: libraryId,
|
IncludeItemTypes: [BaseItemKind.MusicAlbum],
|
||||||
includeItemTypes: [BaseItemKind.MusicAlbum],
|
Recursive: true,
|
||||||
recursive: true,
|
ExcludeItemIds: [artist.Id!],
|
||||||
excludeItemIds: [artist.Id!],
|
SortBy: [ItemSortBy.PremiereDate, ItemSortBy.ProductionYear, ItemSortBy.SortName],
|
||||||
sortBy: [ItemSortBy.PremiereDate, ItemSortBy.ProductionYear, ItemSortBy.SortName],
|
SortOrder: [SortOrder.Descending],
|
||||||
sortOrder: [SortOrder.Descending],
|
ContributingArtistIds: [artist.Id!],
|
||||||
contributingArtistIds: [artist.Id!],
|
})
|
||||||
})
|
.then((data) => {
|
||||||
.then((response) => {
|
return data.Items ? resolve(data.Items) : resolve([])
|
||||||
return response.data.Items ? resolve(response.data.Items) : resolve([])
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error)
|
reject(error)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import RNFS from 'react-native-fs'
|
import RNFS from 'react-native-fs'
|
||||||
|
import DeviceInfo from 'react-native-device-info'
|
||||||
|
|
||||||
type JellifyStorage = {
|
type JellifyStorage = {
|
||||||
totalStorage: number
|
totalStorage: number
|
||||||
@@ -9,10 +10,11 @@ type JellifyStorage = {
|
|||||||
const fetchStorageInUse: () => Promise<JellifyStorage> = async () => {
|
const fetchStorageInUse: () => Promise<JellifyStorage> = async () => {
|
||||||
const totalStorage = await RNFS.getFSInfo()
|
const totalStorage = await RNFS.getFSInfo()
|
||||||
const storageInUse = await RNFS.stat(RNFS.DocumentDirectoryPath)
|
const storageInUse = await RNFS.stat(RNFS.DocumentDirectoryPath)
|
||||||
|
const freeDiskStorage = await DeviceInfo.getFreeDiskStorage()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalStorage: totalStorage.totalSpace,
|
totalStorage: totalStorage.totalSpace,
|
||||||
freeSpace: totalStorage.freeSpace,
|
freeSpace: freeDiskStorage,
|
||||||
storageInUseByJellify: storageInUse.size,
|
storageInUseByJellify: storageInUse.size,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ export async function fetchFavoriteArtists(
|
|||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug(`Fetching user's favorite artists`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
@@ -39,8 +37,6 @@ export async function fetchFavoriteArtists(
|
|||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.debug(`Received favorite artist response`, response)
|
|
||||||
|
|
||||||
if (response.data.Items) return resolve(response.data.Items)
|
if (response.data.Items) return resolve(response.data.Items)
|
||||||
else return resolve([])
|
else return resolve([])
|
||||||
})
|
})
|
||||||
@@ -63,8 +59,6 @@ export async function fetchFavoriteAlbums(
|
|||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug(`Fetching user's favorite albums`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
@@ -80,8 +74,6 @@ export async function fetchFavoriteAlbums(
|
|||||||
sortOrder: [SortOrder.Descending, SortOrder.Ascending],
|
sortOrder: [SortOrder.Descending, SortOrder.Ascending],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.debug(`Received favorite album response`, response)
|
|
||||||
|
|
||||||
if (response.data.Items) return resolve(response.data.Items)
|
if (response.data.Items) return resolve(response.data.Items)
|
||||||
else return resolve([])
|
else return resolve([])
|
||||||
})
|
})
|
||||||
@@ -104,8 +96,6 @@ export async function fetchFavoritePlaylists(
|
|||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug(`Fetching user's favorite playlists`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
@@ -120,7 +110,6 @@ export async function fetchFavoritePlaylists(
|
|||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log(response)
|
|
||||||
if (response.data.Items)
|
if (response.data.Items)
|
||||||
return resolve(
|
return resolve(
|
||||||
response.data.Items.filter(
|
response.data.Items.filter(
|
||||||
@@ -149,8 +138,6 @@ export async function fetchFavoriteTracks(
|
|||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug(`Fetching user's favorite tracks`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
@@ -166,8 +153,6 @@ export async function fetchFavoriteTracks(
|
|||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.debug(`Received favorite artist response`, response)
|
|
||||||
|
|
||||||
if (response.data.Items) return resolve(response.data.Items)
|
if (response.data.Items) return resolve(response.data.Items)
|
||||||
else return resolve([])
|
else return resolve([])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
import { FrequentlyPlayedArtistsQueryKey, FrequentlyPlayedTracksQueryKey } from './keys'
|
import { FrequentlyPlayedArtistsQueryKey, FrequentlyPlayedTracksQueryKey } from './keys'
|
||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from './utils/frequents'
|
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from './utils/frequents'
|
||||||
import { ApiLimits } from '../query.config'
|
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
|
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||||
|
|
||||||
const FREQUENTS_QUERY_CONFIG = {
|
const FREQUENTS_QUERY_CONFIG = {
|
||||||
|
maxPages: MaxPages.Home,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
staleTime: Infinity,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const useFrequentlyPlayedTracks = () => {
|
export const useFrequentlyPlayedTracks = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: FrequentlyPlayedTracksQueryKey(user, library),
|
queryKey: FrequentlyPlayedTracksQueryKey(user, library),
|
||||||
@@ -19,7 +21,6 @@ export const useFrequentlyPlayedTracks = () => {
|
|||||||
select: (data) => data.pages.flatMap((page) => page),
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||||
console.debug('Getting next page for frequently played')
|
|
||||||
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
|
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
|
||||||
},
|
},
|
||||||
...FREQUENTS_QUERY_CONFIG,
|
...FREQUENTS_QUERY_CONFIG,
|
||||||
@@ -27,7 +28,9 @@ export const useFrequentlyPlayedTracks = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useFrequentlyPlayedArtists = () => {
|
export const useFrequentlyPlayedArtists = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
const { data: frequentlyPlayedTracks } = useFrequentlyPlayedTracks()
|
const { data: frequentlyPlayedTracks } = useFrequentlyPlayedTracks()
|
||||||
|
|
||||||
@@ -37,7 +40,6 @@ export const useFrequentlyPlayedArtists = () => {
|
|||||||
select: (data) => data.pages.flatMap((page) => page),
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||||
console.debug('Getting next page for frequent artists')
|
|
||||||
return lastPage.length > 0 ? lastPageParam + 1 : undefined
|
return lastPage.length > 0 ? lastPageParam + 1 : undefined
|
||||||
},
|
},
|
||||||
enabled: !isUndefined(frequentlyPlayedTracks),
|
enabled: !isUndefined(frequentlyPlayedTracks),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Api } from '@jellyfin/sdk'
|
|||||||
import { isEmpty, isNull, isUndefined } from 'lodash'
|
import { isEmpty, isNull, isUndefined } from 'lodash'
|
||||||
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
||||||
import { fetchItem } from '../../item'
|
import { fetchItem } from '../../item'
|
||||||
import { ApiLimits } from '../../query.config'
|
import { ApiLimits } from '../../../../configs/query.config'
|
||||||
import { JellifyUser } from '@/src/types/JellifyUser'
|
import { JellifyUser } from '@/src/types/JellifyUser'
|
||||||
import { queryClient } from '../../../../constants/query-client'
|
import { queryClient } from '../../../../constants/query-client'
|
||||||
import { InfiniteData } from '@tanstack/react-query'
|
import { InfiniteData } from '@tanstack/react-query'
|
||||||
@@ -66,11 +66,7 @@ export function fetchFrequentlyPlayedArtists(
|
|||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching frequently played artists', page)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.debug('Fetching frequently played artists')
|
|
||||||
|
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(library)) return reject('Library instance not set')
|
if (isUndefined(library)) return reject('Library instance not set')
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { useFrequentlyPlayedArtists, useFrequentlyPlayedTracks } from '../frequents'
|
|
||||||
import { useRecentArtists, useRecentlyPlayedTracks } from '../recents'
|
|
||||||
|
|
||||||
const useHomeQueries = () => {
|
|
||||||
const { refetch: refetchRecentArtists } = useRecentArtists()
|
|
||||||
|
|
||||||
const { refetch: refetchRecentlyPlayed } = useRecentlyPlayedTracks()
|
|
||||||
|
|
||||||
const { refetch: refetchFrequentArtists } = useFrequentlyPlayedArtists()
|
|
||||||
|
|
||||||
const { refetch: refetchFrequentlyPlayed } = useFrequentlyPlayedTracks()
|
|
||||||
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['Home'],
|
|
||||||
queryFn: async () => {
|
|
||||||
await Promise.all([refetchRecentlyPlayed(), refetchFrequentlyPlayed()])
|
|
||||||
await Promise.all([refetchFrequentArtists(), refetchRecentArtists()])
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useHomeQueries
|
|
||||||
@@ -2,22 +2,63 @@ import { Api } from '@jellyfin/sdk'
|
|||||||
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
import { BaseItemDto, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
|
|
||||||
|
// Default image size for list thumbnails (optimized for common row heights)
|
||||||
|
const DEFAULT_THUMBNAIL_SIZE = 200
|
||||||
|
|
||||||
|
export interface ImageUrlOptions {
|
||||||
|
/** Maximum width of the requested image */
|
||||||
|
maxWidth?: number
|
||||||
|
/** Maximum height of the requested image */
|
||||||
|
maxHeight?: number
|
||||||
|
/** Image quality (0-100) */
|
||||||
|
quality?: number
|
||||||
|
}
|
||||||
|
|
||||||
export function getItemImageUrl(
|
export function getItemImageUrl(
|
||||||
api: Api | undefined,
|
api: Api | undefined,
|
||||||
item: BaseItemDto,
|
item: BaseItemDto,
|
||||||
type: ImageType,
|
type: ImageType,
|
||||||
|
options?: ImageUrlOptions,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const { AlbumId, AlbumPrimaryImageTag, ImageTags, Id } = item
|
const { AlbumId, AlbumPrimaryImageTag, ImageTags, Id, AlbumArtists } = item
|
||||||
|
|
||||||
if (!api) return undefined
|
if (!api) return undefined
|
||||||
|
|
||||||
return AlbumId
|
// Use provided dimensions or default thumbnail size for list performance
|
||||||
? getImageApi(api).getItemImageUrlById(AlbumId, type, {
|
const imageParams = {
|
||||||
tag: AlbumPrimaryImageTag ?? undefined,
|
tag: undefined as string | undefined,
|
||||||
})
|
maxWidth: options?.maxWidth ?? DEFAULT_THUMBNAIL_SIZE,
|
||||||
: Id
|
maxHeight: options?.maxHeight ?? DEFAULT_THUMBNAIL_SIZE,
|
||||||
? getImageApi(api).getItemImageUrlById(Id, type, {
|
quality: options?.quality ?? 90,
|
||||||
tag: ImageTags ? ImageTags[type] : undefined,
|
}
|
||||||
})
|
|
||||||
: undefined
|
// Check if the item has its own image for the requested type first
|
||||||
|
const hasOwnImage = ImageTags && ImageTags[type]
|
||||||
|
|
||||||
|
if (hasOwnImage && Id) {
|
||||||
|
// Use the item's own image (e.g., track-specific artwork)
|
||||||
|
return getImageApi(api).getItemImageUrlById(Id, type, {
|
||||||
|
...imageParams,
|
||||||
|
tag: ImageTags[type],
|
||||||
|
})
|
||||||
|
} else if (AlbumId && AlbumPrimaryImageTag) {
|
||||||
|
// Fall back to album image (only if the album has an image)
|
||||||
|
return getImageApi(api).getItemImageUrlById(AlbumId, type, {
|
||||||
|
...imageParams,
|
||||||
|
tag: AlbumPrimaryImageTag,
|
||||||
|
})
|
||||||
|
} else if (AlbumArtists && AlbumArtists.length > 0 && AlbumArtists[0].Id) {
|
||||||
|
// Fall back to first album artist's image
|
||||||
|
return getImageApi(api).getItemImageUrlById(AlbumArtists[0].Id, type, {
|
||||||
|
...imageParams,
|
||||||
|
})
|
||||||
|
} else if (Id) {
|
||||||
|
// Last resort: use item's own ID
|
||||||
|
return getImageApi(api).getItemImageUrlById(Id, type, {
|
||||||
|
...imageParams,
|
||||||
|
tag: ImageTags ? ImageTags[type] : undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||||
import { getInstantMixApi } from '@jellyfin/sdk/lib/utils/api'
|
import { getInstantMixApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
import QueryConfig from './query.config'
|
import QueryConfig from '../../configs/query.config'
|
||||||
import { Api } from '@jellyfin/sdk'
|
import { Api } from '@jellyfin/sdk'
|
||||||
import { JellifyUser } from '../../types/JellifyUser'
|
import { JellifyUser } from '../../types/JellifyUser'
|
||||||
/**
|
/**
|
||||||
@@ -16,8 +16,6 @@ export function fetchInstantMixFromItem(
|
|||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
item: BaseItemDto,
|
item: BaseItemDto,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching instant mix from item')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject(new Error('Client not initialized'))
|
if (isUndefined(api)) return reject(new Error('Client not initialized'))
|
||||||
if (isUndefined(user)) return reject(new Error('User not initialized'))
|
if (isUndefined(user)) return reject(new Error('User not initialized'))
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { groupBy, isEmpty, isEqual, isUndefined } from 'lodash'
|
|||||||
import { SectionList } from 'react-native'
|
import { SectionList } from 'react-native'
|
||||||
import { Api } from '@jellyfin/sdk/lib/api'
|
import { Api } from '@jellyfin/sdk/lib/api'
|
||||||
import { JellifyLibrary } from '../../types/JellifyLibrary'
|
import { JellifyLibrary } from '../../types/JellifyLibrary'
|
||||||
import QueryConfig from './query.config'
|
import QueryConfig from '../../configs/query.config'
|
||||||
import { JellifyUser } from '../../types/JellifyUser'
|
import { JellifyUser } from '../../types/JellifyUser'
|
||||||
|
import { nitroFetch } from '../utils/nitro'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches a single Jellyfin item by it's ID
|
* Fetches a single Jellyfin item by it's ID
|
||||||
@@ -19,7 +20,6 @@ import { JellifyUser } from '../../types/JellifyUser'
|
|||||||
* @returns The item - a {@link BaseItemDto}
|
* @returns The item - a {@link BaseItemDto}
|
||||||
*/
|
*/
|
||||||
export async function fetchItem(api: Api | undefined, itemId: string): Promise<BaseItemDto> {
|
export async function fetchItem(api: Api | undefined, itemId: string): Promise<BaseItemDto> {
|
||||||
console.debug('Fetching item by id')
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isEmpty(itemId)) return reject('No item ID proviced')
|
if (isEmpty(itemId)) return reject('No item ID proviced')
|
||||||
if (isUndefined(api)) return reject('Client not initialized')
|
if (isUndefined(api)) return reject('Client not initialized')
|
||||||
@@ -63,27 +63,25 @@ export async function fetchItems(
|
|||||||
parentId?: string | undefined,
|
parentId?: string | undefined,
|
||||||
ids?: string[] | undefined,
|
ids?: string[] | undefined,
|
||||||
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
): Promise<{ title: string | number; data: BaseItemDto[] }> {
|
||||||
console.debug('Fetching items', page)
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client not initialized')
|
if (isUndefined(api)) return reject('Client not initialized')
|
||||||
if (isUndefined(user)) return reject('User not initialized')
|
if (isUndefined(user)) return reject('User not initialized')
|
||||||
if (isUndefined(library)) return reject('Library not initialized')
|
if (isUndefined(library)) return reject('Library not initialized')
|
||||||
|
|
||||||
getItemsApi(api)
|
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||||
.getItems({
|
ParentId: parentId ?? library.musicLibraryId,
|
||||||
parentId: parentId ?? library.musicLibraryId,
|
UserId: user.id,
|
||||||
userId: user.id,
|
IncludeItemTypes: types,
|
||||||
includeItemTypes: types,
|
SortBy: sortBy,
|
||||||
sortBy,
|
Recursive: true,
|
||||||
recursive: true,
|
SortOrder: sortOrder,
|
||||||
sortOrder,
|
Fields: [ItemFields.ChildCount, ItemFields.SortName, ItemFields.Genres],
|
||||||
fields: [ItemFields.ChildCount, ItemFields.SortName, ItemFields.Genres],
|
StartIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
|
||||||
startIndex: typeof page === 'number' ? page * QueryConfig.limits.library : 0,
|
Limit: QueryConfig.limits.library,
|
||||||
limit: QueryConfig.limits.library,
|
IsFavorite: isFavorite,
|
||||||
isFavorite,
|
Ids: ids,
|
||||||
ids,
|
})
|
||||||
})
|
.then((data) => {
|
||||||
.then(({ data }) => {
|
|
||||||
resolve({ title: page, data: data.Items ?? [] })
|
resolve({ title: page, data: data.Items ?? [] })
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -102,7 +100,6 @@ export async function fetchAlbumDiscs(
|
|||||||
api: Api | undefined,
|
api: Api | undefined,
|
||||||
album: BaseItemDto,
|
album: BaseItemDto,
|
||||||
): Promise<{ title: string; data: BaseItemDto[] }[]> {
|
): Promise<{ title: string; data: BaseItemDto[] }[]> {
|
||||||
console.debug('Fetching album discs')
|
|
||||||
return new Promise<{ title: string; data: BaseItemDto[] }[]>((resolve, reject) => {
|
return new Promise<{ title: string; data: BaseItemDto[] }[]>((resolve, reject) => {
|
||||||
if (isEmpty(album.Id)) return reject('No album ID provided')
|
if (isEmpty(album.Id)) return reject('No album ID provided')
|
||||||
if (isUndefined(api)) return reject('Client not initialized')
|
if (isUndefined(api)) return reject('Client not initialized')
|
||||||
@@ -111,12 +108,11 @@ export async function fetchAlbumDiscs(
|
|||||||
|
|
||||||
sortBy = [ItemSortBy.ParentIndexNumber, ItemSortBy.IndexNumber, ItemSortBy.SortName]
|
sortBy = [ItemSortBy.ParentIndexNumber, ItemSortBy.IndexNumber, ItemSortBy.SortName]
|
||||||
|
|
||||||
getItemsApi(api)
|
nitroFetch<{ Items: BaseItemDto[] }>(api, '/Items', {
|
||||||
.getItems({
|
ParentId: album.Id!,
|
||||||
parentId: album.Id!,
|
SortBy: sortBy,
|
||||||
sortBy,
|
})
|
||||||
})
|
.then((data) => {
|
||||||
.then(({ data }) => {
|
|
||||||
const discs = data.Items
|
const discs = data.Items
|
||||||
? Object.keys(groupBy(data.Items, (track) => track.ParentIndexNumber)).map(
|
? Object.keys(groupBy(data.Items, (track) => track.ParentIndexNumber)).map(
|
||||||
(discNumber) => {
|
(discNumber) => {
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { Api } from '@jellyfin/sdk'
|
|||||||
import { JellifyUser } from '../../types/JellifyUser'
|
import { JellifyUser } from '../../types/JellifyUser'
|
||||||
|
|
||||||
export async function fetchMusicLibraries(api: Api | undefined): Promise<BaseItemDto[] | void> {
|
export async function fetchMusicLibraries(api: Api | undefined): Promise<BaseItemDto[] | void> {
|
||||||
console.debug('Fetching music libraries from Jellyfin')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
|
|
||||||
@@ -27,8 +25,6 @@ export async function fetchMusicLibraries(api: Api | undefined): Promise<BaseIte
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPlaylistLibrary(api: Api | undefined): Promise<BaseItemDto | undefined> {
|
export async function fetchPlaylistLibrary(api: Api | undefined): Promise<BaseItemDto | undefined> {
|
||||||
console.debug('Fetching playlist library from Jellyfin')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
|
|
||||||
@@ -57,8 +53,6 @@ export async function fetchUserViews(
|
|||||||
api: Api | undefined,
|
api: Api | undefined,
|
||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
): Promise<BaseItemDto[] | void> {
|
): Promise<BaseItemDto[] | void> {
|
||||||
console.debug('Fetching user views')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useNowPlaying } from '../../../providers/Player/hooks/queries'
|
|
||||||
import { useQuery, UseQueryResult } from '@tanstack/react-query'
|
import { useQuery, UseQueryResult } from '@tanstack/react-query'
|
||||||
import LyricsQueryKey from './keys'
|
import LyricsQueryKey from './keys'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
import { fetchRawLyrics } from './utils'
|
import { fetchRawLyrics } from './utils'
|
||||||
import { useJellifyContext } from '../../../providers'
|
import { useApi } from '../../../stores'
|
||||||
|
import { useCurrentTrack } from '../../../stores/player/queue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook that will return a {@link useQuery}
|
* A hook that will return a {@link useQuery}
|
||||||
@@ -11,8 +11,8 @@ import { useJellifyContext } from '../../../providers'
|
|||||||
* @returns a {@link UseQueryResult} for the
|
* @returns a {@link UseQueryResult} for the
|
||||||
*/
|
*/
|
||||||
const useRawLyrics = () => {
|
const useRawLyrics = () => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
const { data: nowPlaying } = useNowPlaying()
|
const nowPlaying = useCurrentTrack()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: LyricsQueryKey(nowPlaying),
|
queryKey: LyricsQueryKey(nowPlaying),
|
||||||
|
|||||||
@@ -19,15 +19,11 @@ export async function fetchRawLyrics(
|
|||||||
if (isUndefined(api)) throw new Error('Client not initialized')
|
if (isUndefined(api)) throw new Error('Client not initialized')
|
||||||
if (isEmpty(itemId)) throw new Error('No item ID provided')
|
if (isEmpty(itemId)) throw new Error('No item ID provided')
|
||||||
|
|
||||||
console.log('itemId', itemId)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Jellyfin LyricsApi returns plain text (often LRC) for the given item
|
// Jellyfin LyricsApi returns plain text (often LRC) for the given item
|
||||||
// SDK: LyricsApi.getLyrics({ itemId })
|
// SDK: LyricsApi.getLyrics({ itemId })
|
||||||
const lyricsApi: LyricsApi = getLyricsApi(api)
|
const lyricsApi: LyricsApi = getLyricsApi(api)
|
||||||
console.log('lyricsApi', lyricsApi)
|
|
||||||
const { data } = await lyricsApi.getLyrics({ itemId })
|
const { data } = await lyricsApi.getLyrics({ itemId })
|
||||||
console.log('data', data)
|
|
||||||
|
|
||||||
// Some SDK versions may wrap text; defensively unwrap
|
// Some SDK versions may wrap text; defensively unwrap
|
||||||
return data.Lyrics
|
return data.Lyrics
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Api } from '@jellyfin/sdk'
|
import { Api } from '@jellyfin/sdk'
|
||||||
import { useJellifyContext } from '../../../../src/providers'
|
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { JellifyUser } from '@/src/types/JellifyUser'
|
|
||||||
import useStreamingDeviceProfile, {
|
import useStreamingDeviceProfile, {
|
||||||
useDownloadingDeviceProfile,
|
useDownloadingDeviceProfile,
|
||||||
} from '../../../stores/device-profile'
|
} from '../../../stores/device-profile'
|
||||||
import { fetchMediaInfo } from './utils'
|
import { fetchMediaInfo } from './utils'
|
||||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||||
import MediaInfoQueryKey from './keys'
|
import MediaInfoQueryKey from './keys'
|
||||||
|
import { useApi } from '../../../stores'
|
||||||
|
import { ONE_DAY } from '../../../constants/query-client'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A React hook that will retrieve the latest media info
|
* A React hook that will retrieve the latest media info
|
||||||
@@ -16,22 +16,24 @@ import MediaInfoQueryKey from './keys'
|
|||||||
* Depends on the {@link useStreamingDeviceProfile} hook for retrieving
|
* Depends on the {@link useStreamingDeviceProfile} hook for retrieving
|
||||||
* the currently configured device profile
|
* the currently configured device profile
|
||||||
*
|
*
|
||||||
* Depends on the {@link useJellifyContext} hook for retrieving
|
* Depends on the {@link useApi} hook for retrieving
|
||||||
* the currently configured {@link Api} and {@link JellifyUser}
|
* the currently configured {@link Api}
|
||||||
* instance
|
* instance
|
||||||
*
|
*
|
||||||
* @param itemId The Id of the {@link BaseItemDto}
|
* @param itemId The Id of the {@link BaseItemDto}
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const useStreamedMediaInfo = (itemId: string | null | undefined) => {
|
const useStreamedMediaInfo = (itemId: string | null | undefined) => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
const deviceProfile = useStreamingDeviceProfile()
|
const deviceProfile = useStreamingDeviceProfile()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
|
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
|
||||||
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
|
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
|
||||||
staleTime: Infinity, // Only refetch when the user's device profile changes
|
enabled: Boolean(api && deviceProfile && itemId),
|
||||||
|
staleTime: ONE_DAY, // Only refetch when the user's device profile changes
|
||||||
|
gcTime: ONE_DAY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,21 +46,23 @@ export default useStreamedMediaInfo
|
|||||||
* Depends on the {@link useDownloadingDeviceProfile} hook for retrieving
|
* Depends on the {@link useDownloadingDeviceProfile} hook for retrieving
|
||||||
* the currently configured device profile
|
* the currently configured device profile
|
||||||
*
|
*
|
||||||
* Depends on the {@link useJellifyContext} hook for retrieving
|
* Depends on the {@link useApi} hook for retrieving
|
||||||
* the currently configured {@link Api} and {@link JellifyUser}
|
* the currently configured {@link Api}
|
||||||
* instance
|
* instance
|
||||||
*
|
*
|
||||||
* @param itemId The Id of the {@link BaseItemDto}
|
* @param itemId The Id of the {@link BaseItemDto}
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const useDownloadedMediaInfo = (itemId: string | null | undefined) => {
|
export const useDownloadedMediaInfo = (itemId: string | null | undefined) => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
const deviceProfile = useDownloadingDeviceProfile()
|
const deviceProfile = useDownloadingDeviceProfile()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
|
queryKey: MediaInfoQueryKey({ api, deviceProfile, itemId }),
|
||||||
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
|
queryFn: () => fetchMediaInfo(api, deviceProfile, itemId),
|
||||||
staleTime: Infinity, // Only refetch when the user's device profile changes
|
enabled: Boolean(api && deviceProfile && itemId),
|
||||||
|
staleTime: ONE_DAY, // Only refetch when the user's device profile changes
|
||||||
|
gcTime: ONE_DAY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ export async function fetchMediaInfo(
|
|||||||
deviceProfile: DeviceProfile | undefined,
|
deviceProfile: DeviceProfile | undefined,
|
||||||
itemId: string | null | undefined,
|
itemId: string | null | undefined,
|
||||||
): Promise<PlaybackInfoResponse> {
|
): Promise<PlaybackInfoResponse> {
|
||||||
console.debug(`Fetching media info of with ${deviceProfile?.Name} profile`)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ export async function fetchMediaInfo(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
console.debug('Received media info response')
|
|
||||||
resolve(data)
|
resolve(data)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { QueryKeys } from '../../../enums/query-keys'
|
import { QueryKeys } from '../../../enums/query-keys'
|
||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import fetchPatrons from './utils'
|
import fetchPatrons from './utils'
|
||||||
import { ONE_DAY } from '../../../constants/query-client'
|
import { ONE_DAY } from '../../../constants/query-client'
|
||||||
|
import { useApi } from '../../../stores'
|
||||||
|
|
||||||
const usePatronsQuery = () => {
|
const usePatronsQuery = () => {
|
||||||
const { api } = useJellifyContext()
|
const api = useApi()
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [QueryKeys.Patrons],
|
queryKey: [QueryKeys.Patrons],
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import { UserPlaylistsQueryKey } from './keys'
|
import { UserPlaylistsQueryKey } from './keys'
|
||||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
import { fetchUserPlaylists, fetchPublicPlaylists } from './utils'
|
import { fetchUserPlaylists, fetchPublicPlaylists, fetchPlaylistTracks } from './utils'
|
||||||
import { ApiLimits } from '../query.config'
|
import { ApiLimits } from '../../../configs/query.config'
|
||||||
|
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
|
||||||
|
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
|
||||||
|
import { QueryKeys } from '../../../enums/query-keys'
|
||||||
|
|
||||||
export const useUserPlaylists = () => {
|
export const useUserPlaylists = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: UserPlaylistsQueryKey(library),
|
queryKey: UserPlaylistsQueryKey(library),
|
||||||
@@ -13,7 +17,39 @@ export const useUserPlaylists = () => {
|
|||||||
select: (data) => data.pages.flatMap((page) => page),
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||||
|
if (!lastPage) return undefined
|
||||||
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePlaylistTracks = (playlist: BaseItemDto) => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
// Changed from QueryKeys.ItemTracks to avoid cache conflicts with old useQuery data
|
||||||
|
queryKey: [QueryKeys.ItemTracks, 'infinite', playlist.Id!],
|
||||||
|
queryFn: ({ pageParam }) => fetchPlaylistTracks(api, playlist.Id!, pageParam),
|
||||||
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
|
initialPageParam: 0,
|
||||||
|
getNextPageParam: (lastPage, allPages, lastPageParam) => {
|
||||||
|
if (!lastPage) return undefined
|
||||||
|
return lastPage.length === ApiLimits.Library ? lastPageParam + 1 : undefined
|
||||||
|
},
|
||||||
|
enabled: Boolean(api && playlist.Id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePublicPlaylists = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
|
return useInfiniteQuery({
|
||||||
|
queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId],
|
||||||
|
queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam),
|
||||||
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
|
||||||
|
lastPage.length > 0 ? lastPageParam + 1 : undefined,
|
||||||
|
initialPageParam: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BaseItemDto,
|
BaseItemDto,
|
||||||
|
BaseItemKind,
|
||||||
ItemFields,
|
ItemFields,
|
||||||
ItemSortBy,
|
ItemSortBy,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
@@ -9,20 +10,28 @@ import { JellifyUser } from '../../../../types/JellifyUser'
|
|||||||
import { Api } from '@jellyfin/sdk'
|
import { Api } from '@jellyfin/sdk'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
|
||||||
import QueryConfig from '../../query.config'
|
import QueryConfig, { ApiLimits } from '../../../../configs/query.config'
|
||||||
|
import { nitroFetch } from '../../../utils/nitro'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the user's playlists from the Jellyfin server
|
||||||
|
*
|
||||||
|
* Performs filtering to ensure that these are playlists stored in the
|
||||||
|
* config directory of Jellyfin, as to avoid displaying .m3u files from
|
||||||
|
* the library
|
||||||
|
*
|
||||||
|
* @param api The {@link Api} instance from the {@link useApi} hook
|
||||||
|
* @param user The {@link JellifyUser} instance from the {@link useJellifyUser} hook
|
||||||
|
* @param library The {@link JellifyLibrary} instance from the {@link useJellifyLibrary} hook
|
||||||
|
* @param sortBy An array of {@link ItemSortBy} values to sort the response by
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export async function fetchUserPlaylists(
|
export async function fetchUserPlaylists(
|
||||||
api: Api | undefined,
|
api: Api | undefined,
|
||||||
user: JellifyUser | undefined,
|
user: JellifyUser | undefined,
|
||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
sortBy: ItemSortBy[] = [],
|
sortBy: ItemSortBy[] = [],
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug(
|
|
||||||
`Fetching user playlists ${sortBy.length > 0 ? 'sorting by ' + sortBy.toString() : ''}`,
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultSorting: ItemSortBy[] = [ItemSortBy.IsFolder, ItemSortBy.SortName]
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
@@ -37,6 +46,7 @@ export async function fetchUserPlaylists(
|
|||||||
ItemFields.CanDelete,
|
ItemFields.CanDelete,
|
||||||
ItemFields.Genres,
|
ItemFields.Genres,
|
||||||
ItemFields.ChildCount,
|
ItemFields.ChildCount,
|
||||||
|
ItemFields.ItemCounts,
|
||||||
],
|
],
|
||||||
sortBy: [ItemSortBy.SortName],
|
sortBy: [ItemSortBy.SortName],
|
||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
@@ -44,10 +54,9 @@ export async function fetchUserPlaylists(
|
|||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.Items)
|
if (response.data.Items)
|
||||||
|
// Playlists must be stored in Jellyfin's internal config directory
|
||||||
return resolve(
|
return resolve(
|
||||||
response.data.Items.filter((playlist) =>
|
response.data.Items.filter((playlist) => playlist.Path?.includes('data')),
|
||||||
playlist.Path!.includes('/data/playlists'),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else return resolve([])
|
else return resolve([])
|
||||||
})
|
})
|
||||||
@@ -62,8 +71,6 @@ export async function fetchPublicPlaylists(
|
|||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching public playlists')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(library)) return reject('Library instance not set')
|
if (isUndefined(library)) return reject('Library instance not set')
|
||||||
@@ -75,16 +82,19 @@ export async function fetchPublicPlaylists(
|
|||||||
sortOrder: [SortOrder.Ascending],
|
sortOrder: [SortOrder.Ascending],
|
||||||
startIndex: page * QueryConfig.limits.library,
|
startIndex: page * QueryConfig.limits.library,
|
||||||
limit: QueryConfig.limits.library,
|
limit: QueryConfig.limits.library,
|
||||||
fields: ['Path', 'CanDelete', 'Genres'],
|
fields: [
|
||||||
|
ItemFields.Path,
|
||||||
|
ItemFields.CanDelete,
|
||||||
|
ItemFields.Genres,
|
||||||
|
ItemFields.ChildCount,
|
||||||
|
ItemFields.ItemCounts,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.log(response)
|
|
||||||
|
|
||||||
if (response.data.Items)
|
if (response.data.Items)
|
||||||
|
// Playlists must not be stored in Jellyfin's internal config directory
|
||||||
return resolve(
|
return resolve(
|
||||||
response.data.Items.filter(
|
response.data.Items.filter((playlist) => !playlist.Path?.includes('data')),
|
||||||
(playlist) => !playlist.Path?.includes('/data/playlists'),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else return resolve([])
|
else return resolve([])
|
||||||
})
|
})
|
||||||
@@ -94,3 +104,38 @@ export async function fetchPublicPlaylists(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches tracks for a playlist with pagination using NitroFetch
|
||||||
|
* for optimized JSON parsing on a background thread.
|
||||||
|
*
|
||||||
|
* @param api The {@link Api} instance
|
||||||
|
* @param playlistId The ID of the playlist to fetch tracks for
|
||||||
|
* @param pageParam The page number for pagination (0-indexed)
|
||||||
|
* @returns Array of tracks for the playlist
|
||||||
|
*/
|
||||||
|
export async function fetchPlaylistTracks(
|
||||||
|
api: Api | undefined,
|
||||||
|
playlistId: string,
|
||||||
|
pageParam: number = 0,
|
||||||
|
): Promise<BaseItemDto[]> {
|
||||||
|
if (isUndefined(api)) {
|
||||||
|
throw new Error('Client instance not set')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await nitroFetch<{ Items: BaseItemDto[]; TotalRecordCount: number }>(
|
||||||
|
api,
|
||||||
|
'/Items',
|
||||||
|
{
|
||||||
|
ParentId: playlistId,
|
||||||
|
IncludeItemTypes: [BaseItemKind.Audio],
|
||||||
|
EnableUserData: true,
|
||||||
|
Recursive: false,
|
||||||
|
Limit: ApiLimits.Library,
|
||||||
|
StartIndex: pageParam * ApiLimits.Library,
|
||||||
|
Fields: [ItemFields.MediaSources, ItemFields.ParentId, ItemFields.Path],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data.Items ?? []
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useJellifyContext } from '../../../providers'
|
|
||||||
import { RecentlyPlayedArtistsQueryKey, RecentlyPlayedTracksQueryKey } from './keys'
|
import { RecentlyPlayedArtistsQueryKey, RecentlyPlayedTracksQueryKey } from './keys'
|
||||||
import { useInfiniteQuery } from '@tanstack/react-query'
|
import { useInfiniteQuery } from '@tanstack/react-query'
|
||||||
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from './utils'
|
import { fetchRecentlyPlayed, fetchRecentlyPlayedArtists } from './utils'
|
||||||
import { ApiLimits } from '../query.config'
|
import { ApiLimits, MaxPages } from '../../../configs/query.config'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
|
import { useApi, useJellifyUser, useJellifyLibrary } from '../../../stores'
|
||||||
|
|
||||||
const RECENTS_QUERY_CONFIG = {
|
const RECENTS_QUERY_CONFIG = {
|
||||||
maxPages: 2,
|
maxPages: MaxPages.Home,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
staleTime: Infinity,
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const useRecentlyPlayedTracks = () => {
|
export const useRecentlyPlayedTracks = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
queryKey: RecentlyPlayedTracksQueryKey(user, library),
|
queryKey: RecentlyPlayedTracksQueryKey(user, library),
|
||||||
@@ -20,7 +21,6 @@ export const useRecentlyPlayedTracks = () => {
|
|||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
select: (data) => data.pages.flatMap((page) => page),
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||||
console.debug('Getting next page for recent tracks')
|
|
||||||
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
|
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
|
||||||
},
|
},
|
||||||
...RECENTS_QUERY_CONFIG,
|
...RECENTS_QUERY_CONFIG,
|
||||||
@@ -28,7 +28,9 @@ export const useRecentlyPlayedTracks = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useRecentArtists = () => {
|
export const useRecentArtists = () => {
|
||||||
const { api, user, library } = useJellifyContext()
|
const api = useApi()
|
||||||
|
const [user] = useJellifyUser()
|
||||||
|
const [library] = useJellifyLibrary()
|
||||||
|
|
||||||
const { data: recentlyPlayedTracks } = useRecentlyPlayedTracks()
|
const { data: recentlyPlayedTracks } = useRecentlyPlayedTracks()
|
||||||
|
|
||||||
@@ -38,7 +40,6 @@ export const useRecentArtists = () => {
|
|||||||
select: (data) => data.pages.flatMap((page) => page),
|
select: (data) => data.pages.flatMap((page) => page),
|
||||||
initialPageParam: 0,
|
initialPageParam: 0,
|
||||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||||
console.debug('Getting next page for recent artists')
|
|
||||||
return lastPage.length > 0 ? lastPageParam + 1 : undefined
|
return lastPage.length > 0 ? lastPageParam + 1 : undefined
|
||||||
},
|
},
|
||||||
enabled: !isUndefined(recentlyPlayedTracks),
|
enabled: !isUndefined(recentlyPlayedTracks),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
} from '@jellyfin/sdk/lib/generated-client/models'
|
} from '@jellyfin/sdk/lib/generated-client/models'
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'
|
||||||
import QueryConfig, { ApiLimits } from '../../query.config'
|
import QueryConfig, { ApiLimits } from '../../../../configs/query.config'
|
||||||
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api'
|
||||||
import { Api } from '@jellyfin/sdk'
|
import { Api } from '@jellyfin/sdk'
|
||||||
import { isUndefined } from 'lodash'
|
import { isUndefined } from 'lodash'
|
||||||
@@ -56,8 +56,6 @@ export async function fetchRecentlyPlayed(
|
|||||||
page: number,
|
page: number,
|
||||||
limit: number = ApiLimits.Home,
|
limit: number = ApiLimits.Home,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching recently played items')
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(api)) return reject('Client instance not set')
|
if (isUndefined(api)) return reject('Client instance not set')
|
||||||
if (isUndefined(user)) return reject('User instance not set')
|
if (isUndefined(user)) return reject('User instance not set')
|
||||||
@@ -77,8 +75,6 @@ export async function fetchRecentlyPlayed(
|
|||||||
enableUserData: true,
|
enableUserData: true,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
console.debug('Received recently played items response')
|
|
||||||
|
|
||||||
if (response.data.Items) return resolve(response.data.Items)
|
if (response.data.Items) return resolve(response.data.Items)
|
||||||
return resolve([])
|
return resolve([])
|
||||||
})
|
})
|
||||||
@@ -101,7 +97,6 @@ export function fetchRecentlyPlayedArtists(
|
|||||||
library: JellifyLibrary | undefined,
|
library: JellifyLibrary | undefined,
|
||||||
page: number,
|
page: number,
|
||||||
): Promise<BaseItemDto[]> {
|
): Promise<BaseItemDto[]> {
|
||||||
console.debug('Fetching recently played artists')
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (isUndefined(library)) return reject('Library instance not set')
|
if (isUndefined(library)) return reject('Library instance not set')
|
||||||
|
|
||||||
|
|||||||