mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-16 18:55:44 -06:00
Update React Native to 0.79.2, Bump Dependencies, Updates to Settings Tab, Add opt in Logging and telemetry (#338)
Welcome to Jellify 0.12.2! This update comes with a much anticipated developer feature - and that is opt-in logging and telemetry. This is purely an opt-in feature, and can be toggled at anytime either when logging in for the first time, or in the settings tab. Under no circumstances will this ever be required to use Jellify, and all data that is collected is anonymized The Settings tab has had a facelift, stealing design queues from the library page. This is where adjustable settings will make their home in updates to come, as well as how beta testing larger features will occur with “Labs” This update also fixes issues with playback reporting, where the Jellyfin server wasn’t marking songs as played. This was not intended and has been fixed for those using the Last.FM plugin for scrobbling There is also a slew of upgrades to underlying dependencies to make sure we are up to date on that front Thanks for reading! ~Violet
This commit is contained in:
38
.github/workflows/publish-beta.yml
vendored
38
.github/workflows/publish-beta.yml
vendored
@@ -29,6 +29,29 @@ jobs:
|
||||
- name: 🤫 Output App Store Connect API Key JSON to Fastlane
|
||||
run: echo -e '${{ secrets.APPSTORE_CONNECT_API_KEY_JSON }}' > appstore_connect_api_key.json
|
||||
working-directory: ./ios/fastlane
|
||||
|
||||
- name: 🤫 Output TelemetryDeck Secrets to TelemetryDeck.json
|
||||
run: |
|
||||
echo "{" > telemetrydeck.json
|
||||
echo "\"appID\": \"${{ secrets.TELEMETRYDECK_APPID }}\"," >> telemetrydeck.json
|
||||
echo "\"clientUser\": \"anonymous\"," >> telemetrydeck.json
|
||||
echo "\"app\": \"Jellify\"" >> telemetrydeck.json
|
||||
echo "}" >> telemetrydeck.json
|
||||
|
||||
|
||||
- name: 🤫 Output Glitchtip Secrets to Glitchtip.json
|
||||
run: |
|
||||
echo "{" > glitchtip.json
|
||||
echo "\"dsn\": \"${{ secrets.GLITCHTIP_DSN }}\"" >> glitchtip.json
|
||||
echo "}" >> glitchtip.json
|
||||
|
||||
- name: ✅ Validate TelemetryDeck.json
|
||||
run: |
|
||||
node -e "JSON.parse(require('fs').readFileSync('telemetrydeck.json'))"
|
||||
|
||||
- name: ✅ Validate Glitchtip.json
|
||||
run: |
|
||||
node -e "JSON.parse(require('fs').readFileSync('glitchtip.json'))"
|
||||
|
||||
- name: 🚀 Run Android fastlane build
|
||||
run: yarn fastlane:android:build
|
||||
@@ -41,11 +64,19 @@ jobs:
|
||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||
MATCH_REPO_PAT: "anultravioletaurora:${{ secrets.SIGNING_REPO_PAT }}"
|
||||
|
||||
- name: 👩💻 Configure Git
|
||||
run: |
|
||||
git config --global user.email "violet@cosmonautical.cloud"
|
||||
git config --global user.name "anultravioletaurora"
|
||||
|
||||
- name: 🧹 Clean up Glitchtip and TelemetryDeck files
|
||||
run: |
|
||||
git restore telemetrydeck.json
|
||||
git restore glitchtip.json
|
||||
|
||||
# Commit Fastlane Xcode build number increment
|
||||
- name: 🔢 Commit changes for version increment
|
||||
run: |
|
||||
git config --global user.email "jellify@cosmonautical.com"
|
||||
git config --global user.name "anultravioletaurora"
|
||||
git add package.json
|
||||
git add ios/Jellify.xcodeproj/project.pbxproj
|
||||
git add android/app/build.gradle
|
||||
@@ -77,9 +108,6 @@ jobs:
|
||||
prerelease: true
|
||||
tag: ${{ env.VERSION_NUMBER }}
|
||||
token: ${{ secrets.SIGNING_REPO_PAT }}
|
||||
|
||||
|
||||
|
||||
|
||||
- name: 🗣️ Notify on Discord
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -76,4 +76,4 @@ yarn-error.log
|
||||
# Expo
|
||||
.expo
|
||||
dist/
|
||||
web-build/
|
||||
web-build/
|
||||
|
||||
5
App.tsx
5
App.tsx
@@ -19,6 +19,10 @@ import { requestStoragePermission } from './src/helpers/permisson-helpers'
|
||||
import ErrorBoundary from './src/components/ErrorBoundary'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import JellifyToastConfig from './src/constants/toast.config'
|
||||
import { TelemetryDeckProvider, createTelemetryDeck } from '@typedigital/telemetrydeck-react'
|
||||
import telemetryDeckConfig from './telemetrydeck.json'
|
||||
import glitchtipConfig from './glitchtip.json'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
export const backgroundRuntime = createWorkletRuntime('background')
|
||||
|
||||
@@ -36,6 +40,7 @@ export default function App(): React.JSX.Element {
|
||||
capabilities: CAPABILITIES,
|
||||
notificationCapabilities: CAPABILITIES,
|
||||
compactCapabilities: CAPABILITIES,
|
||||
progressUpdateEventInterval: 10,
|
||||
}),
|
||||
)
|
||||
.finally(() => {
|
||||
|
||||
@@ -140,9 +140,10 @@ Playlist
|
||||
[React Native Track Player](https://github.com/doublesymmetry/react-native-track-player)\
|
||||
[React Native URL Polyfill](https://github.com/charpeni/react-native-url-polyfill)
|
||||
|
||||
### 👩💻 Monitoring
|
||||
### 👩💻 *Opt-In* Monitoring
|
||||
|
||||
[GlitchTip](https://glitchtip.com/)
|
||||
[GlitchTip](https://glitchtip.com/)\
|
||||
[TelemetryDeck](https://telemetrydeck.com)
|
||||
|
||||
### 💜 Love from Wisconsin 🧀
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
58
eslint.config.js
Normal file
58
eslint.config.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const { defineConfig } = require('eslint/config')
|
||||
|
||||
const tsParser = require('@typescript-eslint/parser')
|
||||
const typescriptEslint = require('@typescript-eslint/eslint-plugin')
|
||||
const react = require('eslint-plugin-react')
|
||||
const globals = require('globals')
|
||||
const js = require('@eslint/js')
|
||||
|
||||
const { FlatCompat } = require('@eslint/eslintrc')
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
})
|
||||
|
||||
module.exports = defineConfig([
|
||||
{
|
||||
extends: compat.extends(
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
),
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
},
|
||||
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
react,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'no-mixed-spaces-and-tabs': 'off',
|
||||
semi: ['error', 'never'],
|
||||
},
|
||||
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
3
glitchtip.json
Normal file
3
glitchtip.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dsn": "https://glitchtip.jellify.app"
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
217EBE16A3E8C5FBF476C905 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
92C580068317633958E4B0F9 /* libPods-Jellify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4912CEDF8E675A9B3515C88E /* libPods-Jellify.a */; };
|
||||
9D70AF74B3F54D679E527364 /* libPods-Jellify.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 95B4030EF5077AD57C505857 /* libPods-Jellify.a */; };
|
||||
CF620D0C2CF2BB210045E433 /* Aileron-Italic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */; };
|
||||
CF620D0D2CF2BB210045E433 /* Aileron-Thin.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFD2CF2BB1F0045E433 /* Aileron-Thin.otf */; };
|
||||
CF620D0E2CF2BB210045E433 /* Aileron-HeavyItalic.otf in Resources */ = {isa = PBXBuildFile; fileRef = CF620CFE2CF2BB1F0045E433 /* Aileron-HeavyItalic.otf */; };
|
||||
@@ -79,10 +79,10 @@
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Jellify/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Jellify/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = Jellify/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
4912CEDF8E675A9B3515C88E /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1D5EDD3ECA2154FB05E36C5D /* Pods-Jellify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.debug.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = Jellify/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
A4403789D3D6FBE6706E62B4 /* Pods-Jellify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.release.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.release.xcconfig"; sourceTree = "<group>"; };
|
||||
ACD0D4797EFB0AA1C5B6FC7D /* Pods-Jellify.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.debug.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
95B4030EF5077AD57C505857 /* libPods-Jellify.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Jellify.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A42F7FBA73F634466F310088 /* Pods-Jellify.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Jellify.release.xcconfig"; path = "Target Support Files/Pods-Jellify/Pods-Jellify.release.xcconfig"; sourceTree = "<group>"; };
|
||||
CF620CFC2CF2BB1F0045E433 /* Aileron-Italic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-Italic.otf"; path = "../assets/fonts/Aileron-Italic.otf"; sourceTree = "<group>"; };
|
||||
CF620CFD2CF2BB1F0045E433 /* Aileron-Thin.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-Thin.otf"; path = "../assets/fonts/Aileron-Thin.otf"; sourceTree = "<group>"; };
|
||||
CF620CFE2CF2BB1F0045E433 /* Aileron-HeavyItalic.otf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Aileron-HeavyItalic.otf"; path = "../assets/fonts/Aileron-HeavyItalic.otf"; sourceTree = "<group>"; };
|
||||
@@ -164,7 +164,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
92C580068317633958E4B0F9 /* libPods-Jellify.a in Frameworks */,
|
||||
9D70AF74B3F54D679E527364 /* libPods-Jellify.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -209,7 +209,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
4912CEDF8E675A9B3515C88E /* libPods-Jellify.a */,
|
||||
95B4030EF5077AD57C505857 /* libPods-Jellify.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -265,8 +265,8 @@
|
||||
BBD78D7AC51CEA395F1C20DB /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ACD0D4797EFB0AA1C5B6FC7D /* Pods-Jellify.debug.xcconfig */,
|
||||
A4403789D3D6FBE6706E62B4 /* Pods-Jellify.release.xcconfig */,
|
||||
1D5EDD3ECA2154FB05E36C5D /* Pods-Jellify.debug.xcconfig */,
|
||||
A42F7FBA73F634466F310088 /* Pods-Jellify.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -356,13 +356,13 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Jellify" */;
|
||||
buildPhases = (
|
||||
B44EEE05F9243658C0ACC188 /* [CP] Check Pods Manifest.lock */,
|
||||
F74F914889A8D884A0B6CADF /* [CP] Check Pods Manifest.lock */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
18CA487EA4EA691EF7ACA31A /* [CP] Embed Pods Frameworks */,
|
||||
0949ABFBA66388712DB5BAC4 /* [CP] Copy Pods Resources */,
|
||||
1E8DEA05AEB2DD976158686F /* [CP] Embed Pods Frameworks */,
|
||||
20B672AD2C4E05F9590DDE44 /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -491,24 +491,7 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
};
|
||||
0949ABFBA66388712DB5BAC4 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
18CA487EA4EA691EF7ACA31A /* [CP] Embed Pods Frameworks */ = {
|
||||
1E8DEA05AEB2DD976158686F /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -525,7 +508,24 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
B44EEE05F9243658C0ACC188 /* [CP] Check Pods Manifest.lock */ = {
|
||||
20B672AD2C4E05F9590DDE44 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
F74F914889A8D884A0B6CADF /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -634,7 +634,7 @@
|
||||
};
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = ACD0D4797EFB0AA1C5B6FC7D /* Pods-Jellify.debug.xcconfig */;
|
||||
baseConfigurationReference = 1D5EDD3ECA2154FB05E36C5D /* Pods-Jellify.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
@@ -676,7 +676,7 @@
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A4403789D3D6FBE6706E62B4 /* Pods-Jellify.release.xcconfig */;
|
||||
baseConfigurationReference = A42F7FBA73F634466F310088 /* Pods-Jellify.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||
|
||||
604
ios/Podfile.lock
604
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ module.exports = {
|
||||
'./jest/setup-reanimated.ts',
|
||||
'./jest/setup-rnfs.ts',
|
||||
'./jest/setup-rntp.ts',
|
||||
'./jest/setup-sentry.ts',
|
||||
'./tamagui.config.ts',
|
||||
'./jest/setup-native-modules.ts',
|
||||
],
|
||||
|
||||
3
jest/setup-sentry.ts
Normal file
3
jest/setup-sentry.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
jest.mock('@sentry/react-native', () => ({
|
||||
init: jest.fn(),
|
||||
}))
|
||||
244
package.json
244
package.json
@@ -1,120 +1,126 @@
|
||||
{
|
||||
"name": "jellify",
|
||||
"version": "0.12.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"init-android": "yarn",
|
||||
"init-ios": "yarn init-ios:new-arch",
|
||||
"init-ios:new-arch": "yarn && yarn pod:install:new-arch",
|
||||
"reinstall": "rm -rf ./node_modules && yarn install",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"tsc": "tsc",
|
||||
"clean:ios": "cd ios && pod deintegrate",
|
||||
"clean:android": "cd android && rm -rf app/ build/",
|
||||
"pod:install": "echo 'Please run `yarn pod:install:new-arch` to enable the new architecture'",
|
||||
"pod:install:new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
|
||||
"pod:clean": "cd ios && pod deintegrate",
|
||||
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
|
||||
"fastlane:ios:match": "cd ios && bundle exec fastlane match development",
|
||||
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
|
||||
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
|
||||
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
|
||||
"prepare": "husky",
|
||||
"format:check": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/cli": "^18.0.0",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-navigation/bottom-tabs": "^7.3.10",
|
||||
"@react-navigation/material-top-tabs": "^7.2.10",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@react-navigation/native-stack": "^7.3.10",
|
||||
"@react-navigation/stack": "^7.2.10",
|
||||
"@tamagui/config": "^1.126.4",
|
||||
"@tanstack/query-sync-storage-persister": "^5.74.6",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@tanstack/react-query-persist-client": "^5.74.6",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
"axios": "^1.9.0",
|
||||
"bundle": "^2.1.0",
|
||||
"gem": "^2.4.3",
|
||||
"invert-color": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "19.0.0",
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.79.1",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-carplay": "^2.4.1-beta.0",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-draggable-flatlist": "^4.0.2",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "^2.25.0",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-mmkv": "3.2.0",
|
||||
"react-native-pager-view": "^6.7.1",
|
||||
"react-native-reanimated": "^3.17.5",
|
||||
"react-native-safe-area-context": "^5.4.0",
|
||||
"react-native-screens": "^4.11.0-beta.2",
|
||||
"react-native-swipeable-item": "^2.0.9",
|
||||
"react-native-text-ticker": "^1.14.0",
|
||||
"react-native-toast-message": "^2.3.0",
|
||||
"react-native-track-player": "git+https://github.com/riteshshukla04/react-native-track-player.git#APM",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"ruby": "^0.6.1",
|
||||
"tamagui": "^1.126.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@react-native-community/cli-platform-android": "18.0.0",
|
||||
"@react-native-community/cli-platform-ios": "18.0.0",
|
||||
"@react-native/babel-preset": "0.79.1",
|
||||
"@react-native/eslint-config": "0.79.1",
|
||||
"@react-native/metro-config": "0.79.1",
|
||||
"@react-native/typescript-config": "0.79.1",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/lodash": "^4.17.10",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "19.0.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.6.3",
|
||||
"jscodeshift": "^0.15.2",
|
||||
"lint-staged": "^15.5.1",
|
||||
"patch-package": "8.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-native-cli-bump-version": "^1.5.1",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
"name": "jellify",
|
||||
"version": "0.12.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"init-android": "yarn",
|
||||
"init-ios": "yarn init-ios:new-arch",
|
||||
"init-ios:new-arch": "yarn && yarn pod:install:new-arch",
|
||||
"reinstall": "rm -rf ./node_modules && yarn install",
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"tsc": "tsc",
|
||||
"clean:ios": "cd ios && pod deintegrate",
|
||||
"clean:android": "cd android && rm -rf app/ build/",
|
||||
"pod:install": "echo 'Please run `yarn pod:install:new-arch` to enable the new architecture'",
|
||||
"pod:install:new-arch": "cd ios && bundle install && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install",
|
||||
"pod:clean": "cd ios && pod deintegrate",
|
||||
"fastlane:ios:build": "cd ios && bundle exec fastlane build",
|
||||
"fastlane:ios:match": "cd ios && bundle exec fastlane match development",
|
||||
"fastlane:ios:beta": "cd ios && bundle exec fastlane beta",
|
||||
"fastlane:android:build": "cd android && bundle install && bundle exec fastlane build",
|
||||
"androidBuild": "cd android && ./gradlew clean && ./gradlew assembleRelease && cd .. && echo 'find apk in android/app/build/outputs/apk/release'",
|
||||
"prepare": "husky",
|
||||
"format:check": "prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/cli": "^18.0.0",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-navigation/bottom-tabs": "^7.3.12",
|
||||
"@react-navigation/material-top-tabs": "^7.2.12",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"@react-navigation/native-stack": "^7.3.12",
|
||||
"@react-navigation/stack": "^7.3.1",
|
||||
"@sentry/react-native": "^6.13.1",
|
||||
"@tamagui/config": "^1.126.9",
|
||||
"@tanstack/query-sync-storage-persister": "^5.75.7",
|
||||
"@tanstack/react-query": "^5.75.7",
|
||||
"@tanstack/react-query-persist-client": "^5.75.7",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
"@typedigital/telemetrydeck-react": "^0.2.0",
|
||||
"axios": "^1.9.0",
|
||||
"bundle": "^2.1.0",
|
||||
"dlx": "^0.2.1",
|
||||
"gem": "^2.4.3",
|
||||
"invert-color": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "19.0.0",
|
||||
"react-freeze": "^1.0.4",
|
||||
"react-native": "0.79.2",
|
||||
"react-native-background-actions": "^4.0.1",
|
||||
"react-native-carplay": "^2.4.1-beta.0",
|
||||
"react-native-device-info": "^14.0.4",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "^2.25.0",
|
||||
"react-native-haptic-feedback": "^2.3.3",
|
||||
"react-native-mmkv": "3.2.0",
|
||||
"react-native-pager-view": "^6.7.1",
|
||||
"react-native-reanimated": "^3.17.5",
|
||||
"react-native-safe-area-context": "^5.4.0",
|
||||
"react-native-screens": "^4.11.0-beta.2",
|
||||
"react-native-swipeable-item": "^2.0.9",
|
||||
"react-native-text-ticker": "^1.14.0",
|
||||
"react-native-toast-message": "^2.3.0",
|
||||
"react-native-track-player": "git+https://github.com/riteshshukla04/react-native-track-player.git#APM",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-vector-icons": "^10.2.0",
|
||||
"ruby": "^0.6.1",
|
||||
"tamagui": "^1.126.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@react-native-community/cli-platform-android": "18.0.0",
|
||||
"@react-native-community/cli-platform-ios": "18.0.0",
|
||||
"@react-native/babel-preset": "0.79.2",
|
||||
"@react-native/eslint-config": "0.79.2",
|
||||
"@react-native/metro-config": "0.79.2",
|
||||
"@react-native/typescript-config": "0.79.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/react": "^19.1.3",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "19.0.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-native": "^5.0.0",
|
||||
"globals": "^16.1.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jscodeshift": "^0.15.2",
|
||||
"lint-staged": "^15.5.2",
|
||||
"patch-package": "8.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-native-cli-bump-version": "^1.5.1",
|
||||
"react-test-renderer": "19.0.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { Platform, useColorScheme } from 'react-native'
|
||||
import JellifyToastConfig from '../../constants/toast.config'
|
||||
import Toast from 'react-native-toast-message'
|
||||
|
||||
@@ -4,8 +4,8 @@ import HorizontalCardList from '../../../components/Global/components/horizontal
|
||||
import { ItemCard } from '../../../components/Global/components/item-card'
|
||||
import { useDiscoverContext } from '../../../providers/Discover'
|
||||
import { View, XStack } from 'tamagui'
|
||||
import { H2 } from '../../../components/Global/helpers/text'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import { H2, H4 } from '../../../components/Global/helpers/text'
|
||||
import Icon from '../../Global/components/icon'
|
||||
|
||||
export default function RecentlyAdded({
|
||||
navigation,
|
||||
@@ -29,12 +29,11 @@ export default function RecentlyAdded({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<H2 marginLeft={'$2'}>Recently Added</H2>
|
||||
<H4 marginLeft={'$2'}>Recently Added</H4>
|
||||
<Icon name='arrow-right' />
|
||||
</XStack>
|
||||
|
||||
<HorizontalCardList
|
||||
squared
|
||||
data={
|
||||
(recentlyAdded?.pages[0].length ?? 0 > 10)
|
||||
? recentlyAdded!.pages[0].slice(0, 10)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import Icon from '../helpers/icon'
|
||||
import Icon from './icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { getTokens, Spinner } from 'tamagui'
|
||||
@@ -43,7 +43,7 @@ export default function FavoriteButton({
|
||||
) : (
|
||||
<Icon
|
||||
name={isFavorite ? 'heart' : 'heart-outline'}
|
||||
color={getTokens().color.telemagenta.val}
|
||||
color={'$primary'}
|
||||
onPress={() =>
|
||||
toggleFavorite(isFavorite, {
|
||||
item,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getToken, Spacer, YStack } from 'tamagui'
|
||||
import Icon from '../helpers/icon'
|
||||
import Icon from './icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
@@ -22,11 +22,7 @@ export default function FavoriteIcon({ item }: { item: BaseItemDto }): React.JSX
|
||||
|
||||
return (
|
||||
<YStack alignContent='center' justifyContent='center' minWidth={24}>
|
||||
{isFavorite ? (
|
||||
<Icon small name='heart' color={getToken('$color.telemagenta')} />
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
{isFavorite ? <Icon small name='heart' color={'$primary'} /> : <Spacer />}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
|
||||
import React from 'react'
|
||||
import { FlatList, FlatListProps, ListRenderItem } from 'react-native'
|
||||
import IconCard from '../helpers/icon-card'
|
||||
|
||||
interface HorizontalCardListProps extends FlatListProps<BaseItemDto> {
|
||||
squared?: boolean | undefined
|
||||
/**
|
||||
* The number of items that will be displayed before
|
||||
* we cut it off and display a "Show More" card
|
||||
*/
|
||||
cutoff?: number | undefined
|
||||
onSeeMore?: () => void | undefined
|
||||
}
|
||||
interface HorizontalCardListProps extends FlatListProps<BaseItemDto> {}
|
||||
|
||||
/**
|
||||
* Displays a Horizontal FlatList of 20 ItemCards
|
||||
@@ -20,8 +11,6 @@ interface HorizontalCardListProps extends FlatListProps<BaseItemDto> {
|
||||
* @returns
|
||||
*/
|
||||
export default function HorizontalCardList({
|
||||
onSeeMore,
|
||||
squared = false,
|
||||
...props
|
||||
}: HorizontalCardListProps): React.JSX.Element {
|
||||
return (
|
||||
@@ -29,16 +18,6 @@ export default function HorizontalCardList({
|
||||
horizontal
|
||||
data={props.data}
|
||||
renderItem={props.renderItem}
|
||||
ListFooterComponent={() => {
|
||||
return props.data && onSeeMore ? (
|
||||
<IconCard
|
||||
name={squared ? 'arrow-right-box' : 'arrow-right-circle'}
|
||||
circular={!squared}
|
||||
caption='See More'
|
||||
onPress={onSeeMore}
|
||||
/>
|
||||
) : undefined
|
||||
}}
|
||||
removeClippedSubviews
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import { useTheme } from 'tamagui'
|
||||
import { ColorTokens, getToken, getTokens, themeable, ThemeTokens, Tokens, useTheme } from 'tamagui'
|
||||
|
||||
const smallSize = 24
|
||||
|
||||
@@ -25,14 +25,14 @@ export default function Icon({
|
||||
large?: boolean
|
||||
disabled?: boolean
|
||||
extraLarge?: boolean
|
||||
color?: string | undefined
|
||||
color?: ThemeTokens | undefined
|
||||
}): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
const size = extraLarge ? extraLargeSize : large ? largeSize : small ? smallSize : regularSize
|
||||
|
||||
return (
|
||||
<MaterialCommunityIcons
|
||||
color={color ? color : theme.color.val}
|
||||
color={color ? theme[color]?.val : theme.color.val}
|
||||
name={name}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
@@ -5,9 +5,8 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { fetchInstantMixFromItem } from '../../../api/queries/instant-mixes'
|
||||
import Icon from '../helpers/icon'
|
||||
import { getToken, Spacer, Spinner } from 'tamagui'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import Icon from './icon'
|
||||
import { Spacer, Spinner } from 'tamagui'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
export default function InstantMixButton({
|
||||
item,
|
||||
@@ -23,11 +22,10 @@ export default function InstantMixButton({
|
||||
staleTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
})
|
||||
|
||||
const isDarkMode = useColorScheme() === 'dark'
|
||||
return data ? (
|
||||
<Icon
|
||||
name='compass-outline'
|
||||
color={isDarkMode ? getToken('$color.success') : getToken('$color.grape')}
|
||||
color={'$success'}
|
||||
onPress={() =>
|
||||
navigation.navigate('InstantMix', {
|
||||
item,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { getTokens, Separator, Spacer, View, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import Icon from '../helpers/icon'
|
||||
import Icon from './icon'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
@@ -102,7 +102,7 @@ export default function Item({
|
||||
|
||||
<XStack justifyContent='space-between' alignItems='center' flex={2}>
|
||||
{item.UserData?.IsFavorite ? (
|
||||
<Icon small color={getTokens().color.telemagenta.val} name='heart' />
|
||||
<Icon small color={'$primary'} name='heart' />
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { ListItem, Separator, Spacer, XStack, YGroup, YStack } from 'tamagui'
|
||||
import ItemImage from './image'
|
||||
import Icon from '../helpers/icon'
|
||||
import Icon from './icon'
|
||||
import { H5, Text } from '../helpers/text'
|
||||
|
||||
interface ListGroupProps {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getToken, getTokens, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import Icon from '../helpers/icon'
|
||||
import Icon from './icon'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
@@ -83,6 +83,7 @@ export default function Track({
|
||||
<XStack
|
||||
alignContent='center'
|
||||
alignItems='center'
|
||||
height={showArtwork ? '$6' : '$5'}
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
if (onPress) {
|
||||
@@ -119,7 +120,6 @@ export default function Track({
|
||||
justifyContent='center'
|
||||
flex={showArtwork ? 2 : 1}
|
||||
marginHorizontal={'$2'}
|
||||
minHeight={showArtwork ? '$4' : 'unset'}
|
||||
>
|
||||
{showArtwork ? (
|
||||
<FastImage
|
||||
@@ -157,8 +157,13 @@ export default function Track({
|
||||
{track.Name ?? 'Untitled Track'}
|
||||
</Text>
|
||||
|
||||
{(showArtwork || (track.ArtistCount ?? 0 > 1)) && (
|
||||
<Text lineBreakStrategyIOS='standard' numberOfLines={1}>
|
||||
{(showArtwork || (track.Artists && track.Artists.length > 1)) && (
|
||||
<Text
|
||||
lineBreakStrategyIOS='standard'
|
||||
numberOfLines={1}
|
||||
bold
|
||||
color={'$borderColor'}
|
||||
>
|
||||
{track.Artists?.join(', ') ?? ''}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Square, Theme } from 'tamagui'
|
||||
import Icon from './icon'
|
||||
import Icon from '../components/icon'
|
||||
import { TouchableOpacity } from 'react-native'
|
||||
import { Text } from './text'
|
||||
|
||||
@@ -40,13 +40,7 @@ export default function IconButton({
|
||||
justifyContent='center'
|
||||
backgroundColor={'$background'}
|
||||
>
|
||||
<Icon
|
||||
large={largeIcon}
|
||||
small={!largeIcon}
|
||||
name={name}
|
||||
color={'$color'}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Icon large={largeIcon} small={!largeIcon} name={name} disabled={disabled} />
|
||||
|
||||
{title && <Text textAlign='center'>{title}</Text>}
|
||||
</Square>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Card, getTokens, View } from 'tamagui'
|
||||
import { H2, H4, H5 } from './text'
|
||||
import Icon from './icon'
|
||||
|
||||
interface IconCardProps {
|
||||
name: string
|
||||
circular?: boolean | undefined
|
||||
onPress: () => void
|
||||
width?: number | undefined
|
||||
caption?: string | undefined
|
||||
largeIcon?: boolean | undefined
|
||||
}
|
||||
|
||||
export default function IconCard({
|
||||
name,
|
||||
circular = false,
|
||||
onPress,
|
||||
width,
|
||||
caption,
|
||||
largeIcon,
|
||||
}: IconCardProps): React.JSX.Element {
|
||||
return (
|
||||
<View alignItems='center' margin={5}>
|
||||
<Card
|
||||
animation='bouncy'
|
||||
borderRadius={circular ? 300 : 5}
|
||||
hoverStyle={{ scale: 0.925 }}
|
||||
pressStyle={{ scale: 0.875 }}
|
||||
width={width ? width : '$12'}
|
||||
height={width ? width : '$12'}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Card.Header>
|
||||
<H5 color={getTokens().color.purpleDark}>{caption ?? ''}</H5>
|
||||
<Icon
|
||||
color={getTokens().color.purpleDark.val}
|
||||
name={name}
|
||||
large={largeIcon}
|
||||
small={!largeIcon}
|
||||
/>
|
||||
</Card.Header>
|
||||
<Card.Footer padded></Card.Footer>
|
||||
<Card.Background
|
||||
backgroundColor={getTokens().color.telemagenta}
|
||||
borderRadius={circular ? 300 : 5}
|
||||
></Card.Background>
|
||||
</Card>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -21,17 +21,13 @@ export function SwitchWithLabel(props: SwitchWithLabelProps) {
|
||||
const id = `switch-${props.size.toString().slice(1)}-${props.checked ?? ''}}`
|
||||
return (
|
||||
<XStack alignItems='center' gap='$3'>
|
||||
<Label size={props.size} htmlFor={id}>
|
||||
{props.label}
|
||||
</Label>
|
||||
<Separator minHeight={20} vertical />
|
||||
<Switch
|
||||
id={id}
|
||||
size={props.size}
|
||||
checked={props.checked}
|
||||
onCheckedChange={(checked: boolean) => props.onCheckedChange(checked)}
|
||||
backgroundColor={
|
||||
props.checked ? getToken('$color.telemagenta') : getToken('$color.purpleGray')
|
||||
props.checked ? getToken('$color.success') : getToken('$color.purpleGray')
|
||||
}
|
||||
borderColor={
|
||||
isDarkMode ? getToken('$color.amethyst') : getToken('$color.purpleDark')
|
||||
@@ -39,6 +35,10 @@ export function SwitchWithLabel(props: SwitchWithLabelProps) {
|
||||
>
|
||||
<JellifySliderThumb animation='bouncy' />
|
||||
</Switch>
|
||||
<Separator minHeight={20} vertical />
|
||||
<Label size={props.size} htmlFor={id}>
|
||||
{props.label}
|
||||
</Label>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react'
|
||||
import { ItemCard } from '../../../components/Global/components/item-card'
|
||||
import { View, XStack } from 'tamagui'
|
||||
import { H2, H4, Text } from '../../../components/Global/helpers/text'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useHomeContext } from '../../../providers/Home'
|
||||
import { ActivityIndicator } from 'react-native'
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import HorizontalCardList from '../../../components/Global/components/horizontal
|
||||
import { ItemCard } from '../../../components/Global/components/item-card'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { H4 } from '../../../components/Global/helpers/text'
|
||||
|
||||
@@ -6,7 +6,7 @@ import { StackParamList } from '../../types'
|
||||
import { ItemCard } from '../../Global/components/item-card'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
|
||||
export default function RecentArtists({
|
||||
navigation,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
|
||||
export default function RecentlyPlayed({
|
||||
@@ -41,7 +41,6 @@ export default function RecentlyPlayed({
|
||||
</XStack>
|
||||
|
||||
<HorizontalCardList
|
||||
squared
|
||||
data={
|
||||
(recentTracks?.pages.flatMap((page) => page).length ?? 0 > 10)
|
||||
? recentTracks?.pages.flatMap((page) => page).slice(0, 10)
|
||||
|
||||
@@ -3,9 +3,9 @@ import { RouteProp } from '@react-navigation/native'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
|
||||
import PlaylistsScreen from './components/playlists-tab'
|
||||
import { getToken } from 'tamagui'
|
||||
import { useTheme } from 'tamagui'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import TracksTab from './components/tracks-tab'
|
||||
import ArtistsTab from './components/artists-tab'
|
||||
import AlbumsTab from './components/albums-tab'
|
||||
@@ -21,6 +21,7 @@ export default function Library({
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const isDarkMode = useColorScheme() === 'dark'
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<LibraryTabsNavigator.Navigator
|
||||
@@ -28,10 +29,8 @@ export default function Library({
|
||||
screenOptions={{
|
||||
lazy: true,
|
||||
tabBarShowIcon: true,
|
||||
tabBarActiveTintColor: getToken('$color.telemagenta'),
|
||||
tabBarInactiveTintColor: isDarkMode
|
||||
? getToken('$color.amethyst')
|
||||
: getToken('$color.purpleGray'),
|
||||
tabBarActiveTintColor: theme.primary.val,
|
||||
tabBarInactiveTintColor: theme.borderColor.val,
|
||||
tabBarLabelStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
@@ -42,7 +41,11 @@ export default function Library({
|
||||
component={ArtistsTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon name='microphone-variant' color={color} small />
|
||||
<Icon
|
||||
name='microphone-variant'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -52,7 +55,11 @@ export default function Library({
|
||||
component={AlbumsTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon name='music-box-multiple' color={color} small />
|
||||
<Icon
|
||||
name='music-box-multiple'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
initialParams={{ navigation }}
|
||||
@@ -63,7 +70,11 @@ export default function Library({
|
||||
component={TracksTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon name='music-clef-treble' color={color} small />
|
||||
<Icon
|
||||
name='music-clef-treble'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@@ -73,7 +84,11 @@ export default function Library({
|
||||
component={PlaylistsScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon name='playlist-music' color={color} small />
|
||||
<Icon
|
||||
name='playlist-music'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
initialParams={{ navigation }}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { MaterialTopTabBar, MaterialTopTabBarProps } from '@react-navigation/material-top-tabs'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Button, getToken, Separator, XStack, YStack } from 'tamagui'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import { Separator, XStack, YStack } from 'tamagui'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { useLibrarySortAndFilterContext } from '../../providers/Library/sorting-filtering'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import { useLibraryContext } from '../../providers/Library'
|
||||
|
||||
export default function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
useEffect(() => {
|
||||
@@ -40,12 +39,9 @@ export default function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
>
|
||||
<Icon
|
||||
name={'plus-circle-outline'}
|
||||
color={getToken('$color.telemagenta')}
|
||||
/>
|
||||
<Icon name={'plus-circle-outline'} color={'$primary'} />
|
||||
|
||||
<Text color={'$telemagenta'}>Create Playlist</Text>
|
||||
<Text color={'$primary'}>Create Playlist</Text>
|
||||
</XStack>
|
||||
) : (
|
||||
<XStack
|
||||
@@ -56,20 +52,10 @@ export default function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
>
|
||||
<Icon
|
||||
name={isFavorites ? 'heart' : 'heart-outline'}
|
||||
color={
|
||||
isFavorites
|
||||
? getToken('$color.telemagenta')
|
||||
: getToken('$color.purpleGray')
|
||||
}
|
||||
color={isFavorites ? '$primary' : '$borderColor'}
|
||||
/>
|
||||
|
||||
<Text
|
||||
color={
|
||||
isFavorites
|
||||
? getToken('$color.telemagenta')
|
||||
: getToken('$color.purpleGray')
|
||||
}
|
||||
>
|
||||
<Text color={isFavorites ? '$primary' : '$borderColor'}>
|
||||
{isFavorites ? 'Favorites' : 'All'}
|
||||
</Text>
|
||||
</XStack>
|
||||
@@ -90,20 +76,10 @@ export default function LibraryTabBar(props: MaterialTopTabBarProps) {
|
||||
? 'sort-alphabetical-descending'
|
||||
: 'sort-alphabetical-ascending'
|
||||
}
|
||||
color={
|
||||
sortDescending
|
||||
? getToken('$color.success')
|
||||
: getToken('$color.purpleGray')
|
||||
}
|
||||
color={sortDescending ? '$success' : '$borderColor'}
|
||||
/>
|
||||
|
||||
<Text
|
||||
color={
|
||||
sortDescending
|
||||
? getToken('$color.success')
|
||||
: getToken('$color.purpleGray')
|
||||
}
|
||||
>
|
||||
<Text color={sortDescending ? '$success' : '$borderColor'}>
|
||||
{sortDescending ? 'Descending' : 'Ascending'}
|
||||
</Text>
|
||||
</XStack>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'
|
||||
import _ from 'lodash'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { JellifyServer } from '../../../types/JellifyServer'
|
||||
import { Input, Spinner, YStack } from 'tamagui'
|
||||
import { Input, ListItem, Separator, Spacer, Spinner, XStack, YGroup, YStack } from 'tamagui'
|
||||
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
|
||||
import { H2 } from '../../Global/helpers/text'
|
||||
import Button from '../../Global/helpers/button'
|
||||
@@ -15,6 +15,8 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useSettingsContext } from '../../../providers/Settings'
|
||||
import Icon from '../../Global/components/icon'
|
||||
|
||||
export default function ServerAddress({
|
||||
navigation,
|
||||
@@ -26,6 +28,8 @@ export default function ServerAddress({
|
||||
|
||||
const { server, setServer, signOut } = useJellifyContext()
|
||||
|
||||
const { setSendMetrics, sendMetrics } = useSettingsContext()
|
||||
|
||||
useEffect(() => {
|
||||
signOut()
|
||||
}, [])
|
||||
@@ -87,15 +91,7 @@ export default function ServerAddress({
|
||||
</H2>
|
||||
</YStack>
|
||||
|
||||
<YStack marginHorizontal={'$2'}>
|
||||
<SwitchWithLabel
|
||||
checked={useHttps}
|
||||
onCheckedChange={(checked) => setUseHttps(checked)}
|
||||
label='Use HTTPS'
|
||||
size='$2'
|
||||
width={100}
|
||||
/>
|
||||
|
||||
<YStack marginHorizontal={'$4'} gap={'$4'}>
|
||||
<Input
|
||||
onChangeText={setServerAddress}
|
||||
autoCapitalize='none'
|
||||
@@ -103,6 +99,57 @@ export default function ServerAddress({
|
||||
placeholder='jellyfin.org'
|
||||
/>
|
||||
|
||||
<YGroup
|
||||
gap={'$2'}
|
||||
borderColor={'$borderColor'}
|
||||
borderWidth={'$0.5'}
|
||||
borderRadius={'$4'}
|
||||
>
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
icon={
|
||||
<Icon
|
||||
name={useHttps ? 'lock-check' : 'lock-off'}
|
||||
color={useHttps ? '$success' : '$borderColor'}
|
||||
/>
|
||||
}
|
||||
title='HTTPS'
|
||||
subTitle='Use HTTPS to connect to Jellyfin'
|
||||
>
|
||||
<SwitchWithLabel
|
||||
checked={useHttps}
|
||||
onCheckedChange={(checked) => setUseHttps(checked)}
|
||||
label={useHttps ? 'Use HTTPS' : 'Use HTTP'}
|
||||
size='$2'
|
||||
width={100}
|
||||
/>
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
|
||||
<Separator />
|
||||
|
||||
<YGroup.Item>
|
||||
<ListItem
|
||||
icon={
|
||||
<Icon
|
||||
name={sendMetrics ? 'bug-check' : 'bug'}
|
||||
color={sendMetrics ? '$success' : '$borderColor'}
|
||||
/>
|
||||
}
|
||||
title='Submit Usage and Crash Data'
|
||||
subTitle='Send anonymized metrics and crash data'
|
||||
>
|
||||
<SwitchWithLabel
|
||||
checked={sendMetrics}
|
||||
onCheckedChange={(checked) => setSendMetrics(checked)}
|
||||
label='Send Metrics'
|
||||
size='$2'
|
||||
width={100}
|
||||
/>
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
</YGroup>
|
||||
|
||||
{useServerMutation.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { JellifyUser } from '../../../types/JellifyUser'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
import Input from '../../../components/Global/helpers/input'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import Toast from 'react-native-toast-message'
|
||||
@@ -70,15 +70,9 @@ export default function ServerAuthentication({
|
||||
{`Sign in to ${server?.name ?? 'Jellyfin'}`}
|
||||
</H2>
|
||||
</YStack>
|
||||
<YStack marginHorizontal={'$2'}>
|
||||
<YStack marginHorizontal={'$4'}>
|
||||
<Input
|
||||
prependElement={
|
||||
<Icon
|
||||
small
|
||||
name='human-greeting-variant'
|
||||
color={getToken('$color.amethyst')}
|
||||
/>
|
||||
}
|
||||
prependElement={<Icon name='human-greeting-variant' color={'$borderColor'} />}
|
||||
placeholder='Username'
|
||||
value={username}
|
||||
onChangeText={(value: string | undefined) => setUsername(value)}
|
||||
@@ -89,9 +83,7 @@ export default function ServerAuthentication({
|
||||
<Spacer />
|
||||
|
||||
<Input
|
||||
prependElement={
|
||||
<Icon small name='lock-outline' color={getToken('$color.amethyst')} />
|
||||
}
|
||||
prependElement={<Icon name='lock-outline' color={'$borderColor'} />}
|
||||
placeholder='Password'
|
||||
value={password}
|
||||
onChangeText={(value: string | undefined) => setPassword(value)}
|
||||
|
||||
@@ -41,10 +41,13 @@ export default function ServerLibrary({
|
||||
}, [isPending, isSuccess])
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<YStack marginHorizontal={'$2'}>
|
||||
<H2>Select Music Library</H2>
|
||||
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<YStack maxHeight={'$19'} flex={1} justifyContent='center'>
|
||||
<H2 marginHorizontal={'$2'} textAlign='center'>
|
||||
Select Music Library
|
||||
</H2>
|
||||
</YStack>
|
||||
<YStack marginHorizontal={'$4'}>
|
||||
{isPending ? (
|
||||
<Spinner size='large' />
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { State } from 'react-native-track-player'
|
||||
import { Colors } from 'react-native/Libraries/NewAppScreen'
|
||||
import { Circle, Spinner, View } from 'tamagui'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import IconButton from '../../../components/Global/helpers/icon-button'
|
||||
@@ -31,7 +30,7 @@ export default function PlayPauseButton({
|
||||
case State.Loading: {
|
||||
button = (
|
||||
<Circle size={size} disabled>
|
||||
<Spinner marginHorizontal={10} size='small' color={Colors.Primary} />
|
||||
<Spinner marginHorizontal={10} size='small' color={'$borderColor'} />
|
||||
</Circle>
|
||||
)
|
||||
break
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { XStack, getToken } from 'tamagui'
|
||||
import PlayPauseButton from './buttons'
|
||||
import Icon from '../../../components/Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { usePlayerContext } from '../../../providers/Player'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { useQueueContext } from '../../../providers/Player/queue'
|
||||
@@ -15,14 +15,10 @@ export default function Controls(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<XStack alignItems='center' justifyContent='space-evenly' marginVertical={'$4'}>
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
name='rewind-15'
|
||||
onPress={() => useSeekBy.mutate(-15)}
|
||||
/>
|
||||
<Icon color={'$borderColor'} name='rewind-15' onPress={() => useSeekBy.mutate(-15)} />
|
||||
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
color={'$borderColor'}
|
||||
name='skip-previous'
|
||||
onPress={() => usePrevious.mutate()}
|
||||
large
|
||||
@@ -32,14 +28,14 @@ export default function Controls(): React.JSX.Element {
|
||||
<PlayPauseButton size={getToken('$13') - getToken('$5')} />
|
||||
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
color={'$borderColor'}
|
||||
name='skip-next'
|
||||
onPress={() => useSkip.mutate(undefined)}
|
||||
large
|
||||
/>
|
||||
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
color={'$borderColor'}
|
||||
name='fast-forward-15'
|
||||
onPress={() => useSeekBy.mutate(15)}
|
||||
/>
|
||||
|
||||
35
src/components/Player/helpers/footer.tsx
Normal file
35
src/components/Player/helpers/footer.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { YStack } from 'tamagui'
|
||||
|
||||
import { XStack, Spacer } from 'tamagui'
|
||||
|
||||
import Icon from '../../Global/components/icon'
|
||||
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../../components/types'
|
||||
|
||||
export default function Footer({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<YStack justifyContent='flex-end'>
|
||||
<XStack justifyContent='space-evenly' marginVertical={'$3'}>
|
||||
<Icon name='speaker-multiple' />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Icon
|
||||
name='playlist-music'
|
||||
onPress={() => {
|
||||
navigation.navigate('Queue')
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { SafeAreaView, useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { YStack, XStack, Spacer, getTokens, getToken, useTheme } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import FavoriteButton from '../Global/components/favorite-button'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from './component.config'
|
||||
@@ -19,6 +19,7 @@ import JellifyToastConfig from '../../constants/toast.config'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import { useFocusEffect } from '@react-navigation/native'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import Footer from './helpers/footer'
|
||||
export default function PlayerScreen({
|
||||
navigation,
|
||||
}: {
|
||||
@@ -201,24 +202,7 @@ export default function PlayerScreen({
|
||||
|
||||
<Controls />
|
||||
|
||||
<YStack justifyContent='flex-end' height={'$10'} maxHeight={height / 10}>
|
||||
<XStack justifyContent='space-evenly' marginVertical={'$3'}>
|
||||
<Icon name='speaker-multiple' />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Icon
|
||||
name='playlist-music'
|
||||
onPress={() => {
|
||||
navigation.navigate('Queue')
|
||||
}}
|
||||
/>
|
||||
</XStack>
|
||||
</YStack>
|
||||
<Footer navigation={navigation} />
|
||||
</YStack>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getToken, getTokens, Image, useTheme, View, XStack, YStack } from 'tama
|
||||
import { usePlayerContext } from '../../providers/Player'
|
||||
import { BottomTabNavigationEventMap } from '@react-navigation/bottom-tabs'
|
||||
import { NavigationHelpers, ParamListBase } from '@react-navigation/native'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import PlayPauseButton from './helpers/buttons'
|
||||
@@ -79,7 +79,7 @@ export function Miniplayer({
|
||||
|
||||
<Icon
|
||||
large
|
||||
color={theme.borderColor.val}
|
||||
color={'$borderColor'}
|
||||
name='skip-next'
|
||||
onPress={() => useSkip.mutate(undefined)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import Track from '../Global/components/track'
|
||||
import { StackParamList } from '../types'
|
||||
import { usePlayerContext } from '../../providers/Player'
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { getToken, getTokens, Separator, View, XStack, YStack } from 'tamagui'
|
||||
import { AnimatedH5 } from '../../Global/helpers/text'
|
||||
import InstantMixButton from '../../Global/components/instant-mix-button'
|
||||
import Icon from '../../Global/helpers/icon'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { usePlaylistContext } from '../../../providers/Playlist'
|
||||
import Animated, { useAnimatedStyle, withSpring } from 'react-native-reanimated'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
@@ -136,7 +136,7 @@ function PlaylistHeaderControls({
|
||||
<YStack justifyContent='center' alignContent='center'>
|
||||
{editing ? (
|
||||
<Icon
|
||||
color={getToken('$color.danger')}
|
||||
color={'$danger'}
|
||||
name='delete-sweep-outline' // otherwise use "delete-circle"
|
||||
onPress={() => navigation.navigate('DeletePlaylist', { playlist })}
|
||||
/>
|
||||
@@ -147,7 +147,7 @@ function PlaylistHeaderControls({
|
||||
|
||||
<YStack justifyContent='center' alignContent='center'>
|
||||
<Icon
|
||||
color={getToken('$color.amethyst')}
|
||||
color={'$borderColor'}
|
||||
name={editing ? 'content-save-outline' : 'pencil'}
|
||||
onPress={() => setEditing(!editing)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Separator, XStack } from 'tamagui'
|
||||
import Track from '../Global/components/track'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { RefreshControl } from 'react-native'
|
||||
import { PlaylistProps } from './interfaces'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FlatList, RefreshControl } from 'react-native-gesture-handler'
|
||||
import { ItemCard } from '../Global/components/item-card'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { getToken, getTokens } from 'tamagui'
|
||||
import { fetchFavoritePlaylists } from '../../api/queries/favorites'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { XStack } from '@tamagui/stacks'
|
||||
import React from 'react'
|
||||
import { Text } from '../../components/Global/helpers/text'
|
||||
import Icon from '../../components/Global/helpers/icon'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
export default function AccountDetails(): React.JSX.Element {
|
||||
const { user } = useJellifyContext()
|
||||
|
||||
return (
|
||||
<XStack alignItems='center'>
|
||||
<Icon name='account-music-outline' />
|
||||
<Text>{user!.name}</Text>
|
||||
</XStack>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
|
||||
interface CategoryRoute {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
name: any // ¯\_(ツ)_/¯
|
||||
iconName: string
|
||||
params?: {
|
||||
query: QueryKeys
|
||||
}
|
||||
}
|
||||
|
||||
const Categories: CategoryRoute[] = [
|
||||
{ name: 'Account', iconName: 'account-key-outline' },
|
||||
{ name: 'Server', iconName: 'server-network' },
|
||||
{ name: 'Playback', iconName: 'disc-player' },
|
||||
{ name: 'Labs', iconName: 'flask-outline' },
|
||||
]
|
||||
|
||||
export default Categories
|
||||
@@ -1,42 +1,80 @@
|
||||
import React from 'react'
|
||||
import SignOut from '../../components/Settings/sign-out'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../types'
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context'
|
||||
import { FlatList } from 'react-native'
|
||||
import IconCard from '../Global/helpers/icon-card'
|
||||
import Categories from '../../components/Settings/categories'
|
||||
import StorageBar from '../Storage'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
|
||||
import { getToken, useTheme } from 'tamagui'
|
||||
import AccountTab from './components/account-tab'
|
||||
import Icon from '../Global/components/icon'
|
||||
import LabsTab from './components/labs-tab'
|
||||
import PreferencesTab from './components/preferences-tab'
|
||||
import InfoTab from './components/info-tab'
|
||||
|
||||
export default function Root({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
const { width } = useSafeAreaFrame()
|
||||
const SettingsTabsNavigator = createMaterialTopTabNavigator()
|
||||
|
||||
export default function Settings(): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
contentInsetAdjustmentBehavior='automatic'
|
||||
data={Categories}
|
||||
numColumns={2}
|
||||
renderItem={({ index, item }) => (
|
||||
<IconCard
|
||||
name={item.iconName}
|
||||
caption={item.name}
|
||||
width={width / 2.1}
|
||||
onPress={() => {
|
||||
navigation.navigate(item.name, item.params)
|
||||
}}
|
||||
largeIcon
|
||||
/>
|
||||
)}
|
||||
ListFooterComponent={
|
||||
<>
|
||||
<StorageBar />
|
||||
<SignOut />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SettingsTabsNavigator.Navigator
|
||||
screenOptions={{
|
||||
tabBarShowIcon: true,
|
||||
tabBarActiveTintColor: theme.primary.val,
|
||||
tabBarInactiveTintColor: theme.borderColor.val,
|
||||
tabBarLabelStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Settings'
|
||||
component={PreferencesTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon
|
||||
name='headphones-settings'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Account'
|
||||
component={AccountTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon
|
||||
name='account-music'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='Labs'
|
||||
component={LabsTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon name='flask' color={focused ? '$primary' : '$borderColor'} small />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsTabsNavigator.Screen
|
||||
name='About'
|
||||
component={InfoTab}
|
||||
options={{
|
||||
tabBarIcon: ({ focused, color }) => (
|
||||
<Icon
|
||||
name='information'
|
||||
color={focused ? '$primary' : '$borderColor'}
|
||||
small
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</SettingsTabsNavigator.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
47
src/components/Settings/components/account-tab.tsx
Normal file
47
src/components/Settings/components/account-tab.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import SignOut from './sign-out-button'
|
||||
import { SettingsStackParamList } from '../../../screens/Settings/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
|
||||
export default function AccountTab(): React.JSX.Element {
|
||||
const { user, library, server } = useJellifyContext()
|
||||
|
||||
const navigation = useNavigation<NativeStackNavigationProp<SettingsStackParamList>>()
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Username',
|
||||
subTitle: "You're awesome!",
|
||||
iconName: 'account-music',
|
||||
iconColor: '$borderColor',
|
||||
children: <Text>{user?.name ?? 'Unknown User'}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Selected Library',
|
||||
subTitle: '',
|
||||
iconName: 'book-music',
|
||||
iconColor: '$borderColor',
|
||||
children: <Text>{library?.musicLibraryName ?? 'Unknown Library'}</Text>,
|
||||
},
|
||||
{
|
||||
title: 'Jellyfin Server',
|
||||
subTitle: server?.version ?? 'Unknown Jellyfin Version',
|
||||
iconName: 'server',
|
||||
iconColor: '$borderColor',
|
||||
children: <Text>{server?.name ?? 'Unknown Server'}</Text>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<SignOut navigation={navigation} />
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
34
src/components/Settings/components/info-tab.tsx
Normal file
34
src/components/Settings/components/info-tab.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { getToken, ListItem, Progress, Separator, YGroup } from 'tamagui'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { version } from '../../../../package.json'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import { useNetworkContext } from '../../../providers/Network'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
|
||||
export default function InfoTab() {
|
||||
const { downloadedTracks, storageUsage } = useNetworkContext()
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Storage',
|
||||
subTitle: `${downloadedTracks?.length ?? '0'} ${
|
||||
downloadedTracks?.length === 1 ? 'song' : 'songs'
|
||||
} in your pocket`,
|
||||
iconName: 'harddisk',
|
||||
iconColor: '$borderColor',
|
||||
},
|
||||
{
|
||||
title: 'Jellify',
|
||||
subTitle: 'Made with 💜 by Violet Caulfield',
|
||||
iconName: 'jellyfish',
|
||||
iconColor: '$borderColor',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
21
src/components/Settings/components/labs-tab.tsx
Normal file
21
src/components/Settings/components/labs-tab.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ListItem, View, YGroup } from 'tamagui'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
|
||||
export default function LabsTab(): React.JSX.Element {
|
||||
return (
|
||||
<SettingsListGroup
|
||||
borderColor={'$danger'}
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Nothing to see here...(yet)',
|
||||
subTitle: 'Come back later to enable beta features',
|
||||
iconName: 'test-tube-off',
|
||||
iconColor: '$danger',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
31
src/components/Settings/components/preferences-tab.tsx
Normal file
31
src/components/Settings/components/preferences-tab.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { getToken } from 'tamagui'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useSettingsContext } from '../../../providers/Settings'
|
||||
import { SwitchWithLabel } from '../../Global/helpers/switch-with-label'
|
||||
import SettingsListGroup from './settings-list-group'
|
||||
|
||||
export default function PreferencesTab(): React.JSX.Element {
|
||||
const { setSendMetrics, sendMetrics } = useSettingsContext()
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SettingsListGroup
|
||||
settingsList={[
|
||||
{
|
||||
title: 'Send Metrics and Crash Reports',
|
||||
iconName: sendMetrics ? 'bug-check' : 'bug',
|
||||
iconColor: sendMetrics ? '$success' : '$borderColor',
|
||||
subTitle: 'Send anonymous usage and crash data',
|
||||
children: (
|
||||
<SwitchWithLabel
|
||||
checked={sendMetrics}
|
||||
onCheckedChange={setSendMetrics}
|
||||
size={'$2'}
|
||||
label={sendMetrics ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
42
src/components/Settings/components/settings-list-group.tsx
Normal file
42
src/components/Settings/components/settings-list-group.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { ListItem, Separator, YGroup } from 'tamagui'
|
||||
import { SettingsTabList } from '../types'
|
||||
import Icon from '../../Global/components/icon'
|
||||
import { ThemeTokens } from 'tamagui'
|
||||
import React from 'react'
|
||||
|
||||
interface SettingsListGroupProps {
|
||||
settingsList: SettingsTabList
|
||||
borderColor?: ThemeTokens
|
||||
}
|
||||
|
||||
export default function SettingsListGroup({
|
||||
settingsList,
|
||||
borderColor,
|
||||
}: SettingsListGroupProps): React.JSX.Element {
|
||||
return (
|
||||
<YGroup
|
||||
alignSelf='center'
|
||||
borderColor={borderColor ?? '$borderColor'}
|
||||
borderWidth={'$1'}
|
||||
borderRadius={'$4'}
|
||||
margin={'$4'}
|
||||
>
|
||||
{settingsList.map((setting, index, self) => (
|
||||
<>
|
||||
<YGroup.Item key={setting.title}>
|
||||
<ListItem
|
||||
size={'$5'}
|
||||
title={setting.title}
|
||||
icon={<Icon name={setting.iconName} color={setting.iconColor} />}
|
||||
subTitle={setting.subTitle}
|
||||
>
|
||||
{setting.children}
|
||||
</ListItem>
|
||||
</YGroup.Item>
|
||||
|
||||
{index !== self.length - 1 && <Separator />}
|
||||
</>
|
||||
))}
|
||||
</YGroup>
|
||||
)
|
||||
}
|
||||
26
src/components/Settings/components/sign-out-button.tsx
Normal file
26
src/components/Settings/components/sign-out-button.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { SettingsStackParamList } from '../../../screens/Settings/types'
|
||||
import { Text } from '../../Global/helpers/text'
|
||||
|
||||
export default function SignOut({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<SettingsStackParamList>
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<Button
|
||||
color={'$danger'}
|
||||
borderColor={'$danger'}
|
||||
marginHorizontal={'$6'}
|
||||
onPress={() => {
|
||||
navigation.navigate('SignOut')
|
||||
}}
|
||||
>
|
||||
<Text bold color={'$danger'}>
|
||||
Sign Out
|
||||
</Text>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ScrollView } from 'tamagui'
|
||||
|
||||
export default function DevTools(): React.JSX.Element {
|
||||
return (
|
||||
<ScrollView contentInsetAdjustmentBehavior='automatic' removeClippedSubviews></ScrollView>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Text } from '../../components/Global/helpers/text'
|
||||
import React from 'react'
|
||||
import { View } from 'tamagui'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
export default function LibraryDetails(): React.JSX.Element {
|
||||
const { library } = useJellifyContext()
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>{`LibraryID: ${library!.musicLibraryId}`}</Text>
|
||||
<Text>{`Playlist LibraryID: ${library!.playlistLibraryId}`}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react'
|
||||
import Button from '../Global/helpers/button'
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { StackParamList } from '../types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
|
||||
export default function SignOut(): React.JSX.Element {
|
||||
const navigation = useNavigation<NativeStackNavigationProp<StackParamList>>()
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={() => {
|
||||
TrackPlayer.reset()
|
||||
.then(() => {
|
||||
console.debug('TrackPlayer cleared')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error clearing TrackPlayer', error)
|
||||
})
|
||||
.finally(() => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: 'Login',
|
||||
params: {
|
||||
screen: 'ServerAddress',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}}
|
||||
>
|
||||
Sign Out
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
7
src/components/Settings/types.d.ts
vendored
Normal file
7
src/components/Settings/types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export type SettingsTabList = {
|
||||
title: string
|
||||
iconName: string
|
||||
iconColor: ThemeTokens
|
||||
subTitle: string
|
||||
children?: React.ReactNode
|
||||
}[]
|
||||
@@ -4,7 +4,7 @@ import RNFS from 'react-native-fs'
|
||||
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
|
||||
import { deleteAudioCache } from '../../components/Network/offlineModeUtils'
|
||||
import { useNetworkContext } from '../../providers/Network'
|
||||
import Icon from '../Global/helpers/icon'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { getToken, View } from 'tamagui'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ import { JellifyUserDataProvider } from '../providers/UserData'
|
||||
import { NetworkContextProvider } from '../providers/Network'
|
||||
import { QueueProvider } from '../providers/Player/queue'
|
||||
import { DisplayProvider } from '../providers/Display/display-provider'
|
||||
import { SettingsProvider, useSettingsContext } from '../providers/Settings'
|
||||
import { createTelemetryDeck, TelemetryDeckProvider } from '@typedigital/telemetrydeck-react'
|
||||
import telemetryDeckConfig from '../../telemetrydeck.json'
|
||||
import glitchtipConfig from '../../glitchtip.json'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
/**
|
||||
* The main component for the Jellify app. Children are wrapped in the {@link JellifyProvider}
|
||||
@@ -14,13 +19,38 @@ import { DisplayProvider } from '../providers/Display/display-provider'
|
||||
*/
|
||||
export default function Jellify(): React.JSX.Element {
|
||||
return (
|
||||
<DisplayProvider>
|
||||
<JellifyProvider>
|
||||
<App />
|
||||
</JellifyProvider>
|
||||
</DisplayProvider>
|
||||
<SettingsProvider>
|
||||
<JellifyLoggingWrapper>
|
||||
<DisplayProvider>
|
||||
<JellifyProvider>
|
||||
<App />
|
||||
</JellifyProvider>
|
||||
</DisplayProvider>
|
||||
</JellifyLoggingWrapper>
|
||||
</SettingsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function JellifyLoggingWrapper({ children }: { children: React.ReactNode }): React.JSX.Element {
|
||||
const { sendMetrics } = useSettingsContext()
|
||||
|
||||
let telemetrydeck = undefined
|
||||
if (sendMetrics) {
|
||||
telemetrydeck = createTelemetryDeck(telemetryDeckConfig)
|
||||
}
|
||||
|
||||
Sentry.init({
|
||||
...glitchtipConfig,
|
||||
enabled: sendMetrics,
|
||||
})
|
||||
|
||||
return sendMetrics && telemetrydeck ? (
|
||||
<TelemetryDeckProvider telemetryDeck={telemetrydeck}>{children}</TelemetryDeckProvider>
|
||||
) : (
|
||||
<>{children}</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The main component for the Jellify app. Depends on {@link useJellifyContext} hook to determine if the user is logged in
|
||||
* @returns The {@link App} component
|
||||
|
||||
@@ -5,7 +5,7 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI
|
||||
import SettingsScreen from '../screens/Settings'
|
||||
import { Discover } from '../screens/Discover'
|
||||
import { Miniplayer } from './Player/mini-player'
|
||||
import { getToken, getTokens, Separator } from 'tamagui'
|
||||
import { getToken, getTokens, Separator, useTheme } from 'tamagui'
|
||||
import { usePlayerContext } from '../providers/Player'
|
||||
import SearchStack from '../screens/Search'
|
||||
import LibraryStack from '../screens/Library'
|
||||
@@ -15,7 +15,7 @@ import InternetConnectionWatcher from './Network/internetConnectionWatcher'
|
||||
const Tab = createBottomTabNavigator()
|
||||
|
||||
export function Tabs(): React.JSX.Element {
|
||||
const isDarkMode = useColorScheme() === 'dark'
|
||||
const theme = useTheme()
|
||||
const { nowPlaying } = usePlayerContext()
|
||||
|
||||
return (
|
||||
@@ -23,13 +23,8 @@ export function Tabs(): React.JSX.Element {
|
||||
initialRouteName='Home'
|
||||
screenOptions={{
|
||||
animation: 'shift',
|
||||
tabBarActiveTintColor: getTokens().color.telemagenta.val,
|
||||
tabBarInactiveTintColor: isDarkMode
|
||||
? getToken('$color.amethyst')
|
||||
: getToken('$color.purpleGray'),
|
||||
tabBarLabelStyle: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
tabBarActiveTintColor: theme.primary.val,
|
||||
tabBarInactiveTintColor: theme.borderColor.val,
|
||||
}}
|
||||
tabBar={(props) => (
|
||||
<>
|
||||
|
||||
1
src/components/types.d.ts
vendored
1
src/components/types.d.ts
vendored
@@ -73,6 +73,7 @@ export type StackParamList = {
|
||||
Server: undefined
|
||||
Playback: undefined
|
||||
Labs: undefined
|
||||
SignOut: undefined
|
||||
|
||||
Tabs: {
|
||||
screen: keyof StackParamList
|
||||
|
||||
@@ -12,4 +12,6 @@ export enum MMKVStorageKeys {
|
||||
Api = 'Api',
|
||||
LibrarySortDescending = 'LibrarySortDescending',
|
||||
LibraryIsFavorites = 'LibraryIsFavorites',
|
||||
SendMetrics = 'SEND_METRICS',
|
||||
AutoDownload = 'AutoDownload',
|
||||
}
|
||||
|
||||
@@ -78,4 +78,5 @@ export enum QueryKeys {
|
||||
AllArtists = 'AllArtists',
|
||||
AllTracks = 'AllTracks',
|
||||
AllAlbums = 'AllAlbums',
|
||||
StorageInUse = 'StorageInUse',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { SharedValue } from 'react-native-reanimated'
|
||||
|
||||
/**
|
||||
* Converts the run time seconds of a track to the RunTimeTicks standard set by Emby / Jellyfin
|
||||
* @param seconds The run time seconds of the item to convert to Jellyfin ticks
|
||||
|
||||
@@ -47,7 +47,8 @@ export async function handlePlaybackProgress(
|
||||
track: JellifyTrack,
|
||||
progress: Progress,
|
||||
) {
|
||||
if (Math.floor(progress.duration - progress.position) === 5) {
|
||||
console.debug('Playback progress updated')
|
||||
if (Math.floor(progress.duration) - Math.floor(progress.position) <= 9) {
|
||||
console.debug(`Track finished. ${playstateApi ? 'scrobbling...' : ''}`)
|
||||
|
||||
if (playstateApi)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React, { createContext, ReactNode, useContext } from 'react'
|
||||
import { JellifyDownload } from '../../types/JellifyDownload'
|
||||
import { useMutation, UseMutationResult, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
useMutation,
|
||||
UseMutationResult,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
} from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { mapDtoToTrack } from '../../helpers/mappings'
|
||||
import { deleteAudio, getAudioCache, saveAudio } from '../../components/Network/offlineModeUtils'
|
||||
@@ -9,9 +15,13 @@ import { networkStatusTypes } from '../../components/Network/internetConnectionW
|
||||
import DownloadProgress from '../../types/DownloadProgress'
|
||||
import { useJellifyContext } from '..'
|
||||
import { isUndefined } from 'lodash'
|
||||
import RNFS from 'react-native-fs'
|
||||
import { JellifyStorage } from './types'
|
||||
|
||||
interface NetworkContext {
|
||||
useDownload: UseMutationResult<void, Error, BaseItemDto, unknown>
|
||||
useRemoveDownload: UseMutationResult<void, Error, BaseItemDto, unknown>
|
||||
storageUsage: JellifyStorage | undefined
|
||||
downloadedTracks: JellifyDownload[] | undefined
|
||||
activeDownloads: DownloadProgress[] | undefined
|
||||
networkStatus: networkStatusTypes | undefined
|
||||
@@ -21,6 +31,17 @@ const NetworkContextInitializer = () => {
|
||||
const { api, sessionId } = useJellifyContext()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const fetchStorageInUse: () => Promise<JellifyStorage> = async () => {
|
||||
const totalStorage = await RNFS.getFSInfo()
|
||||
const storageInUse = await RNFS.stat(RNFS.DocumentDirectoryPath)
|
||||
|
||||
return {
|
||||
totalStorage: totalStorage.totalSpace,
|
||||
freeSpace: totalStorage.freeSpace,
|
||||
storageInUseByJellify: storageInUse.size,
|
||||
}
|
||||
}
|
||||
|
||||
const useDownload = useMutation({
|
||||
mutationFn: (trackItem: BaseItemDto) => {
|
||||
if (isUndefined(api)) throw new Error('API client not initialized')
|
||||
@@ -37,6 +58,12 @@ const NetworkContextInitializer = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const { data: storageUsage } = useQuery({
|
||||
queryKey: [QueryKeys.StorageInUse],
|
||||
queryFn: () => fetchStorageInUse(),
|
||||
staleTime: 1000 * 60 * 60 * 1, // 1 hour
|
||||
})
|
||||
|
||||
const useRemoveDownload = useMutation({
|
||||
mutationFn: (trackItem: BaseItemDto) => deleteAudio(trackItem),
|
||||
onSuccess: (data, { Id }) => {
|
||||
@@ -67,6 +94,7 @@ const NetworkContextInitializer = () => {
|
||||
activeDownloads,
|
||||
downloadedTracks,
|
||||
networkStatus,
|
||||
storageUsage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +138,7 @@ const NetworkContext = createContext<NetworkContext>({
|
||||
downloadedTracks: [],
|
||||
activeDownloads: [],
|
||||
networkStatus: networkStatusTypes.ONLINE,
|
||||
storageUsage: undefined,
|
||||
})
|
||||
|
||||
export const NetworkContextProvider: ({
|
||||
@@ -117,22 +146,9 @@ export const NetworkContextProvider: ({
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) => React.JSX.Element = ({ children }: { children: ReactNode }) => {
|
||||
const { useDownload, useRemoveDownload, downloadedTracks, activeDownloads, networkStatus } =
|
||||
NetworkContextInitializer()
|
||||
const context = NetworkContextInitializer()
|
||||
|
||||
return (
|
||||
<NetworkContext.Provider
|
||||
value={{
|
||||
useDownload,
|
||||
useRemoveDownload,
|
||||
activeDownloads,
|
||||
downloadedTracks,
|
||||
networkStatus,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NetworkContext.Provider>
|
||||
)
|
||||
return <NetworkContext.Provider value={context}>{children}</NetworkContext.Provider>
|
||||
}
|
||||
|
||||
export const useNetworkContext = () => useContext(NetworkContext)
|
||||
|
||||
5
src/providers/Network/types.d.ts
vendored
Normal file
5
src/providers/Network/types.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export type JellifyStorage = {
|
||||
totalStorage: number
|
||||
freeSpace: number
|
||||
storageInUseByJellify: number
|
||||
}
|
||||
@@ -69,6 +69,8 @@ const PlayerContextInitializer = () => {
|
||||
const handlePlaybackProgressUpdated = async (progress: Progress) => {
|
||||
if (playStateApi && nowPlaying)
|
||||
await handlePlaybackProgress(sessionId, playStateApi, nowPlaying, progress)
|
||||
else if (!playStateApi) console.warn('No play state API found')
|
||||
else console.warn('No now playing track found')
|
||||
}
|
||||
|
||||
//#endregion Functions
|
||||
@@ -147,6 +149,7 @@ const PlayerContextInitializer = () => {
|
||||
break
|
||||
}
|
||||
case Event.PlaybackProgressUpdated: {
|
||||
console.debug('Playback progress updated')
|
||||
usePlaybackProgressUpdated.mutate(event)
|
||||
|
||||
// Cache playing track at 20 seconds if it's not already downloaded
|
||||
|
||||
50
src/providers/Settings/index.tsx
Normal file
50
src/providers/Settings/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { storage } from '../../constants/storage'
|
||||
import { MMKVStorageKeys } from '../../enums/mmkv-storage-keys'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
interface SettingsContext {
|
||||
sendMetrics: boolean
|
||||
setSendMetrics: React.Dispatch<React.SetStateAction<boolean>>
|
||||
autoDownload: boolean
|
||||
setAutoDownload: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const SettingsContextInitializer = () => {
|
||||
const sendMetricsInit = storage.getBoolean(MMKVStorageKeys.SendMetrics)
|
||||
|
||||
const autoDownloadInit = storage.getBoolean(MMKVStorageKeys.AutoDownload)
|
||||
|
||||
const [sendMetrics, setSendMetrics] = useState(sendMetricsInit ?? false)
|
||||
|
||||
const [autoDownload, setAutoDownload] = useState(autoDownloadInit ?? false)
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.SendMetrics, sendMetrics)
|
||||
}, [sendMetrics])
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(MMKVStorageKeys.AutoDownload, autoDownload)
|
||||
}, [autoDownload])
|
||||
|
||||
return {
|
||||
sendMetrics,
|
||||
setSendMetrics,
|
||||
autoDownload,
|
||||
setAutoDownload,
|
||||
}
|
||||
}
|
||||
|
||||
export const SettingsContext = createContext<SettingsContext>({
|
||||
sendMetrics: false,
|
||||
setSendMetrics: () => {},
|
||||
autoDownload: false,
|
||||
setAutoDownload: () => {},
|
||||
})
|
||||
|
||||
export const SettingsProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const context = SettingsContextInitializer()
|
||||
|
||||
return <SettingsContext.Provider value={context}>{children}</SettingsContext.Provider>
|
||||
}
|
||||
|
||||
export const useSettingsContext = () => useContext(SettingsContext)
|
||||
@@ -21,8 +21,7 @@ export function Discover(): React.JSX.Element {
|
||||
name='Discover'
|
||||
component={Index}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
headerTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -26,8 +26,15 @@ export default function Home(): React.JSX.Element {
|
||||
<HomeProvider>
|
||||
<HomeStack.Navigator initialRouteName='Home' screenOptions={{ headerShown: true }}>
|
||||
<HomeStack.Group>
|
||||
<HomeStack.Screen name='Home' component={ProvidedHome} />
|
||||
|
||||
<HomeStack.Screen
|
||||
name='Home'
|
||||
component={ProvidedHome}
|
||||
options={{
|
||||
headerTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<HomeStack.Screen
|
||||
name='Artist'
|
||||
component={ArtistScreen}
|
||||
@@ -35,6 +42,7 @@ export default function Home(): React.JSX.Element {
|
||||
title: route.params.artist.Name ?? 'Unknown Artist',
|
||||
headerTitleStyle: {
|
||||
color: theme.background.val,
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Label } from '../../Global/helpers/text'
|
||||
import Input from '../../Global/helpers/input'
|
||||
import { Label } from '../../components/Global/helpers/text'
|
||||
import Input from '../../components/Global/helpers/input'
|
||||
import React, { useState } from 'react'
|
||||
import { View, XStack } from 'tamagui'
|
||||
import Button from '../../Global/helpers/button'
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { StackParamList } from '../../types'
|
||||
import { StackParamList } from '../../components/types'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { createPlaylist } from '../../../api/mutations/playlists'
|
||||
import { createPlaylist } from '../../api/mutations/playlists'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
// import * as Burnt from 'burnt'
|
||||
|
||||
export default function AddPlaylist({
|
||||
@@ -1,14 +1,14 @@
|
||||
import { View, XStack } from 'tamagui'
|
||||
import { DeletePlaylistProps } from '../../../components/types'
|
||||
import Button from '../../../components/Global/helpers/button'
|
||||
import { Text } from '../../../components/Global/helpers/text'
|
||||
import { DeletePlaylistProps } from '../../components/types'
|
||||
import Button from '../../components/Global/helpers/button'
|
||||
import { Text } from '../../components/Global/helpers/text'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { deletePlaylist } from '../../../api/mutations/playlists'
|
||||
import { deletePlaylist } from '../../api/mutations/playlists'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
import { queryClient } from '../../../constants/query-client'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
// import * as Burnt from 'burnt'
|
||||
|
||||
export default function DeletePlaylist({
|
||||
@@ -5,8 +5,8 @@ import Library from '../../components/Library/component'
|
||||
import { AlbumScreen } from '../../components/Album'
|
||||
import { PlaylistScreen } from '../Playlist'
|
||||
import DetailsScreen from '../Detail'
|
||||
import AddPlaylist from '../../components/Library/components/add-playlist'
|
||||
import DeletePlaylist from '../../components/Library/components/delete-playlist'
|
||||
import AddPlaylist from './add-playlist'
|
||||
import DeletePlaylist from './delete-playlist'
|
||||
import { ArtistScreen } from '../Artist'
|
||||
import InstantMix from '../../components/InstantMix/component'
|
||||
import { useTheme } from 'tamagui'
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { StackParamList } from '../../components/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import AccountDetails from '../../components/Settings/account-details'
|
||||
import AccountTab from '../../components/Settings/components/account-tab'
|
||||
|
||||
export default function AccountDetailsScreen({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
return <AccountDetails />
|
||||
return <AccountTab />
|
||||
}
|
||||
|
||||
@@ -1,65 +1,31 @@
|
||||
import React from 'react'
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack'
|
||||
import Root from '../../components/Settings/component'
|
||||
import AccountDetails from './account-details'
|
||||
import Labs from './labs'
|
||||
import DetailsScreen from '../Detail'
|
||||
import { StackParamList } from '../../components/types'
|
||||
import PlaybackDetails from './playback-details'
|
||||
import ServerDetails from './server-details'
|
||||
import Settings from '../../components/Settings/component'
|
||||
import SignOutModal from './sign-out-modal'
|
||||
import { SettingsStackParamList } from './types'
|
||||
|
||||
export const SettingsStack = createNativeStackNavigator<StackParamList>()
|
||||
export const SettingsStack = createNativeStackNavigator<SettingsStackParamList>()
|
||||
|
||||
export default function SettingsScreen(): React.JSX.Element {
|
||||
return (
|
||||
<SettingsStack.Navigator>
|
||||
<SettingsStack.Screen
|
||||
name='Settings'
|
||||
component={Root}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<SettingsStack.Navigator
|
||||
initialRouteName='Settings'
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<SettingsStack.Screen name='Settings' component={Settings} />
|
||||
|
||||
<SettingsStack.Screen
|
||||
name='Account'
|
||||
component={AccountDetails}
|
||||
name='SignOut'
|
||||
component={SignOutModal}
|
||||
options={{
|
||||
title: 'Account',
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
/* https://www.reddit.com/r/reactnative/comments/1dgktbn/comment/lxd23sj/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button */
|
||||
presentation: 'formSheet',
|
||||
sheetInitialDetentIndex: 0,
|
||||
sheetAllowedDetents: [0.2],
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsStack.Screen name='Server' component={ServerDetails} />
|
||||
|
||||
<SettingsStack.Screen name='Playback' component={PlaybackDetails} />
|
||||
|
||||
<SettingsStack.Screen
|
||||
name='Labs'
|
||||
component={Labs}
|
||||
options={{
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleStyle: {
|
||||
fontFamily: 'Aileron-Bold',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingsStack.Group screenOptions={{ presentation: 'modal' }}>
|
||||
<SettingsStack.Screen
|
||||
name='Details'
|
||||
component={DetailsScreen}
|
||||
options={{
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</SettingsStack.Group>
|
||||
</SettingsStack.Navigator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { StackParamList } from '../../components/types'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import DevTools from '../../components/Settings/dev-tools'
|
||||
|
||||
export default function Labs({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: NativeStackNavigationProp<StackParamList>
|
||||
}): React.JSX.Element {
|
||||
return <DevTools />
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { View } from 'tamagui'
|
||||
|
||||
export default function PlaybackDetails(): React.JSX.Element {
|
||||
return <View />
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react'
|
||||
import { YStack, XStack } from 'tamagui'
|
||||
import { H5, Text } from '../../components/Global/helpers/text'
|
||||
import Icon from '../../components/Global/helpers/icon'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
export default function ServerDetails(): React.JSX.Element {
|
||||
const { api, library } = useJellifyContext()
|
||||
return (
|
||||
<YStack>
|
||||
{api && (
|
||||
<YStack>
|
||||
<H5>Access Token</H5>
|
||||
<XStack>
|
||||
<Icon name='hand-coin-outline' />
|
||||
<Text>{api.accessToken}</Text>
|
||||
</XStack>
|
||||
<H5>Jellyfin Server</H5>
|
||||
<XStack>
|
||||
<Icon name='server-network' />
|
||||
<Text>{api.basePath}</Text>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)}
|
||||
{library && (
|
||||
<YStack>
|
||||
<H5>Library</H5>
|
||||
<XStack>
|
||||
<Icon name='book-outline' />
|
||||
<Text>{library.musicLibraryName!}</Text>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)}
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
58
src/screens/Settings/sign-out-modal.tsx
Normal file
58
src/screens/Settings/sign-out-modal.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import TrackPlayer from 'react-native-track-player'
|
||||
import { Button, Spacer, View, XStack, YStack } from 'tamagui'
|
||||
import { SignOutModalProps } from './types'
|
||||
import { H5, Text } from '../../components/Global/helpers/text'
|
||||
export default function SignOutModal({ navigation }: SignOutModalProps): React.JSX.Element {
|
||||
return (
|
||||
<YStack marginHorizontal={'$6'}>
|
||||
<H5>Sign out?</H5>
|
||||
|
||||
<Spacer />
|
||||
<XStack gap={'$2'}>
|
||||
<Button
|
||||
borderWidth={'$1'}
|
||||
borderColor={'$borderColor'}
|
||||
flex={1}
|
||||
onPress={() => {
|
||||
navigation.goBack()
|
||||
}}
|
||||
>
|
||||
<Text bold color={'$borderColor'}>
|
||||
Cancel
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
flex={1}
|
||||
color={'$danger'}
|
||||
borderColor={'$danger'}
|
||||
onPress={() => {
|
||||
TrackPlayer.reset()
|
||||
.then(() => {
|
||||
console.debug('TrackPlayer cleared')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error clearing TrackPlayer', error)
|
||||
})
|
||||
.finally(() => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: 'Login',
|
||||
params: {
|
||||
screen: 'ServerAddress',
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text bold color={'$danger'}>
|
||||
Sign out
|
||||
</Text>
|
||||
</Button>
|
||||
</XStack>
|
||||
</YStack>
|
||||
)
|
||||
}
|
||||
7
src/screens/Settings/types.d.ts
vendored
Normal file
7
src/screens/Settings/types.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export type SettingsStackParamList = {
|
||||
Settings: undefined
|
||||
SignOut: undefined
|
||||
}
|
||||
|
||||
export type SettingsProps = NativeStackScreenProps<SettingsStackParamList, 'Settings'>
|
||||
export type SignOutModalProps = NativeStackScreenProps<SettingsStackParamList, 'SignOut'>
|
||||
@@ -37,22 +37,34 @@ const jellifyConfig = createTamagui({
|
||||
backgroundHover: tokens.color.purpleGray,
|
||||
borderColor: tokens.color.amethyst,
|
||||
color: tokens.color.white,
|
||||
success: tokens.color.success,
|
||||
primary: tokens.color.telemagenta,
|
||||
danger: tokens.color.danger,
|
||||
},
|
||||
dark_inverted_purple: {
|
||||
color: tokens.color.purpleDark,
|
||||
borderColor: tokens.color.amethyst,
|
||||
background: tokens.color.amethyst,
|
||||
success: tokens.color.success,
|
||||
primary: tokens.color.telemagenta,
|
||||
danger: tokens.color.danger,
|
||||
},
|
||||
light: {
|
||||
background: tokens.color.white,
|
||||
backgroundActive: tokens.color.amethyst,
|
||||
borderColor: tokens.color.purpleGray,
|
||||
color: tokens.color.purpleDark,
|
||||
success: tokens.color.success,
|
||||
primary: tokens.color.telemagenta,
|
||||
danger: tokens.color.danger,
|
||||
},
|
||||
light_inverted_purple: {
|
||||
color: tokens.color.purpleDark,
|
||||
borderColor: tokens.color.purpleDark,
|
||||
background: tokens.color.purpleGray,
|
||||
success: tokens.color.success,
|
||||
primary: tokens.color.telemagenta,
|
||||
danger: tokens.color.danger,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
5
telemetrydeck.json
Normal file
5
telemetrydeck.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"app": "Jellify",
|
||||
"appID": "00000000-0000-0000-0000-000000000000",
|
||||
"clientUser": "anonymous"
|
||||
}
|
||||
Reference in New Issue
Block a user