Compare commits
117 Commits
v2.3.2
...
feat/emby-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
914ca6a44f | ||
|
|
a3b281ca72 | ||
|
|
7079a7c653 | ||
|
|
5bf68b6c42 | ||
|
|
0592d64253 | ||
|
|
e149aa796c | ||
|
|
0bf8b7a6e0 | ||
|
|
98c64e30eb | ||
|
|
b7436afabe | ||
|
|
f2a8c4c9b2 | ||
|
|
f1232ba177 | ||
|
|
e19bc64dd2 | ||
|
|
e8413dd099 | ||
|
|
ef391f49d1 | ||
|
|
5803cc199e | ||
|
|
096182e753 | ||
|
|
4fc1431479 | ||
|
|
01c0c0280f | ||
|
|
e8d674d369 | ||
|
|
cbd1882d36 | ||
|
|
a45f20a57d | ||
|
|
712b086fb3 | ||
|
|
7f70b93faa | ||
|
|
3a45895b22 | ||
|
|
d9f9226d62 | ||
|
|
4f04f68e76 | ||
|
|
84388bc28f | ||
|
|
3a90be2372 | ||
|
|
a0c32fd5a8 | ||
|
|
887aa3ab75 | ||
|
|
a64bcec4b4 | ||
|
|
75a333a17b | ||
|
|
0d09c6f0b8 | ||
|
|
189491b90a | ||
|
|
c9f7f71194 | ||
|
|
0b13e69854 | ||
|
|
83b890192e | ||
|
|
c5b1406e16 | ||
|
|
a64f52c4f9 | ||
|
|
87b08050e4 | ||
|
|
c34d7a8e71 | ||
|
|
7e165d3bcc | ||
|
|
0944128290 | ||
|
|
1b40f7257e | ||
|
|
e815dc89dc | ||
|
|
94593859e2 | ||
|
|
30f410ee5e | ||
|
|
0357cc61ef | ||
|
|
1e9dd2577b | ||
|
|
7b02b72e24 | ||
|
|
72f7c2cf5e | ||
|
|
0d2e502f2d | ||
|
|
14f56685df | ||
|
|
10a9e60574 | ||
|
|
65e630025b | ||
|
|
9410c26212 | ||
|
|
f505f68981 | ||
|
|
ce0bacbfd6 | ||
|
|
4820f80cfa | ||
|
|
038b1bf437 | ||
|
|
7646a796b2 | ||
|
|
ff55f807cf | ||
|
|
a3d266f01d | ||
|
|
cd82377373 | ||
|
|
415fbdc528 | ||
|
|
981a45fceb | ||
|
|
b51a8a4caf | ||
|
|
71deca818d | ||
|
|
d6d21c0206 | ||
|
|
aa575605c0 | ||
|
|
15ffcfb0aa | ||
|
|
62e5309498 | ||
|
|
d3eb839ea2 | ||
|
|
0e9c3e116a | ||
|
|
8beefb4180 | ||
|
|
9131a601e8 | ||
|
|
5c4d8ea214 | ||
|
|
0a8997588d | ||
|
|
621cf73e50 | ||
|
|
02ff4dda3a | ||
|
|
03355c138c | ||
|
|
2d17bd7872 | ||
|
|
c975455e94 | ||
|
|
746d67ed36 | ||
|
|
1249857196 | ||
|
|
c7d8ec3151 | ||
|
|
97b10a4faf | ||
|
|
7af98d0983 | ||
|
|
47c1782652 | ||
|
|
cb5ad032c5 | ||
|
|
752dec5bc2 | ||
|
|
0d5f4bd4e0 | ||
|
|
cca8f43ada | ||
|
|
a8e024dd00 | ||
|
|
902178c48b | ||
|
|
f73cec2097 | ||
|
|
f3388132c1 | ||
|
|
ec4a2b6831 | ||
|
|
7cdd01e713 | ||
|
|
38ce9986e5 | ||
|
|
9952b39044 | ||
|
|
e0177fb89b | ||
|
|
68c8808188 | ||
|
|
746c96d459 | ||
|
|
a97611c0ad | ||
|
|
e511f744ad | ||
|
|
a6a306b5be | ||
|
|
881ab95029 | ||
|
|
968e98d8df | ||
|
|
b01470bde8 | ||
|
|
823f7b59e8 | ||
|
|
16162d8e35 | ||
|
|
ea817025e1 | ||
|
|
00675bbbd3 | ||
|
|
24b5a47a7c | ||
|
|
bb655cb719 | ||
|
|
e472d043cf |
@@ -58,7 +58,8 @@ module.exports = {
|
||||
{
|
||||
ignoreProps: true
|
||||
}
|
||||
]
|
||||
],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
||||
## [2.3.3](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.3.2...v2.3.3) (2024-06-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* actually send out /Playing events as session updates. ([b01470b](https://github.com/leinelissen/jellyfin-audio-player/commit/b01470bde8ea353ea7139c0708ec9cfdaf600fe4)), closes [#218](https://github.com/leinelissen/jellyfin-audio-player/issues/218)
|
||||
* do extra checks for album ids in ([00675bb](https://github.com/leinelissen/jellyfin-audio-player/commit/00675bbbd3e72e8e710d8aa9b73b491e65153d40))
|
||||
* double-check albums have dates ([881ab95](https://github.com/leinelissen/jellyfin-audio-player/commit/881ab9502960786dc9685cf3612793fea3c1be4c))
|
||||
* hermes version in cocoapods ([ea81702](https://github.com/leinelissen/jellyfin-audio-player/commit/ea817025e1bf67fcd3c183c12f4f1f93c3218785))
|
||||
* react-native-screens android setup ([968e98d](https://github.com/leinelissen/jellyfin-audio-player/commit/968e98d8dffa79ea3165d1209542bd91dd914ef5))
|
||||
* refactor JellyfinApi to be less burdensome to implement ([a6a306b](https://github.com/leinelissen/jellyfin-audio-player/commit/a6a306b5be6988469449b17ed527f1d365901e6d))
|
||||
* throw errors when requests do not yield 200 OKs ([16162d8](https://github.com/leinelissen/jellyfin-audio-player/commit/16162d8e3505ea195c8aaf03b82df88405196025))
|
||||
|
||||
|
||||
|
||||
## [2.3.2](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.3.1...v2.3.2) (2024-03-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build with xcode 15.3 ([845eac7](https://github.com/leinelissen/jellyfin-audio-player/commit/845eac70a0afa189cd76e97f739ad627f648566a))
|
||||
* remove conflicting app transport properties ([c966276](https://github.com/leinelissen/jellyfin-audio-player/commit/c9662769faec8771b6a70da815ec36e62c8c43a2))
|
||||
|
||||
|
||||
|
||||
## [2.3.1](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.3.0...v2.3.1) (2024-03-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* revert to supporting HTTP-based backends ([f310bb8](https://github.com/leinelissen/jellyfin-audio-player/commit/f310bb82f61f532f9557787d364e9f342166806d)), closes [#205](https://github.com/leinelissen/jellyfin-audio-player/issues/205)
|
||||
|
||||
|
||||
|
||||
# [2.3.0](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.2.0...v2.3.0) (2024-02-11)
|
||||
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ android {
|
||||
applicationId "nl.moeilijkedingen.jellyfinaudioplayer"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 27
|
||||
versionName "2.3.2"
|
||||
versionCode 29
|
||||
versionName "2.3.3"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -126,7 +126,6 @@ android {
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
implementation("com.facebook.react:flipper-integration")
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
/**
|
||||
@@ -19,4 +21,8 @@ class MainActivity : ReactActivity() {
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.flipper.ReactNativeFlipper
|
||||
import com.facebook.soloader.SoLoader
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
@@ -31,7 +30,7 @@ class MainApplication : Application(), ReactApplication {
|
||||
}
|
||||
|
||||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(this.applicationContext, reactNativeHost)
|
||||
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -40,6 +39,5 @@ class MainApplication : Application(), ReactApplication {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "34.0.0"
|
||||
minSdkVersion = 21
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
ndkVersion = "25.1.8937393"
|
||||
kotlinVersion = "1.8.0"
|
||||
ndkVersion = "26.1.10909125"
|
||||
kotlinVersion = "1.9.22"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
||||
@@ -38,6 +38,4 @@ newArchEnabled=false
|
||||
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
FLIPPER_VERSION=0.201.0
|
||||
hermesEnabled=true
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
14
android/gradlew
vendored
14
android/gradlew
vendored
@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -202,11 +202,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
||||
20
android/gradlew.bat
vendored
20
android/gradlew.bat
vendored
@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Privacy policy for Fintunes
|
||||
|
||||
Fintunes does not collect any personal data. Period. We respect your right to
|
||||
Fintunes does not collect any personal data. Period. We respect your right to
|
||||
autonomy and vow to not collect any information without user consent at all.
|
||||
|
||||
If you opt-in to crash logging, we will collect analytics data from your device,
|
||||
every time a crash occurs. This data includes debugging information such as
|
||||
devices, versions and the specific error. All data is sent to a server
|
||||
controlled by the first party. No third parties can access this data in any
|
||||
form. No personal data is included in the analytics data.
|
||||
If you opt-in to crash logging, we will collect analytics data from your device,
|
||||
every time a crash occurs. This data includes debugging information such as
|
||||
devices, versions and the specific error. All data is sent to a server
|
||||
controlled by the first party. No third parties can access this data in any
|
||||
form. No personal data is included in the analytics data.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package_name("nl.moeilijkedingen.jellyfinaudioplayer")
|
||||
app_identifier("nl.moeilijkedingen.jellyfinaudioplayer")
|
||||
apple_id("lei@moeilijkedingen.nl")
|
||||
team_id("238P3C58WC")
|
||||
json_key_file("./fastlane/play-store-credentials.json")
|
||||
apple_id("lei@codified.nl")
|
||||
team_id("HD2D35G9Y4")
|
||||
json_key_file("./fastlane/play-store-credentials.json")
|
||||
itc_team_id("127114471")
|
||||
1
fastlane/metadata/android/pl-PL/full_description.txt
Normal file
1
fastlane/metadata/android/pl-PL/full_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Fintunes to strumieniowy odtwarzacz audio dla systemu multimedialnego Jellyfin. Posiada wspaniały interfejs, który pozwala z łatwością odtwarzać ulubioną muzykę. Możesz przeszukać całą bibliotekę w poszukiwaniu dowolnego utworu lub po prostu skorzystać z listy odtwarzania utworzonej wcześniej w Jellyfin. Wszystkie utwory są przesyłane strumieniowo bezpośrednio z biblioteki Jellyfin w najwyższej jakości. Streaming nie zawsze jest możliwy? Każdy utwór z biblioteki Jellyfin można pobrać i odtwarzać offline.
|
||||
1
fastlane/metadata/android/pl-PL/short_description.txt
Normal file
1
fastlane/metadata/android/pl-PL/short_description.txt
Normal file
@@ -0,0 +1 @@
|
||||
Strumieniowy odtwarzacz audio dla Jellyfin
|
||||
1
fastlane/metadata/android/pl-PL/title.txt
Normal file
1
fastlane/metadata/android/pl-PL/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Fintunes
|
||||
@@ -1 +1 @@
|
||||
jellyfin, audio, odtwarzacz, przesyłanie strumieniowe, pobieranie, muzyka
|
||||
jellyfin, audio, odtwarzacz, streaming, pobrane, muzyka
|
||||
|
||||
@@ -7,16 +7,17 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
01DDB50991998A6D20A1A5CD /* libPods-Fintunes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */; };
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
38B3606A2D29107567360ACF /* libPods-Fintunes-FintunesTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EBC468D2DE6EB8FF02B72B7 /* libPods-Fintunes-FintunesTests.a */; };
|
||||
4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */; };
|
||||
4FA1B23D2550A94C007A035E /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA1B23C2550A94C007A035E /* File.swift */; };
|
||||
96A76B2DA812E1F2E353959C /* libPods-Fintunes-FintunesTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 406012091F4F831F72DFB5D2 /* libPods-Fintunes-FintunesTests.a */; };
|
||||
AB393FCA2857CC8400773469 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB393FC92857CC8400773469 /* SnapshotHelper.swift */; };
|
||||
AB4A8DFE2857C8DA005A1ED0 /* FintunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.swift */; };
|
||||
D7439709FB704B4FE23C538F /* libPods-Fintunes.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 12335560B9820FD5AD98AB8F /* libPods-Fintunes.a */; };
|
||||
FA01635F2599C28FC19F2EC3 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -38,8 +39,6 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
00E356EE1AD99517003FC87E /* FintunesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FintunesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0973197F4BDB99413C326AD0 /* Pods-Fintunes.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes.release.xcconfig"; path = "Target Support Files/Pods-Fintunes/Pods-Fintunes.release.xcconfig"; sourceTree = "<group>"; };
|
||||
12335560B9820FD5AD98AB8F /* libPods-Fintunes.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07F961A680F5B00A75B9A /* Fintunes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Fintunes.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Fintunes/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Fintunes/AppDelegate.m; sourceTree = "<group>"; };
|
||||
@@ -47,18 +46,19 @@
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Fintunes/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Fintunes/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Fintunes/main.m; sourceTree = "<group>"; };
|
||||
3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Fintunes/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
406012091F4F831F72DFB5D2 /* libPods-Fintunes-FintunesTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-FintunesTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
4B4A0465FF364579B28CF5D7 /* Inter-VariableFont_slnt,wght.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Inter-VariableFont_slnt,wght.ttf"; path = "../src/assets/fonts/Inter-VariableFont_slnt,wght.ttf"; sourceTree = "<group>"; };
|
||||
4FA1B23B2550A94B007A035E /* Fintunes-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Fintunes-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
4FA1B23C2550A94C007A035E /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
|
||||
5370B45C5DDCD952C6569B8D /* Pods-Fintunes.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes.debug.xcconfig"; path = "Target Support Files/Pods-Fintunes/Pods-Fintunes.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
7D43C7610851B9666193E3F6 /* libPods-Fintunes-tvOSTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-tvOSTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8EBC468D2DE6EB8FF02B72B7 /* libPods-Fintunes-FintunesTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-FintunesTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
55063C1C8FC150384B504BD6 /* Pods-Fintunes.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes.debug.xcconfig"; path = "Target Support Files/Pods-Fintunes/Pods-Fintunes.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5892110C5BD456492E65B0FC /* Pods-Fintunes.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes.release.xcconfig"; path = "Target Support Files/Pods-Fintunes/Pods-Fintunes.release.xcconfig"; sourceTree = "<group>"; };
|
||||
AB393FC92857CC8400773469 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = "<group>"; };
|
||||
AB4A8DFB2857C8DA005A1ED0 /* FintunesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FintunesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FintunesUITests.swift; sourceTree = "<group>"; };
|
||||
AFAE700A256C6B0ED0D20FE3 /* Pods-Fintunes-FintunesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes-FintunesTests.release.xcconfig"; path = "Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B20CBCFF11E124551F286B84 /* Pods-Fintunes-FintunesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes-FintunesTests.debug.xcconfig"; path = "Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
E35451F7979C52C1692C4C9F /* libPods-Fintunes-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BB181C2EAAC2E99F00A27B5F /* Pods-Fintunes-FintunesTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes-FintunesTests.release.xcconfig"; path = "Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E9A22426CA08309D7A874468 /* Pods-Fintunes-FintunesTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fintunes-FintunesTests.debug.xcconfig"; path = "Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -68,7 +68,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
38B3606A2D29107567360ACF /* libPods-Fintunes-FintunesTests.a in Frameworks */,
|
||||
96A76B2DA812E1F2E353959C /* libPods-Fintunes-FintunesTests.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -76,7 +76,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D7439709FB704B4FE23C538F /* libPods-Fintunes.a in Frameworks */,
|
||||
01DDB50991998A6D20A1A5CD /* libPods-Fintunes.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -101,6 +101,7 @@
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
4FA1B23C2550A94C007A035E /* File.swift */,
|
||||
4FA1B23B2550A94B007A035E /* Fintunes-Bridging-Header.h */,
|
||||
3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = Fintunes;
|
||||
sourceTree = "<group>";
|
||||
@@ -110,10 +111,8 @@
|
||||
children = (
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
|
||||
E35451F7979C52C1692C4C9F /* libPods-Fintunes-tvOS.a */,
|
||||
7D43C7610851B9666193E3F6 /* libPods-Fintunes-tvOSTests.a */,
|
||||
12335560B9820FD5AD98AB8F /* libPods-Fintunes.a */,
|
||||
8EBC468D2DE6EB8FF02B72B7 /* libPods-Fintunes-FintunesTests.a */,
|
||||
E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */,
|
||||
406012091F4F831F72DFB5D2 /* libPods-Fintunes-FintunesTests.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -121,10 +120,10 @@
|
||||
46001D7383D71A837AAF6E07 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5370B45C5DDCD952C6569B8D /* Pods-Fintunes.debug.xcconfig */,
|
||||
0973197F4BDB99413C326AD0 /* Pods-Fintunes.release.xcconfig */,
|
||||
B20CBCFF11E124551F286B84 /* Pods-Fintunes-FintunesTests.debug.xcconfig */,
|
||||
AFAE700A256C6B0ED0D20FE3 /* Pods-Fintunes-FintunesTests.release.xcconfig */,
|
||||
55063C1C8FC150384B504BD6 /* Pods-Fintunes.debug.xcconfig */,
|
||||
5892110C5BD456492E65B0FC /* Pods-Fintunes.release.xcconfig */,
|
||||
E9A22426CA08309D7A874468 /* Pods-Fintunes-FintunesTests.debug.xcconfig */,
|
||||
BB181C2EAAC2E99F00A27B5F /* Pods-Fintunes-FintunesTests.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -186,12 +185,12 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "FintunesTests" */;
|
||||
buildPhases = (
|
||||
BBD71961640F29097BE9932A /* [CP] Check Pods Manifest.lock */,
|
||||
4C89666008ED1AECA3700F1B /* [CP] Check Pods Manifest.lock */,
|
||||
00E356EA1AD99517003FC87E /* Sources */,
|
||||
00E356EB1AD99517003FC87E /* Frameworks */,
|
||||
00E356EC1AD99517003FC87E /* Resources */,
|
||||
BDE784ECF29EF861DBFF49D7 /* [CP] Copy Pods Resources */,
|
||||
A02366876E56A727F566EC3A /* [CP] Embed Pods Frameworks */,
|
||||
105D0C84EA50AA33C7A575A2 /* [CP] Embed Pods Frameworks */,
|
||||
D9A3A76E8CEBB2167393A14A /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -207,13 +206,13 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Fintunes" */;
|
||||
buildPhases = (
|
||||
E68FAF43791AC236CF4BF8CB /* [CP] Check Pods Manifest.lock */,
|
||||
3A2533CC05843338D35BF11A /* [CP] Check Pods Manifest.lock */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
B9FB8FC65CEFF9AFAC71127E /* [CP] Copy Pods Resources */,
|
||||
2917566AA57EE087FC9FCCE9 /* [CP] Embed Pods Frameworks */,
|
||||
062FA28CBD13CA4E60734D70 /* [CP] Embed Pods Frameworks */,
|
||||
EDBDE27F51B5399CA455AD4D /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -253,19 +252,19 @@
|
||||
TargetAttributes = {
|
||||
00E356ED1AD99517003FC87E = {
|
||||
CreatedOnToolsVersion = 6.2;
|
||||
DevelopmentTeam = 238P3C58WC;
|
||||
DevelopmentTeam = HD2D35G9Y4;
|
||||
ProvisioningStyle = Manual;
|
||||
TestTargetID = 13B07F861A680F5B00A75B9A;
|
||||
};
|
||||
13B07F861A680F5B00A75B9A = {
|
||||
DevelopmentTeam = 238P3C58WC;
|
||||
DevelopmentTeam = HD2D35G9Y4;
|
||||
LastSwiftMigration = 1210;
|
||||
ProvisioningStyle = Manual;
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
AB4A8DFA2857C8DA005A1ED0 = {
|
||||
CreatedOnToolsVersion = 13.4.1;
|
||||
DevelopmentTeam = 238P3C58WC;
|
||||
ProvisioningStyle = Manual;
|
||||
DevelopmentTeam = HD2D35G9Y4;
|
||||
ProvisioningStyle = Automatic;
|
||||
TestTargetID = 13B07F861A680F5B00A75B9A;
|
||||
};
|
||||
};
|
||||
@@ -305,6 +304,7 @@
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
|
||||
4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */,
|
||||
FA01635F2599C28FC19F2EC3 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -332,9 +332,9 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\n\nWITH_ENVIRONMENT=\"../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n";
|
||||
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";
|
||||
};
|
||||
2917566AA57EE087FC9FCCE9 /* [CP] Embed Pods Frameworks */ = {
|
||||
062FA28CBD13CA4E60734D70 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -352,7 +352,7 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A02366876E56A727F566EC3A /* [CP] Embed Pods Frameworks */ = {
|
||||
105D0C84EA50AA33C7A575A2 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -370,27 +370,29 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
B9FB8FC65CEFF9AFAC71127E /* [CP] Copy Pods Resources */ = {
|
||||
3A2533CC05843338D35BF11A /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
|
||||
"${PODS_ROOT}/Sentry/Sources/Resources/PrivacyInfo.xcprivacy",
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PrivacyInfo.xcprivacy",
|
||||
"$(DERIVED_FILE_DIR)/Pods-Fintunes-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-resources.sh\"\n";
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
BBD71961640F29097BE9932A /* [CP] Check Pods Manifest.lock */ = {
|
||||
4C89666008ED1AECA3700F1B /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -412,7 +414,7 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
BDE784ECF29EF861DBFF49D7 /* [CP] Copy Pods Resources */ = {
|
||||
D9A3A76E8CEBB2167393A14A /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -420,38 +422,36 @@
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
|
||||
"${PODS_ROOT}/Sentry/Sources/Resources/PrivacyInfo.xcprivacy",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PrivacyInfo.xcprivacy",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E68FAF43791AC236CF4BF8CB /* [CP] Check Pods Manifest.lock */ = {
|
||||
EDBDE27F51B5399CA455AD4D /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-resources.sh",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Fintunes-checkManifestLockResult.txt",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
@@ -513,12 +513,12 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
00E356F61AD99517003FC87E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = B20CBCFF11E124551F286B84 /* Pods-Fintunes-FintunesTests.debug.xcconfig */;
|
||||
baseConfigurationReference = E9A22426CA08309D7A874468 /* Pods-Fintunes-FintunesTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
@@ -543,13 +543,13 @@
|
||||
};
|
||||
00E356F71AD99517003FC87E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AFAE700A256C6B0ED0D20FE3 /* Pods-Fintunes-FintunesTests.release.xcconfig */;
|
||||
baseConfigurationReference = BB181C2EAAC2E99F00A27B5F /* Pods-Fintunes-FintunesTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
INFOPLIST_FILE = FintunesTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
@@ -572,15 +572,14 @@
|
||||
};
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5370B45C5DDCD952C6569B8D /* Pods-Fintunes.debug.xcconfig */;
|
||||
baseConfigurationReference = 55063C1C8FC150384B504BD6 /* Pods-Fintunes.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 84;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
@@ -601,7 +600,6 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer;
|
||||
PRODUCT_NAME = Fintunes;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1707846041";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -611,16 +609,16 @@
|
||||
};
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 0973197F4BDB99413C326AD0 /* Pods-Fintunes.release.xcconfig */;
|
||||
baseConfigurationReference = 5892110C5BD456492E65B0FC /* Pods-Fintunes.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 84;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = HD2D35G9Y4;
|
||||
INFOPLIST_FILE = Fintunes/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
@@ -637,7 +635,7 @@
|
||||
PRODUCT_NAME = Fintunes;
|
||||
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1707846041";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -648,6 +646,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CC = "";
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -675,6 +674,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CXX = "";
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
|
||||
@@ -695,6 +695,8 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
@@ -717,6 +719,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CC = "";
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "c++20";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
@@ -744,6 +747,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)";
|
||||
COPY_PHASE_STRIP = YES;
|
||||
CXX = "";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
|
||||
@@ -760,6 +764,8 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
LD = "";
|
||||
LDPLUSPLUS = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
@@ -788,10 +794,11 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 84;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
@@ -801,6 +808,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -820,11 +828,12 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 84;
|
||||
CURRENT_PROJECT_VERSION = 94;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = 238P3C58WC;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
@@ -833,6 +842,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
|
||||
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
|
||||
{
|
||||
return [self getBundleURL];
|
||||
return [self bundleURL];
|
||||
}
|
||||
|
||||
- (NSURL *)getBundleURL
|
||||
- (NSURL *)bundleURL
|
||||
{
|
||||
#if DEBUG
|
||||
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.3.2</string>
|
||||
<string>2.4.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>84</string>
|
||||
<string>94</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
@@ -48,7 +48,7 @@
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
|
||||
37
ios/Fintunes/PrivacyInfo.xcprivacy
Normal file
37
ios/Fintunes/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
19
ios/Podfile
19
ios/Podfile
@@ -8,17 +8,6 @@ require Pod::Executable.execute_command('node', ['-p',
|
||||
platform :ios, min_ios_version_supported
|
||||
prepare_react_native_project!
|
||||
|
||||
# If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set.
|
||||
# because `react-native-flipper` depends on (FlipperKit,...) that will be excluded
|
||||
#
|
||||
# To fix this you can also exclude `react-native-flipper` using a `react-native.config.js`
|
||||
# ```js
|
||||
# module.exports = {
|
||||
# dependencies: {
|
||||
# ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}),
|
||||
# ```
|
||||
flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled
|
||||
|
||||
linkage = ENV['USE_FRAMEWORKS']
|
||||
if linkage != nil
|
||||
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
|
||||
@@ -30,11 +19,6 @@ target 'Fintunes' do
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# Enables Flipper.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable the next line.
|
||||
# :flipper_configuration => flipper_config,
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/.."
|
||||
)
|
||||
@@ -49,7 +33,8 @@ target 'Fintunes' do
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false
|
||||
:mac_catalyst_enabled => false,
|
||||
# :ccache_enabled => true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
1054
ios/Podfile.lock
1054
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
1676
package-lock.json
generated
1676
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fintunes",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.0",
|
||||
"main": "src/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -30,7 +30,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-airplay": "^1.2.0",
|
||||
"react-native": "^0.73.4",
|
||||
"react-native": "0.74.3",
|
||||
"react-native-accessibility-settings": "^0.1.2",
|
||||
"react-native-collapsible": "^1.6.1",
|
||||
"react-native-dotenv": "^3.4.9",
|
||||
@@ -39,8 +39,8 @@
|
||||
"react-native-gesture-handler": "^2.15.0",
|
||||
"react-native-localize": "^3.0.6",
|
||||
"react-native-modal-datetime-picker": "^17.1.0",
|
||||
"react-native-reanimated": "^3.7.0",
|
||||
"react-native-safe-area-context": "^4.9.0",
|
||||
"react-native-reanimated": "^3.14.0",
|
||||
"react-native-safe-area-context": "^4.10.8",
|
||||
"react-native-screens": "^3.29.0",
|
||||
"react-native-shadow-2": "^7.0.8",
|
||||
"react-native-svg": "^14.1.0",
|
||||
@@ -48,24 +48,21 @@
|
||||
"react-native-webview": "^13.7.1",
|
||||
"react-redux": "^9.1.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-flipper": "^2.0.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"styled-components": "^6.1.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@babel/runtime": "^7.20.1",
|
||||
"@react-native/babel-preset": "^0.73.21",
|
||||
"@react-native/metro-config": "^0.73.5",
|
||||
"@react-native/typescript-config": "^0.74.0",
|
||||
"@sentry/cli": "^2.28.0",
|
||||
"@sentry/react-native": "^5.18.0",
|
||||
"@react-native/babel-preset": "0.74.85",
|
||||
"@react-native/metro-config": "0.74.85",
|
||||
"@react-native/typescript-config": "0.74.85",
|
||||
"@sentry/cli": "^2.33.0",
|
||||
"@sentry/react-native": "^5.26.0",
|
||||
"@types/i18n-js": "^3.8.9",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
@@ -74,7 +71,6 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"metro-react-native-babel-transformer": "^0.77.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"react-native-flipper": "^0.212.0",
|
||||
"react-native-svg-transformer": "^1.3.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3"
|
||||
|
||||
@@ -4,12 +4,4 @@ module.exports = {
|
||||
android: {}
|
||||
},
|
||||
assets: ['./src/assets/fonts/'],
|
||||
dependencies: {
|
||||
// Deal with unruly react-native-flipper dependencies, per: https://github.com/facebook/flipper/issues/5266
|
||||
'react-native-flipper': {
|
||||
platforms: {
|
||||
ios: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
3
src/assets/icons/lyrics.svg
Normal file
3
src/assets/icons/lyrics.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="17" height="15" viewBox="0 0 17 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.314163 14.9808V3.01292C0.314163 2.68584 0.428955 2.40744 0.658538 2.17771C0.88826 1.94813 1.16659 1.83334 1.49354 1.83334H9.46791C9.795 1.83334 10.0733 1.94813 10.3029 2.17771C10.5326 2.40744 10.6475 2.68584 10.6475 3.01292V3.41021C10.4989 3.51605 10.3717 3.63598 10.266 3.77001C10.1602 3.90417 10.0651 4.05084 9.98083 4.21001V3.01292C9.98083 2.86334 9.93271 2.74042 9.83646 2.64417C9.74034 2.54806 9.6175 2.50001 9.46791 2.50001H1.49354C1.34395 2.50001 1.22111 2.54806 1.125 2.64417C1.02889 2.74042 0.98083 2.86334 0.98083 3.01292V12.5H9.46791C9.6175 12.5 9.74034 12.4519 9.83646 12.3558C9.93271 12.2596 9.98083 12.1367 9.98083 11.9871V8.79001C10.0651 8.94917 10.1633 9.09653 10.2752 9.23209C10.387 9.36764 10.5111 9.48688 10.6475 9.5898V11.9871C10.6475 12.3142 10.5326 12.5926 10.3029 12.8223C10.0733 13.0519 9.795 13.1667 9.46791 13.1667H2.12812L0.314163 14.9808ZM2.8975 10.5833H5.06416V9.91667H2.8975V10.5833ZM12.6796 8.58334C12.1026 8.58334 11.6112 8.38035 11.2052 7.97438C10.7992 7.56841 10.5962 7.07695 10.5962 6.50001C10.5962 5.92306 10.7992 5.4316 11.2052 5.02563C11.6112 4.61966 12.1026 4.41667 12.6796 4.41667C12.936 4.41667 13.1611 4.45806 13.355 4.54084C13.5489 4.62362 13.796 4.76917 14.0962 4.9775V0.416672H16.2629V1.08334H14.7629V6.50001C14.7629 7.07695 14.5599 7.56841 14.1537 7.97438C13.7478 8.38035 13.2564 8.58334 12.6796 8.58334ZM2.8975 7.83334H8.06416V7.16667H2.8975V7.83334ZM2.8975 5.08334H8.06416V4.41667H2.8975V5.08334Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
8
src/assets/icons/waveform.svg
Normal file
8
src/assets/icons/waveform.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M50.5303 36.5379C51.7368 36.5379 52.7016 35.573 52.7016 34.3935V21.6065C52.7016 20.427 51.7368 19.4352 50.5303 19.4352C49.2972 19.4352 48.3859 20.427 48.3859 21.6065V34.3935C48.3859 35.573 49.2972 36.5379 50.5303 36.5379Z" />
|
||||
<path d="M41.5233 50.0488C42.7295 50.0488 43.6677 49.0839 43.6677 47.9043V8.09575C43.6677 6.91623 42.7295 5.92436 41.5233 5.92436C40.2633 5.92436 39.352 6.91623 39.352 8.09575V47.9043C39.352 49.0839 40.2633 50.0488 41.5233 50.0488Z" />
|
||||
<path d="M32.4894 41.9261C33.7224 41.9261 34.6607 40.9879 34.6607 39.7817V16.2183C34.6607 15.012 33.7224 14.0469 32.4894 14.0469C31.256 14.0469 30.3447 15.012 30.3447 16.2183V39.7817C30.3447 40.9879 31.256 41.9261 32.4894 41.9261Z" />
|
||||
<path d="M23.4553 56C24.6884 56 25.6535 55.0348 25.6535 53.8287V2.17137C25.6535 0.965053 24.6884 0 23.4553 0C22.249 0 21.3376 0.965053 21.3376 2.17137V53.8287C21.3376 55.0348 22.249 56 23.4553 56Z" />
|
||||
<path d="M14.4481 45.1966C15.6812 45.1966 16.6195 44.2317 16.6195 43.0253V12.9746C16.6195 11.7683 15.6812 10.7764 14.4481 10.7764C13.2418 10.7764 12.3035 11.7683 12.3035 12.9746V43.0253C12.3035 44.2317 13.2418 45.1966 14.4481 45.1966Z" />
|
||||
<path d="M5.41411 34.2326C6.67405 34.2326 7.61231 33.2675 7.61231 32.0613V23.9387C7.61231 22.7324 6.67405 21.7405 5.41411 21.7405C4.2078 21.7405 3.29636 22.7324 3.29636 23.9387V32.0613C3.29636 33.2675 4.2078 34.2326 5.41411 34.2326Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -3,16 +3,14 @@ import { Provider } from 'react-redux';
|
||||
import TrackPlayer, { Capability } from 'react-native-track-player';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import Routes from '../screens';
|
||||
import store, { persistedStore, useTypedSelector } from '@/store';
|
||||
import store, { persistedStore } from '@/store';
|
||||
import {
|
||||
NavigationContainer,
|
||||
DefaultTheme,
|
||||
DarkTheme as BaseDarkTheme,
|
||||
} from '@react-navigation/native';
|
||||
import { ColorSchemeProvider, themes } from './Colors';
|
||||
import { ColorSchemeProvider, themes, useUserOrSystemScheme } from './Colors';
|
||||
import DownloadManager from './DownloadManager';
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { ColorScheme } from '@/store/settings/types';
|
||||
|
||||
const LightTheme = {
|
||||
...DefaultTheme,
|
||||
@@ -35,9 +33,7 @@ const DarkTheme = {
|
||||
* right theme is selected based on OS color scheme settings along with user preferences.
|
||||
*/
|
||||
function ThemedNavigationContainer({ children }: PropsWithChildren<{}>) {
|
||||
const systemScheme = useColorScheme();
|
||||
const userScheme = useTypedSelector((state) => state.settings.colorScheme);
|
||||
const scheme = userScheme === ColorScheme.System ? systemScheme : userScheme;
|
||||
const scheme = useUserOrSystemScheme();
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
|
||||
@@ -62,6 +62,9 @@ function generateStyles(scheme: ColorSchemeName, highContrast: boolean) {
|
||||
backgroundColor: scheme === 'dark' ? '#191919' : '#f3f3f3',
|
||||
color: scheme === 'dark' ? '#fff' : '#000',
|
||||
},
|
||||
trackBackground: {
|
||||
backgroundColor: scheme === 'dark' ? '#111' : '#fff',
|
||||
},
|
||||
stackHeader: {
|
||||
color: scheme === 'dark' ? 'white' : 'black'
|
||||
},
|
||||
@@ -108,14 +111,21 @@ export const themes: Record<'dark' | 'light' | 'dark-highcontrast' | 'light-high
|
||||
// Create context for supplying the theming information
|
||||
export const ColorSchemeContext = React.createContext(themes.dark);
|
||||
|
||||
/**
|
||||
* This hook returns the proper color scheme, taking into account potential user overrides.
|
||||
*/
|
||||
export function useUserOrSystemScheme() {
|
||||
const systemScheme = useColorScheme();
|
||||
const userScheme = useTypedSelector((state) => state.settings.colorScheme);
|
||||
return userScheme === ColorScheme.System ? systemScheme : userScheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* This provider contains the logic for settings the right theme on the ColorSchemeContext.
|
||||
*/
|
||||
export function ColorSchemeProvider({ children }: PropsWithChildren<{}>) {
|
||||
const systemScheme = useColorScheme();
|
||||
const highContrast = useAccessibilitySetting('darkerSystemColors');
|
||||
const userScheme = useTypedSelector((state) => state.settings.colorScheme);
|
||||
const scheme = userScheme === ColorScheme.System ? systemScheme : userScheme;
|
||||
const scheme = useUserOrSystemScheme();
|
||||
const theme = highContrast
|
||||
? themes[`${scheme || 'light'}-highcontrast`]
|
||||
: themes[scheme || 'light'];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Dimensions, useColorScheme, ViewProps } from 'react-native';
|
||||
import { Dimensions, ViewProps } from 'react-native';
|
||||
import { Canvas, Blur, Image as SkiaImage, useImage, Offset, Mask, RoundedRect, Shadow } from '@shopify/react-native-skia';
|
||||
import useDefaultStyles from './Colors';
|
||||
import useDefaultStyles, { useUserOrSystemScheme } from './Colors';
|
||||
import styled from 'styled-components/native';
|
||||
|
||||
const Screen = Dimensions.get('screen');
|
||||
@@ -45,10 +45,13 @@ function CoverImage({
|
||||
src,
|
||||
}: Props) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const colorScheme = useColorScheme();
|
||||
const colorScheme = useUserOrSystemScheme();
|
||||
|
||||
const image = useImage(src || null);
|
||||
const fallback = useImage(colorScheme === 'light' ? require('@/assets/images/empty-album-light.png') : require('@/assets/images/empty-album-dark.png'));
|
||||
const fallback = useImage(colorScheme === 'light'
|
||||
? require('@/assets/images/empty-album-light.png')
|
||||
: require('@/assets/images/empty-album-dark.png')
|
||||
);
|
||||
const { canvasSize, imageSize } = useMemo(() => {
|
||||
const imageSize = Screen.width - margin;
|
||||
const canvasSize = imageSize + blurRadius * 2;
|
||||
|
||||
@@ -20,8 +20,8 @@ export function calculateProgressTranslation(
|
||||
const completion = position / reference;
|
||||
|
||||
// GUARD: Check whether the calculated number is valid and not infinite
|
||||
if (Number.isNaN(completion) || !Number.isFinite(completion)) {
|
||||
return 0;
|
||||
if (Number.isNaN(completion) || !Number.isFinite(completion) || !width) {
|
||||
return -1_000;
|
||||
}
|
||||
|
||||
const output = (1 - completion) * -1 * width;
|
||||
@@ -29,6 +29,7 @@ export function calculateProgressTranslation(
|
||||
return output;
|
||||
}
|
||||
|
||||
// Progress track did not show up on Lyrics screen if min height is not set
|
||||
export const ProgressTrackContainer = styled.View`
|
||||
overflow: hidden;
|
||||
height: 5px;
|
||||
@@ -37,6 +38,7 @@ export const ProgressTrackContainer = styled.View`
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
min-height: 5px;
|
||||
`;
|
||||
|
||||
export interface ProgressTrackProps {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"play-album": "Пусни албум",
|
||||
"track": "Песен",
|
||||
"jellyfin-user-id": "Jellyfin потребител",
|
||||
"user-id": "Jellyfin потребител",
|
||||
"play-playlist": "Пусни списък",
|
||||
"color-scheme-system": "Система",
|
||||
"streaming": "Предава се поточно",
|
||||
"download-album": "Изтегли албум",
|
||||
"jellyfin-access-token": "Jellyfin Жетон за достъп (access token)",
|
||||
"access-token": "Jellyfin Жетон за достъп (access token)",
|
||||
"delete-playlist": "Изтрий списък",
|
||||
"add-to-queue": "Добави към опашката",
|
||||
"playback-reporting": "Докладване на изпълненията",
|
||||
@@ -16,7 +16,7 @@
|
||||
"no-results": "Няма резултати…",
|
||||
"total-duration": "Обща продължителност",
|
||||
"error-reporting": "Доклаване на грешки",
|
||||
"jellyfin-server-url": "Адрес на Jellyfin сървър",
|
||||
"server-url": "Адрес на Jellyfin сървър",
|
||||
"downloads": "Изтеглени",
|
||||
"onboarding-cta": "За да започнете Ви трябва Jellyfin сървър. Натиснете бутона отдолу, за да въведете адресът на вашия Jellyfin сървър и се впишете в него.",
|
||||
"shuffle-album": "Разбъркай албум",
|
||||
@@ -26,7 +26,7 @@
|
||||
"artists": "Изпълнители",
|
||||
"queue": "Опашка",
|
||||
"error-reporting-description": "Докато използвате приложението, може да срещнете грешки. Докладването на тези грешки помага на програмата да стане по-сигурна и стабилна.",
|
||||
"set-jellyfin-server": "Настройте Jellyfin сървър",
|
||||
"set-server": "Настройте Jellyfin сървър",
|
||||
"similar-albums": "Подобни албуми",
|
||||
"albums": "Албуми",
|
||||
"why-use-tracking-description": "Проследяването помага да се ускори процесът по разработка на приложението, като се докладват странни гранични случаи и недоглеждания. Това помага на приложението да стане по стабилно и приятно за използване от всички.",
|
||||
@@ -52,7 +52,7 @@
|
||||
"disable": "Изключи",
|
||||
"search": "Търси",
|
||||
"download-playlist": "Изтегли списък",
|
||||
"jellyfin-library": "Jellyfin Библиотека",
|
||||
"library": "Jellyfin Библиотека",
|
||||
"enable-error-reporting": "Желаете ли да пуснете докладването на грешки?",
|
||||
"color-scheme-description": "По подразбиране Fintunes ще следва цветовата схмена на вашата операционна система. Вие обаче можете да изберете Fintunes да се показва винаги в тъмен или светъл режим.",
|
||||
"color-scheme": "Цветова схема",
|
||||
@@ -66,7 +66,7 @@
|
||||
"setting-cache": "Кеш",
|
||||
"local-playback": "Локално изпълняване",
|
||||
"clear-queue": "Изчисти опашката",
|
||||
"set-jellyfin-server-instruction": "Моля въведете адресът на вашия Jellyfin сървър. Трябва да включва протоколът и порта",
|
||||
"set-server-instruction": "Моля въведете адресът на вашия Jellyfin сървър. Трябва да включва протоколът и порта",
|
||||
"delete-all-tracks": "Изтрий всички песни",
|
||||
"where-is-data-stored": "Къде се пазят данните?",
|
||||
"color-scheme-light": "Светъл режим",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Таймер за заспиване",
|
||||
"confirm-delete-all-tracks": "Сигурни ли сте, че искате да изтриете всички текущо изтеглени песни?",
|
||||
"cancel": "Отказ",
|
||||
"delete": "Изтрий"
|
||||
"delete": "Изтрий",
|
||||
"disc": "диск",
|
||||
"lyrics": "Текст на песен"
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"streaming": "Transmissió",
|
||||
"total-duration": "Durada total",
|
||||
"similar-albums": "Àlbums similars",
|
||||
"playback-reporting-description": "Amb Playback Reporting, tots els vostres esdeveniments de reproducció es retransmeten a Jellyfin. Aquest lloc web utilitza galetes per millorar la vostra experiència. Podeu desactivar-lo si ho voleu. Accepto Més informació.",
|
||||
"playback-reporting-description": "Amb Playback Reporting, tots els teus esdeveniments de reproducció es retransmeten a Jellyfin. Aquest lloc web utilitza galetes per millorar la vostra experiència. Podeu desactivar-lo si ho voleu. Accepto Més informació.",
|
||||
"color-scheme": "Aspecte",
|
||||
"color-scheme-description": "Per defecte, Fintunes farà servir l'aspecte del vostre sistema operatiu. Tanmateix, podeu optar per anul·lar-ho i així assegurar-vos que Fintunes estigui sempre en mode fosc o en mode clar.",
|
||||
"color-scheme-system": "Sistema",
|
||||
@@ -52,21 +52,21 @@
|
||||
"onboarding-welcome": "Benvinguts!",
|
||||
"onboarding-intro": "Fintunes us permetrà reproduir la vostra biblioteca de música des de qualsevol lloc, amb total suport per a l'àudio en segon pla i l'enviament del so a altres dispositius.",
|
||||
"onboarding-cta": "Per començar, necessiteu un servidor de Jellyfin. Si us plau, introdueix dins de la caixa de text els caràcters que veu a la imatge de sota. Això és requerit per evitar enviaments automàtics.",
|
||||
"set-jellyfin-server": "Configura el servidor Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Introduïu l'URL del vostre servidor Jellyfin. Assegureu-vos d'incloure el protocol i el port",
|
||||
"set-server": "Configura el servidor Jellyfin",
|
||||
"set-server-instruction": "Introduïu l'URL del vostre servidor Jellyfin. Assegureu-vos d'incloure el protocol i el port",
|
||||
"settings": "Configuració",
|
||||
"jellyfin-library": "Biblioteca Jellyfin",
|
||||
"jellyfin-server-url": "Preguntes Freqüents - FAQ",
|
||||
"jellyfin-access-token": "Jellyfin Access Token",
|
||||
"jellyfin-user-id": "Jellyfin User ID",
|
||||
"library": "Biblioteca Jellyfin",
|
||||
"server-url": "Preguntes Freqüents - FAQ",
|
||||
"access-token": "Jellyfin Access Token",
|
||||
"user-id": "Jellyfin User ID",
|
||||
"setting-cache": "Caché",
|
||||
"setting-cache-description": "Si heu actualitzat la vostra biblioteca de Jellyfin, però l’aplicació està subjectant a actius amagats, podeu esborrar la memòria cau amb aquest botó. Això obligarà l’aplicació a fer una cerca de la biblioteca des de zero.",
|
||||
"recent-albums": "Afegit Recentment",
|
||||
"error-reporting": "Informe d'errors",
|
||||
"error-reporting-description": "Durant l'ús d'aquesta aplicació, es poden trobar errors. L’estudi d’aquests errors ajuda a crear una experiència d’aplicació més segura i estable.",
|
||||
"error-reporting-rationale": "Quan activeu els informes d'errors, cada vegada que es produeix un error, es crea i s'envia automàticament a un servidor, juntament amb informació útil de depuració, com ara dispositius, versions i l'error específic.",
|
||||
"error-reporting-description": "Durant l'ús d'aquesta aplicació, es poden trobar errors. L’aplicació és més segura i estable.",
|
||||
"error-reporting-rationale": "Quan activeu els informes d'errors, cada vegada que es produeix un error, es crea i s'envia automàticament a un servidor, juntament amb informació útil de depuració com ara dispositius, versions i l'error específic.",
|
||||
"why-use-tracking": "Per què utilitzar el seguiment?",
|
||||
"why-use-tracking-description": "El seguiment ajuda a accelerar el desenvolupament d'aquesta aplicació mitjançant l'informe de casos de vora estranys i oversights. Això ajuda a fer que l’aplicació sigui més estable i robusta, millorant així l’experiència de l’aplicació per a tothom.",
|
||||
"why-use-tracking-description": "El seguiment ajuda a accelerar el desenvolupament d'aquesta aplicació mitjançant la presentació de casos de vora estranys i oversights. Això ajuda a fer que l'aplicació sigui més estable i robusta, millorant així l'experiència de l'aplicació per a tothom.",
|
||||
"what-data-is-gathered": "Quines dades es recullen?",
|
||||
"what-data-is-gathered-description": "Registrem l'error, el tipus de dispositiu, la versió del sistema operatiu, la versió de l'aplicació i l'identificador del dispositiu. No s'envia cap estat de l'aplicació en cap informe d'errors. L'identificador del dispositiu és un hash únic que es pot restablir a la configuració del dispositiu i no podem deduir cap informació personal d'aquest identificador.",
|
||||
"where-is-data-stored": "On s’emmagatzemen les dades?",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Temporitzador son",
|
||||
"confirm-delete-all-tracks": "Estàs segur que vols eliminar totes les pistes descarregades actualment?",
|
||||
"delete": "Esborrar",
|
||||
"cancel": "Cancel·lar"
|
||||
"cancel": "Cancel·la",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Lletres"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"play-album": "Přehrát Album",
|
||||
"track": "Skladba",
|
||||
"jellyfin-user-id": "ID uživatele Jellyfin",
|
||||
"user-id": "ID uživatele Jellyfin",
|
||||
"play-playlist": "Přehrát seznam skladeb",
|
||||
"color-scheme-system": "Systém",
|
||||
"streaming": "Streamování",
|
||||
"download-album": "Stáhnout Album",
|
||||
"jellyfin-access-token": "Přístupový token Jellyfin",
|
||||
"access-token": "Přístupový token Jellyfin",
|
||||
"delete-playlist": "Smazat seznam skladeb",
|
||||
"add-to-queue": "Přidat do Fronty",
|
||||
"playback-reporting": "Hlášení o přehrávání",
|
||||
@@ -16,7 +16,7 @@
|
||||
"no-results": "Žádné výsledky…",
|
||||
"total-duration": "Celková doba trvání",
|
||||
"error-reporting": "Hlášení chyb",
|
||||
"jellyfin-server-url": "Adresa URL serveru Jellyfin",
|
||||
"server-url": "Adresa URL serveru Jellyfin",
|
||||
"downloads": "Ke stažení",
|
||||
"onboarding-cta": "Abyste mohli začít, potřebujete server Jellyfin. Kliknutím na tlačítko níže zadejte adresu svého serveru Jellyfin a přihlaste se k němu.",
|
||||
"shuffle-album": "Zamíchat album",
|
||||
@@ -26,7 +26,7 @@
|
||||
"artists": "Umělci",
|
||||
"queue": "Fronta",
|
||||
"error-reporting-description": "Během používání této aplikace můžete narazit na chyby. Nahlášení těchto chyb pomáhá vytvářet bezpečnější a stabilnější prostředí aplikace.",
|
||||
"set-jellyfin-server": "Nastavit Jellyfin Server",
|
||||
"set-server": "Nastavit Jellyfin Server",
|
||||
"similar-albums": "Podobná alba",
|
||||
"albums": "Alba",
|
||||
"why-use-tracking-description": "Sledování pomáhá urychlit vývoj pro tuto aplikaci tím, že nahlásí divné případy hran a dohledy. To pomáhá, aby aplikace stabilnější a robustní, čímž zlepšuje zážitek z aplikace pro každého.",
|
||||
@@ -52,7 +52,7 @@
|
||||
"disable": "Zakázat",
|
||||
"search": "Vyhledat",
|
||||
"download-playlist": "Stáhnnout seznam skladeb",
|
||||
"jellyfin-library": "Knihovna Jellyfin",
|
||||
"library": "Knihovna Jellyfin",
|
||||
"enable-error-reporting": "Chcete povolit hlášení chyb?",
|
||||
"color-scheme-description": "Ve výchozím nastavení bude Fintunes sledovat barevné schéma vašeho operačního systému. Můžete se však rozhodnout toto nastavení změnit, abyste zajistili, že Fintunes bude vždy v tmavém nebo světlém režimu.",
|
||||
"color-scheme": "Barevné Schéma",
|
||||
@@ -66,7 +66,7 @@
|
||||
"setting-cache": "Mezipaměť",
|
||||
"local-playback": "Místní přehrávání",
|
||||
"clear-queue": "Vymazat Frontu",
|
||||
"set-jellyfin-server-instruction": "Zadejte adresu URL serveru Jellyfin. Nezapomeňte uvést protokol a port",
|
||||
"set-server-instruction": "Zadejte adresu URL serveru Jellyfin. Nezapomeňte uvést protokol a port",
|
||||
"delete-all-tracks": "Smazat Všechny Skladby",
|
||||
"where-is-data-stored": "Kde jsou data uložena?",
|
||||
"color-scheme-light": "Světlý Režim",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Časovač spánku",
|
||||
"confirm-delete-all-tracks": "Přejete si skutečně odstranit všechny stažené skladby?",
|
||||
"delete": "Čeština",
|
||||
"cancel": "Hledat"
|
||||
"cancel": "Hledat",
|
||||
"disc": "Čeština",
|
||||
"lyrics": "Text skladby"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"error-reporting": "Fejlrapportering",
|
||||
"music": "Musik",
|
||||
"album": "Album",
|
||||
"jellyfin-server-url": "Jellyfin server URL",
|
||||
"server-url": "Jellyfin server URL",
|
||||
"setting-cache": "Cache",
|
||||
"reset-cache": "Nulstil cache",
|
||||
"enable": "Aktivér",
|
||||
@@ -31,18 +31,18 @@
|
||||
"play-album": "Afspil album",
|
||||
"queue": "Kø",
|
||||
"add-to-queue": "Tilføj til kø",
|
||||
"jellyfin-library": "Jellyfin bibliotek",
|
||||
"jellyfin-user-id": "Jellyfin bruger ID",
|
||||
"library": "Jellyfin bibliotek",
|
||||
"user-id": "Jellyfin bruger ID",
|
||||
"recent-albums": "Seneste albummer",
|
||||
"clear-queue": "Ryd kø",
|
||||
"no-results": "Ingen resultater…",
|
||||
"set-jellyfin-server": "Sæt Jellyfin Server",
|
||||
"set-server": "Sæt Jellyfin Server",
|
||||
"why-use-tracking": "Hvorfor bruge sporing?",
|
||||
"what-data-is-gathered": "Hvilke data indsamles?",
|
||||
"where-is-data-stored": "Hvor gemmes data?",
|
||||
"enable-error-reporting": "Vil du aktivere fejlrapportering?",
|
||||
"playlists": "Spillelister",
|
||||
"playlist": "Spilleliste",
|
||||
"playlist": "Playlist",
|
||||
"play-playlist": "Playlist",
|
||||
"shuffle-playlist": "Shuffle Playlist",
|
||||
"streaming": "Streaming",
|
||||
@@ -51,10 +51,10 @@
|
||||
"color-scheme-system": "Systemsystem",
|
||||
"why-use-tracking-description": "Tracing hjælper med at fremskynde udviklingen for denne app ved at rapportere underlige kantsager og tilsyn. Dette hjælper med at gøre appen mere stabil og robust og dermed forbedre appoplevelsen for alle.",
|
||||
"sleep-timer": "Søvn timer",
|
||||
"onboarding-intro": "Fineunes giver dig mulighed for at streame dit musikbibliotek overalt, med fuld støtte til baggrundslyd og støbning.",
|
||||
"onboarding-cta": "For at komme i gang skal du bruge en Jellyfin server. Klik på knappen nedenfor for at indtaste din Jellyfin server adresse og logge ind på den.",
|
||||
"set-jellyfin-server-instruction": "Indtast venligst din Jellyfin server URL. Sørg for at inkludere protokollen og havnen",
|
||||
"jellyfin-access-token": "Jellyfin Access Token",
|
||||
"onboarding-intro": "Fintunes giver dig mulighed for at streame dit musikbibliotek overalt, med fuld støtte til baggrundslyd og støbning.",
|
||||
"onboarding-cta": "For at komme i gang skal du bruge en Jellyfin server. Klik på knappen nedenfor for at indtaste din Jellyfin-serveradresse og log ind på den.",
|
||||
"set-server-instruction": "Indtast venligst din Jellyfin server URL. Sørg for at inkludere protokollen og havnen",
|
||||
"access-token": "Jellyfin Access Token",
|
||||
"setting-cache-description": "Hvis du har opdateret dit Jellyfin-bibliotek, men appen holder på cachelagrede aktiver, kan du tvinges til at rydde cachen ved hjælp af denne knap. Dette vil tvinge app til at hente biblioteket fra bunden.",
|
||||
"error-reporting-description": "Under brug af denne app, kan du støde fejl. Rapportering af disse fejl hjælper med at skabe en mere sikker og stabil appoplevelse.",
|
||||
"error-reporting-rationale": "Når du aktiverer fejlrapportering, hver gang en fejl opstår, oprettes en rapport automatisk og sendes til en server, sammen med nyttige fejlfindingsoplysninger såsom enheder, versioner og den specifikke fejl.",
|
||||
@@ -66,7 +66,7 @@
|
||||
"download-playlist": "Download Playlist",
|
||||
"no-downloads": "Du har endnu ikke downloadet nogen spor",
|
||||
"delete-playlist": "Slet Playlist",
|
||||
"total-download-size": "Samlet download Størrelse Størrelse Størrelse",
|
||||
"total-download-size": "Samlet download Størrelse Størrelse Størrelse Størrelse",
|
||||
"retry-failed-downloads": "Detaljerede downloads",
|
||||
"you-are-offline-message": "Du er i øjeblikket offline. Du kan kun afspille tidligere downloadet musik.",
|
||||
"playing-on": "At spille på",
|
||||
@@ -74,5 +74,7 @@
|
||||
"color-scheme-description": "Som standard vil Fineunes følge dit operativsystems farveordning. Du kan dog vælge at tilsidesætte dette for at sikre, at Fineunes altid er i mørk tilstand eller lystilstand.",
|
||||
"delete": "Slet",
|
||||
"cancel": "Annuller",
|
||||
"confirm-delete-all-tracks": "Er du sikker på, at du vil slette alle aktuelt downloadede spor?"
|
||||
"confirm-delete-all-tracks": "Er du sikker på, at du vil slette alle aktuelt downloadede spor?",
|
||||
"disc": "Disk",
|
||||
"lyrics": "Sangtekster"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"onboarding-welcome": "Willkommen!",
|
||||
"settings": "Einstellungen",
|
||||
"jellyfin-library": "Jellyfin-Bibliothek",
|
||||
"library": "Jellyfin-Bibliothek",
|
||||
"more-info": "Mehr Informationen",
|
||||
"track": "Titel",
|
||||
"playlists": "Wiedergabelisten",
|
||||
@@ -28,11 +28,11 @@
|
||||
"now-playing": "Läuft gerade",
|
||||
"onboarding-intro": "Mit Fintunes können Sie Ihre Musikbibliothek von überall aus streamen, mit voller Unterstützung für Hintergrundwiedergabe und Casting.",
|
||||
"onboarding-cta": "Um loslegen zu können benötigen Sie einen Jellyfin Server. Klicken Sie auf den Button und geben Sie ihre Jellyfin Serveradresse und Logindaten ein.",
|
||||
"set-jellyfin-server": "Legen Sie den Jellyfin-Server fest",
|
||||
"set-jellyfin-server-instruction": "Bitte geben Sie die URL Ihres Jellyfin-Servers ein. Stellen Sie sicher, dass Sie das Protokoll und den Port angeben",
|
||||
"jellyfin-server-url": "Jellyfin-Server-URL",
|
||||
"jellyfin-access-token": "Jellyfin-Zugriffstoken",
|
||||
"jellyfin-user-id": "Jellyfin-Benutzer-ID",
|
||||
"set-server": "Legen Sie den Jellyfin-Server fest",
|
||||
"set-server-instruction": "Bitte geben Sie die URL Ihres Jellyfin-Servers ein. Stellen Sie sicher, dass Sie das Protokoll und den Port angeben",
|
||||
"server-url": "Jellyfin-Server-URL",
|
||||
"access-token": "Jellyfin-Zugriffstoken",
|
||||
"user-id": "Jellyfin-Benutzer-ID",
|
||||
"setting-cache": "Zwischenspeicher",
|
||||
"setting-cache-description": "Wenn Sie Ihre Jellyfin-Bibliothek aktualisiert haben, aber weiterhin zwischengespeicherte Elemente angezeigt werden kann man mit diesen Button erzwingen, den Zwischenspeicher vollständig zu leeren. Dies zwingt die App Ihre Bibliothek vollständig neu abzurufen.",
|
||||
"reset-cache": "Zwischenspeicher zurücksetzen",
|
||||
@@ -71,8 +71,10 @@
|
||||
"playback-reporting": "Wiedergabeberichte",
|
||||
"playback-reporting-description": "Mit Wiedergabeberichte werden alle Ihre Wiedergabeereignisse zurück an Jellyfin weitergeleitet. Auf diese Weise können Sie Ihre am häufigsten gehörten Songs verfolgen, insbesondere mit Jellyfin-Plugins wie ListenBrainz.",
|
||||
"color-scheme": "Farbschema",
|
||||
"sleep-timer": "Schlafdauer",
|
||||
"sleep-timer": "Schlummerfunktion",
|
||||
"confirm-delete-all-tracks": "Sind Sie sicher, dass Sie alle aktuell heruntergeladenen Titel löschen möchten?",
|
||||
"delete": "Löschen",
|
||||
"cancel": "Abbrechen"
|
||||
"cancel": "Abbrechen",
|
||||
"disc": "Scheiben",
|
||||
"lyrics": "Sangtekster"
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
"now-playing": "Now Playing",
|
||||
"onboarding-welcome": "Welcome!",
|
||||
"onboarding-intro": "Fintunes will allow you to stream your music library from anywhere, with full support for background audio and casting.",
|
||||
"onboarding-cta": "In order to get started, you need a Jellyfin server. Click the button below to enter your Jellyfin server address and login to it.",
|
||||
"set-jellyfin-server": "Set Jellyfin Server",
|
||||
"set-jellyfin-server-instruction": "Please enter your Jellyfin server URL. Make sure to include the protocol and port",
|
||||
"onboarding-cta": "In order to get started, you need a Jellyfin (or Emby) server. Click the button below to enter your server address and login to it.",
|
||||
"set-server": "Set Server",
|
||||
"set-server-instruction": "Please enter your server URL. Make sure to include the protocol and port",
|
||||
"settings": "Settings",
|
||||
"jellyfin-library": "Jellyfin Library",
|
||||
"jellyfin-server-url": "Jellyfin Server URL",
|
||||
"jellyfin-access-token": "Jellyfin Access Token",
|
||||
"jellyfin-user-id": "Jellyfin User ID",
|
||||
"library": "Library",
|
||||
"server-url": "Server URL",
|
||||
"access-token": "Access Token",
|
||||
"user-id": "User ID",
|
||||
"setting-cache": "Cache",
|
||||
"setting-cache-description": "If you have updated your Jellyfin library, but the app is holding on to cached assets, you can forcefully clear the cache using this button. This will force the app to fetch the library from scratch.",
|
||||
"setting-cache-description": "If you have updated your library, but the app is holding on to cached assets, you can forcefully clear the cache using this button. This will force the app to fetch the library from scratch.",
|
||||
"reset-cache": "Reset Cache",
|
||||
"recent-albums": "Recent Albums",
|
||||
"error-reporting": "Error Reporting",
|
||||
@@ -64,7 +64,7 @@
|
||||
"total-duration": "Total duration",
|
||||
"similar-albums": "Similar albums",
|
||||
"playback-reporting": "Playback Reporting",
|
||||
"playback-reporting-description": "With Playback Reporting, all your playback events are relayed back to Jellyfin. This allows you to track your most listened songs, particularly with Jellyfin plugins such as ListenBrainz.",
|
||||
"playback-reporting-description": "With Playback Reporting, all your playback events are relayed back to your server. This allows you to track your most listened songs, particularly with plugins such as ListenBrainz or LastFM.",
|
||||
"color-scheme": "Color Scheme",
|
||||
"color-scheme-description": "By default, Fintunes will follow your operating system's color scheme. You can however choose to override this to make sure Fintunes is always in dark mode or light mode.",
|
||||
"color-scheme-system": "System",
|
||||
@@ -74,5 +74,11 @@
|
||||
"privacy-policy": "Privacy Policy",
|
||||
"sleep-timer": "Sleep timer",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Lyrics",
|
||||
"direct-play": "Direct play",
|
||||
"transcoded": "Transcoded",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
"onboarding-welcome": "Bienvenido!",
|
||||
"onboarding-intro": "Fintunes te permitirá reproducir tu biblioteca musical desde cualquier sitio, con suporte completo para audio en segundo plano y casteo en otros dispositivos.",
|
||||
"onboarding-cta": "Para empezar necesitas un servidor de Jellyfin. Pulsa el botón de abajo para introducir la dirección del servidor y autentifícate con tus credenciales.",
|
||||
"set-jellyfin-server": "Introduce servidor de Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Por favor introduce la URL de tu servidor de Jellyfin. Acuérdate de incluir protocolo y puerto",
|
||||
"set-server": "Introduce servidor de Jellyfin",
|
||||
"set-server-instruction": "Por favor introduce la URL de tu servidor de Jellyfin. Acuérdate de incluir protocolo y puerto",
|
||||
"settings": "Configuración",
|
||||
"jellyfin-library": "Biblioteca Jellyfin",
|
||||
"jellyfin-server-url": "Url del servidor Jellyfin",
|
||||
"jellyfin-access-token": "Token de acceso Jellyfin",
|
||||
"jellyfin-user-id": "ID de usuario Jellyfin",
|
||||
"library": "Biblioteca Jellyfin",
|
||||
"server-url": "Url del servidor Jellyfin",
|
||||
"access-token": "Token de acceso Jellyfin",
|
||||
"user-id": "ID de usuario Jellyfin",
|
||||
"setting-cache": "Caché",
|
||||
"setting-cache-description": "Si actualizó la biblioteca Jellyfin, pero el software aún muestra el contenido anterior, puede hacer clic en el botón a continuación para forzar una actualización. Esto obligará al software a recuperar la información del servidor.",
|
||||
"reset-cache": "Resetear Caché",
|
||||
@@ -72,7 +72,9 @@
|
||||
"color-scheme-system": "Sistema",
|
||||
"privacy-policy": "Política de privacidad",
|
||||
"sleep-timer": "Temporizador",
|
||||
"confirm-delete-all-tracks": "¿Estás seguro de que quieres eliminar todas las pistas descargadas actualmente?",
|
||||
"delete": "Suprimir",
|
||||
"cancel": "Cancelar"
|
||||
"confirm-delete-all-tracks": "¿Estás seguro de que quieres borrar todas las pistas descargadas actualmente?",
|
||||
"delete": "Borrar",
|
||||
"cancel": "Cancelar",
|
||||
"disc": "Disco",
|
||||
"lyrics": "Letras"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"queue": "File d'attente",
|
||||
"add-to-queue": "Ajouter à la file d'attente",
|
||||
"clear-queue": "Vider la file d'attente",
|
||||
"no-results": "Pas de résultats…",
|
||||
"no-results": "Aucun résultat…",
|
||||
"album": "Album",
|
||||
"albums": "Albums",
|
||||
"all-albums": "Tous les Albums",
|
||||
@@ -14,20 +14,20 @@
|
||||
"onboarding-welcome": "Bienvenue !",
|
||||
"onboarding-intro": "Fintunes vous permettra de diffuser votre bibliothèque musicale de n'importe où, avec un support de la lecture en arrière plan et la diffusion à distance.",
|
||||
"onboarding-cta": "Pour utiliser Fintunes, vous avez besoin d'un serveur Jellyfin. Cliquez sur le bouton ci-dessous pour entrer l'adresse de votre serveur Jellyfin et vous y connecter.",
|
||||
"set-jellyfin-server": "Configurer le serveur Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Veuillez entrer l'URL de votre serveur Jellyfin. Assurez-vous d'inclure le protocole et le port",
|
||||
"set-server": "Configurer le serveur Jellyfin",
|
||||
"set-server-instruction": "Veuillez entrer l'URL de votre serveur Jellyfin. Assurez-vous d'inclure le protocole et le port",
|
||||
"settings": "Réglages",
|
||||
"jellyfin-library": "Bibliothèque Jellyfin",
|
||||
"jellyfin-server-url": "URL du serveur Jellyfin",
|
||||
"jellyfin-access-token": "Jeton d'accès à la Jellyfin",
|
||||
"jellyfin-user-id": "ID utilisateur Jellyfin",
|
||||
"library": "Bibliothèque Jellyfin",
|
||||
"server-url": "URL du serveur Jellyfin",
|
||||
"access-token": "Jeton d'accès à la Jellyfin",
|
||||
"user-id": "ID utilisateur Jellyfin",
|
||||
"setting-cache": "Cache",
|
||||
"setting-cache-description": "Si vous avez mis à jour votre bibliothèque Jellyfin mais que l'application conserve toujours des ressources en cache, vous pouvez vider le cache en utilisant ce bouton. Cela forcera l'application à récupérer l’intégralité de bibliothèque.",
|
||||
"reset-cache": "Réinitialiser le cache",
|
||||
"recent-albums": "Albums récents",
|
||||
"what-data-is-gathered": "Quelles données sont collectées ?",
|
||||
"what-data-is-gathered": "Quelles données sont collectées ?",
|
||||
"error-reporting-rationale": "Lorsque vous activez le rapport d'erreurs, chaque fois qu'une erreur se produit, un rapport est automatiquement créé et envoyé à un serveur, avec des informations de débogage utiles telles que les périphériques, les versions et l'erreur spécifique.",
|
||||
"why-use-tracking-description": "Le traçage permet d'accélérer le développement de cette application en signalant des cas bizarres et des surveillances. Cela permet de rendre l'application plus stable et robuste, améliorant ainsi l'expérience de l'application pour tous.",
|
||||
"why-use-tracking-description": "Le suivi aide à accélérer le développement de cette application en signalant les cas étranges et les oublis. Ceci contribue à rendre l'application plus stable et plus robuste, augmentant ainsi l'expérience de l'application pour tout le monde.",
|
||||
"what-data-is-gathered-description": "Nous enregistrons l'erreur, le type d'appareil, la version du système d'exploitation, la version de l'application et l'identifiant de l'appareil. Aucun état d'application n'est envoyé dans les rapports d'erreurs. L'identifiant de l'appareil est un hachage unique qui peut être réinitialisé dans les paramètres de votre appareil, et nous ne pouvons déduire aucune information personnelle de cet identifiant.",
|
||||
"more-info": "Plus d'informations",
|
||||
"play-playlist": "Lire la liste de lecture",
|
||||
@@ -37,16 +37,16 @@
|
||||
"you-are-offline-message": "Vous êtes actuellement hors ligne. Vous ne pouvez lire que de la musique précédemment téléchargée.",
|
||||
"similar-albums": "Albums similaires",
|
||||
"playback-reporting": "Rapport de lecture",
|
||||
"playback-reporting-description": "Avec Playback Reporting, tous vos évènements de lecture sont relayés vers Jellyfin. Ceci vous permet de suivre vos chansons les plus écoutées, notamment avec les greffons Jellyfin tels que ListenBrainz.",
|
||||
"color-scheme-description": "Par défaut, Fintunes suivra le schéma de couleurs de votre système d'exploitation. Vous pouvez cependant choisir de remplacer cela pour vous assurer que Fintunes est toujours en mode sombre ou en mode clair.",
|
||||
"why-use-tracking": "Pourquoi utiliser le suivi ?",
|
||||
"where-is-data-stored": "Où sont stockées les données ?",
|
||||
"enable-error-reporting": "Voulez-vous activer le rapport d'erreur ?",
|
||||
"playback-reporting-description": "Avec Playback Reporting, tous vos évènements de lecture sont relayés vers Jellyfin. Ceci vous permet de suivre vos chansons les plus écoutées, notamment avec les plugins Jellyfin tels que ListenBrainz.",
|
||||
"color-scheme-description": "Par défaut, Fintunes suivra le schéma de couleurs de votre système d'exploitation. Vous pouvez cependant choisir de forcer ce réglage pour vous assurer que Fintunes est toujours en mode sombre ou en mode clair.",
|
||||
"why-use-tracking": "Pourquoi utiliser le suivi ?",
|
||||
"where-is-data-stored": "Où sont stockées les données ?",
|
||||
"enable-error-reporting": "Voulez-vous activer le rapport d'erreur ?",
|
||||
"local-playback": "Lecture locale",
|
||||
"streaming": "Streaming",
|
||||
"color-scheme": "Schéma de couleur",
|
||||
"color-scheme-system": "Système",
|
||||
"where-is-data-stored-description": "Le backend Sentry est auto-hébergé sur notre propre infrastructure. Personne d'autre que nous n'a accès aux serveurs, aux bases de données, aux applications et aux journaux de données, et encore moins au personnel de Sentry. L'infrastructure est hébergée dans l'Union européenne.",
|
||||
"where-is-data-stored-description": "Le backend Sentry est auto-hébergé sur notre propre infrastructure. Personne d'autre que nous n'a accès aux serveurs, aux bases de données, aux applications et aux journaux de données, et encore moins le personnel de Sentry. L'infrastructure est hébergée dans l'Union Européenne.",
|
||||
"enable-error-reporting-description": "Cela permet d'améliorer l'expérience de l'application en nous envoyant des rapports de plantage et d'erreur.",
|
||||
"enable": "Activer",
|
||||
"disable": "Désactiver",
|
||||
@@ -68,11 +68,13 @@
|
||||
"playlists": "Listes de lecture",
|
||||
"downloads": "Téléchargements",
|
||||
"track": "Piste",
|
||||
"error-reporting-description": "Lors de l'utilisation de cette application, vous pouvez rencontrer des erreurs. Le signalement de ces erreurs aide à créer une expérience d'application plus sécurisée et stable.",
|
||||
"error-reporting-description": "Lors de l'utilisation de cette application, vous pouvez rencontrer des erreurs. Le signalement de ces erreurs aide à créer une expérience d'application plus stable et sécurisée.",
|
||||
"error-reporting": "Rapport d'erreur",
|
||||
"privacy-policy": "Politique de confidentialité",
|
||||
"sleep-timer": "Temporaire de sommeil",
|
||||
"confirm-delete-all-tracks": "Êtes-vous sûr de vouloir supprimer toutes les pistes actuellement téléchargées?",
|
||||
"sleep-timer": "Minuterie de veille",
|
||||
"confirm-delete-all-tracks": "Êtes-vous sûr de vouloir supprimer toutes les pistes actuellement téléchargées ?",
|
||||
"delete": "Supprimer",
|
||||
"cancel": "Annuler"
|
||||
"cancel": "Annuler",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Paroles"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"jellyfin-server-url": "URL del Server Jellyfin",
|
||||
"server-url": "URL del Server Jellyfin",
|
||||
"settings": "Impostazioni",
|
||||
"why-use-tracking": "Perché utilizzare il Monitoraggio?",
|
||||
"streaming": "Streaming",
|
||||
@@ -23,11 +23,11 @@
|
||||
"onboarding-welcome": "Benvenuto!",
|
||||
"onboarding-intro": "Fintunes ti consentirà di riprodurre in streaming la tua libreria musicale da dove vuoi, con pieno supporto per l'audio di sottofondo e la trasmissione.",
|
||||
"onboarding-cta": "Per iniziare, hai bisogno di un server Jellyfin. Fai clic sul pulsante in basso per inserire l'indirizzo del tuo server Jellyfin e accedere.",
|
||||
"set-jellyfin-server": "Imposta il Server Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Inserisci l'URL del tuo server Jellyfin. Assicurati di includere il protocollo e la porta",
|
||||
"jellyfin-library": "Libreria Jellyfin",
|
||||
"jellyfin-access-token": "Token di Accesso Jellyfin",
|
||||
"jellyfin-user-id": "ID Utente Jellyfin",
|
||||
"set-server": "Imposta il Server Jellyfin",
|
||||
"set-server-instruction": "Inserisci l'URL del tuo server Jellyfin. Assicurati di includere il protocollo e la porta",
|
||||
"library": "Libreria Jellyfin",
|
||||
"access-token": "Token di Accesso Jellyfin",
|
||||
"user-id": "ID Utente Jellyfin",
|
||||
"setting-cache": "Cache",
|
||||
"setting-cache-description": "Se hai aggiornato la tua libreria Jellyfin, ma l'app trattiene le risorse memorizzate nella cache, puoi forzare la cancellazione della cache utilizzando questo pulsante. Ciò costringerà l'app a recuperare la libreria da zero.",
|
||||
"reset-cache": "Reimposta Cache",
|
||||
@@ -35,7 +35,7 @@
|
||||
"error-reporting": "Segnalazione Errori",
|
||||
"error-reporting-description": "Durante l'utilizzo di questa app, potresti riscontrare errori. La segnalazione di questi errori ci aiuta a creare un'esperienza dell'app più sicura e stabile.",
|
||||
"error-reporting-rationale": "Quando abiliti la segnalazione degli errori, ogni volta che si verifica un errore, viene creato automaticamente un rapporto e inviato a un server, insieme a utili informazioni di debug come dispositivi, versioni e l'errore specifico.",
|
||||
"why-use-tracking-description": "Il tracciamento aiuta a velocizzare lo sviluppo di questa applicazione segnalando strani casi di bordo e sovratensioni. Questo aiuta a rendere l'applicazione più stabile e robusta, migliorando così l'esperienza app per tutti.",
|
||||
"why-use-tracking-description": "Tracing aiuta a velocizzare lo sviluppo per questa applicazione segnalando strani casi di bordo e sovrastudini. Questo aiuta a rendere l'applicazione più stabile e robusto, migliorando così l'esperienza app per tutti.",
|
||||
"what-data-is-gathered": "Quali dati vengono raccolti?",
|
||||
"what-data-is-gathered-description": "Registriamo l'errore, il tipo di dispositivo, la versione del sistema operativo, la versione dell'app e l'ID del dispositivo. Nessuno stato dell'applicazione viene inviato in nessuna segnalazione di errore. L'ID del dispositivo è un hash unico che può essere ripristinato nelle impostazioni del dispositivo e non possiamo dedurre alcuna informazione personale da questo identificatore.",
|
||||
"where-is-data-stored": "Dove vengono archiviati i dati?",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Tempo di sonno",
|
||||
"delete": "Cancella",
|
||||
"cancel": "Annulla",
|
||||
"confirm-delete-all-tracks": "Sei sicuro di voler eliminare tutte le tracce attualmente scaricate?"
|
||||
"confirm-delete-all-tracks": "Sei sicuro di voler eliminare tutte le tracce attualmente scaricate?",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Testi"
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
"onboarding-welcome": "ようこそ。",
|
||||
"onboarding-intro": "Fintunes は、どこからでも音楽ライブラリをストリーミングすることができます。バックグラウンド・オーディオやキャスティングを完全にサポートします。",
|
||||
"onboarding-cta": "始めるためには、Jellyfin サーバーが必要です。下のボタンをクリックして、Jellyfin サーバーのアドレスを入力し、ログインしてください。",
|
||||
"set-jellyfin-server": "Jellyfin サーバ を設定",
|
||||
"set-jellyfin-server-instruction": "Jellyfin サーバの URL を入力してください。プロトコルとポートを必ず含めてください。",
|
||||
"set-server": "Jellyfin サーバ を設定",
|
||||
"set-server-instruction": "Jellyfin サーバの URL を入力してください。プロトコルとポートを必ず含めてください。",
|
||||
"settings": "設定",
|
||||
"jellyfin-library": "Jellyfin ライブライ",
|
||||
"jellyfin-server-url": "Jellyfin サーバの URL",
|
||||
"jellyfin-access-token": "Jellyfin アクセストークン",
|
||||
"jellyfin-user-id": "Jellyfin ユーザ ID",
|
||||
"library": "Jellyfin ライブライ",
|
||||
"server-url": "Jellyfin サーバの URL",
|
||||
"access-token": "Jellyfin アクセストークン",
|
||||
"user-id": "Jellyfin ユーザ ID",
|
||||
"setting-cache": "キャッシュ",
|
||||
"setting-cache-description": "Jellyfinライブラリをアップデートしたにもかかわらず、アプリがキャッシュされたアセットを保持している場合、このボタンを使って強制的にキャッシュをクリアすることができます。これにより、アプリはライブラリを最初から取得するようになります。",
|
||||
"reset-cache": "キャッシュをリセット",
|
||||
@@ -29,7 +29,7 @@
|
||||
"error-reporting-description": "このアプリを使用中に、エラーの可能性があります。これらのエラーを報告することで、よりセキュアで安定したアプリ体験を実現することができます。",
|
||||
"error-reporting-rationale": "エラーレポートを有効にすると、エラーが発生するたびにレポートが自動的に作成され、デバイス、バージョン、特定のエラーなど、デバッグに役立つ情報とともにサーバーに送信されます。",
|
||||
"why-use-tracking": "なぜトラッキングを使うか?",
|
||||
"why-use-tracking-description": "トレンドは、奇妙なエッジケースやオーバーサイトを報告することで、このアプリの開発をスピードアップするのに役立ちます。 これにより、アプリがより安定して堅牢になり、誰もがアプリ体験を向上させることができます.",
|
||||
"why-use-tracking-description": "リリック",
|
||||
"what-data-is-gathered": "どのデータが集まりますか?",
|
||||
"what-data-is-gathered-description": "エラー、デバイスタイプ、OSバージョン、アプリバージョン、デバイスIDが記録されます。いかなるエラー報告においても、アプリケーションの状態は送信されません。デバイスIDは、デバイスの設定でリセット可能な一意のハッシュであり、この識別子から個人情報を推測することはできません。",
|
||||
"where-is-data-stored": "データはどこに保存されていますか?",
|
||||
@@ -71,8 +71,10 @@
|
||||
"local-playback": "ローカル再生",
|
||||
"color-scheme-system": "システム",
|
||||
"privacy-policy": "プライバシーポリシー",
|
||||
"sleep-timer": "睡眠タイマー",
|
||||
"confirm-delete-all-tracks": "現在ダウンロードしたすべてのトラックを削除したいですか?",
|
||||
"sleep-timer": "スリープタイマー",
|
||||
"confirm-delete-all-tracks": "ダウンロードしたすべてのトラックを削除したいですか?",
|
||||
"delete": "削除",
|
||||
"cancel": "キャンセル"
|
||||
"cancel": "キャンセル",
|
||||
"disc": "ディスク",
|
||||
"lyrics": "歌詞"
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
"onboarding-welcome": "Velkommen.",
|
||||
"onboarding-intro": "Fintunes lar deg strømme musikkbiblioteket ditt fra hvor som helst, med full støtte for bakgrunnslyd og casting.",
|
||||
"onboarding-cta": "For å begynne trenger du en Jellyfin-tjener. Klikk på knappen nedenfor for å angi Jellyfin-tjeneradressen din og logge på den.",
|
||||
"set-jellyfin-server": "Sett Jellyfin-tjener",
|
||||
"set-jellyfin-server-instruction": "Skriv inn Jellyfin-tjenerens nettadresse. Sørg for å inkludere protokollen og porten.",
|
||||
"set-server": "Sett Jellyfin-tjener",
|
||||
"set-server-instruction": "Skriv inn Jellyfin-tjenerens nettadresse. Sørg for å inkludere protokollen og porten.",
|
||||
"settings": "Innstillinger",
|
||||
"jellyfin-library": "Jellyfin-bibliotek",
|
||||
"jellyfin-server-url": "Jellyfin-tjenernettadresse",
|
||||
"jellyfin-access-token": "Jellyfin-tilgangssymbol",
|
||||
"jellyfin-user-id": "Jellyfin-bruker-ID",
|
||||
"library": "Jellyfin-bibliotek",
|
||||
"server-url": "Jellyfin-tjenernettadresse",
|
||||
"access-token": "Jellyfin-tilgangssymbol",
|
||||
"user-id": "Jellyfin-bruker-ID",
|
||||
"setting-cache": "Mellomlager",
|
||||
"setting-cache-description": "Du tømme hurtiglageret med denne knappen hvis du har oppdatert Jellyfin-biblioteket ditt, men programmet holder på hurtiglagrede eiendeler. Dette vil tvinge programmet til å hente biblioteket fra bunnen av.",
|
||||
"reset-cache": "Tilbakestill hurtiglager",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Sleep timer",
|
||||
"delete": "Slett",
|
||||
"cancel": "Avbryt",
|
||||
"confirm-delete-all-tracks": "Er du sikker på at du vil slette alle nedlastede spor?"
|
||||
"confirm-delete-all-tracks": "Er du sikker på at du vil slette alle nedlastede spor?",
|
||||
"disc": "Plate",
|
||||
"lyrics": "Sangtekster"
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
"onboarding-welcome": "Welkom!",
|
||||
"onboarding-intro": "Fintunes maakt het mogelijk om van waar dan ook je muziek te streamen, met volledige support voor achtergroundaudio en casting.",
|
||||
"onboarding-cta": "Om te starten moet je een Jellyfin server hebben. Klik de onderstaande knop om het adres van je Jellyfin server in te vullen en er in te loggen.",
|
||||
"set-jellyfin-server": "Stel Jellyfin Server in",
|
||||
"set-jellyfin-server-instruction": "Vul alsjeblieft je Jellyfin server URL in. Voeg ook het protocol en de poort toe",
|
||||
"set-server": "Stel Jellyfin Server in",
|
||||
"set-server-instruction": "Vul alsjeblieft je Jellyfin server URL in. Voeg ook het protocol en de poort toe",
|
||||
"settings": "Instellingen",
|
||||
"jellyfin-library": "Jellyfin Bibliotheek",
|
||||
"jellyfin-server-url": "Jellyfin Server URL",
|
||||
"jellyfin-access-token": "Jellyfin Toegangstoken",
|
||||
"jellyfin-user-id": "Jellyfin Gebruiker ID",
|
||||
"library": "Jellyfin Bibliotheek",
|
||||
"server-url": "Jellyfin Server URL",
|
||||
"access-token": "Jellyfin Toegangstoken",
|
||||
"user-id": "Jellyfin Gebruiker ID",
|
||||
"setting-cache": "Cache",
|
||||
"setting-cache-description": "Als je Jellyfin bibliotheek geüpdatet hebt, maar de app nog aan elementen uit de cache vasthoudt, kun je de cache geforceerd leegmaken met deze knop. Dit forceert de app om de bibliotheek weer vanaf het begin op te bouwen.",
|
||||
"reset-cache": "Leeg Cache",
|
||||
@@ -74,5 +74,11 @@
|
||||
"privacy-policy": "Privacybeleid",
|
||||
"sleep-timer": "Slaaptimer",
|
||||
"delete": "Verwijder",
|
||||
"cancel": "Annuleer"
|
||||
"cancel": "Annuleer",
|
||||
"disc": "Schijf",
|
||||
"direct-play": "Direct afgespeeld",
|
||||
"transcoded": "Getranscodeerd",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps",
|
||||
"lyrics": "Songtekst"
|
||||
}
|
||||
|
||||
@@ -1,78 +1,80 @@
|
||||
{
|
||||
"onboarding-welcome": "Welcome!",
|
||||
"onboarding-intro": "Fintuny pozwolą ściągać swoją bibliotekę muzyczną z nigdzie, z pełnym wsparciem dla dźwięku i odlewu.",
|
||||
"set-jellyfin-server": "Set Jellyfin Server (ang.)",
|
||||
"settings": "Setting",
|
||||
"reset-cache": "Reset Cache",
|
||||
"color-scheme-light": "Przełomowy",
|
||||
"color-scheme-dark": "Dark Mode",
|
||||
"play-next": "Playować",
|
||||
"play-album": "Play Album",
|
||||
"queue": "Queue",
|
||||
"add-to-queue": "Addd",
|
||||
"clear-queue": "Clear Queue",
|
||||
"no-results": "Nie ma rezultatów.",
|
||||
"album": "Albumy",
|
||||
"albums": "Album",
|
||||
"onboarding-welcome": "Witaj!",
|
||||
"onboarding-intro": "Fintunes umożliwia strumieniowe przesyłanie biblioteki muzycznej z dowolnego miejsca, z pełną obsługą dźwięku w tle i przesyłania.",
|
||||
"set-server": "Ustaw Serwer Jellyfin",
|
||||
"settings": "Ustawienia",
|
||||
"reset-cache": "Zresetuj pamięć podręczną",
|
||||
"color-scheme-light": "Jasny motyw",
|
||||
"color-scheme-dark": "Ciemny motyw",
|
||||
"play-next": "Odtwórz następne",
|
||||
"play-album": "Odtwórz Album",
|
||||
"queue": "Kolejka",
|
||||
"add-to-queue": "Dodaj do kolejki",
|
||||
"clear-queue": "Wyczyść kolejkę",
|
||||
"no-results": "Brak wyników…",
|
||||
"album": "Album",
|
||||
"albums": "Albumy",
|
||||
"all-albums": "Wszystkie albumy",
|
||||
"search": "Search",
|
||||
"music": "Music",
|
||||
"now-playing": "Teraz grasz",
|
||||
"onboarding-cta": "Aby rozpocząć pracę, potrzebuje serwera Jellyfin. Click the button below to enter your Jellyfin server address and login to it.",
|
||||
"set-jellyfin-server-instruction": "Wstęp do serwera Jellyfin URL. Umożliwia to łączenie protokołu i portu",
|
||||
"jellyfin-library": "Biblioteka Jellyfin",
|
||||
"jellyfin-server-url": "Jellyfin Server URL",
|
||||
"jellyfin-access-token": "Jellyfin Access Token (ang.)",
|
||||
"jellyfin-user-id": "Jellyfin User ID",
|
||||
"setting-cache": "Kache",
|
||||
"setting-cache-description": "Jeśli uaktualnisz swoją bibliotekę Jellyfin, ale aplikacja trzymana jest w pamięci podręcznej, możesz wyjaśnić pamięć podręczną używając tego przycisku. Wykorzystuje to aplikację, aby uwolnić bibliotekę z odrzutu.",
|
||||
"recent-albums": "Recent Album",
|
||||
"error-reporting": "Error Reporting (ang.)",
|
||||
"error-reporting-description": "Podczas korzystania z tej aplikacji może poznać błędy. Odnotowując te błędy pomagają stworzyć bardziej bezpieczne i stabilne doświadczenie.",
|
||||
"error-reporting-rationale": "Kiedy umożliwiasz informację błędów, każdy czas, gdy następuje błąd, automatycznie zostaje stworzony i wysłany do serwera, wraz z pomocnym w debugowaniu informacji takich jak urządzenia, wersje i błąd.",
|
||||
"why-use-tracking": "Dlaczego użyczasz?",
|
||||
"why-use-tracking-description": "Tracing pomaga szybko rozwijać się na tę aplikację poprzez sprawdzenie dziwnych przypadków brzegowych i nadzorów. Pomoże to uczynić aplikację bardziej stabilną i solidną, tym samym poprawiając doświadczenie aplikacji dla wszystkich.",
|
||||
"what-data-is-gathered": "Jakie dane są zebrane?",
|
||||
"what-data-is-gathered-description": "Wykorzystujemy błąd, typ urządzenia, wersja OS, wersja aplikacji i urządzenie. Żaden z nich nie jest wysyłany w żadnym raporcie. Urządzenie id jest unikalnym hasłem, który może być resetowany w twoich ustawieniach urządzeniem, i nie można wywnioskować żadnych informacji osobistych z tych identyfikatorów.",
|
||||
"where-is-data-stored": "Gdzie jest przechowywane dane?",
|
||||
"where-is-data-stored-description": "Sentry backend jest samoprowadzany na własną infrastrukturę. Nikt nie ma dostępu do serwerów, baz danych, aplikacji i logów danych, przynajmniej dla wszystkich pracowników Sentry. Infrastruktura jest organizowana w Unii Europejskiej.",
|
||||
"enable-error-reporting": "Czy chcesz pozwolić na błędy?",
|
||||
"enable-error-reporting-description": "Umożliwia to poprawę doświadczenia aplikacji poprzez wysłanie raportów zderzenia i błędów.",
|
||||
"enable": "Pełny",
|
||||
"disable": "Dyskretny",
|
||||
"more-info": "Info",
|
||||
"track": "Track",
|
||||
"playlists": "Playlista",
|
||||
"playlist": "Playlista",
|
||||
"play-playlist": "Playlista",
|
||||
"shuffle-album": "Album Shuffle Album",
|
||||
"shuffle-playlist": "Oficjalna strona Shuffle Play",
|
||||
"downloads": "Download",
|
||||
"download-track": "Download Track",
|
||||
"download-album": "Download Album",
|
||||
"download-playlist": "Download Playlist",
|
||||
"no-downloads": "Nie można jeszcze pobrać żadnych utworów",
|
||||
"delete-track": "Trasa Delete",
|
||||
"delete-all-tracks": "Delete All Tracks",
|
||||
"delete-album": "Album Delete",
|
||||
"delete-playlist": "Delete Playlista",
|
||||
"total-download-size": "Total Download Size",
|
||||
"retry-failed-downloads": "Retry Failed Download (ang.)",
|
||||
"you-are-offline-message": "Obecnie są one offline. Mogą grać tylko wcześniej.",
|
||||
"playing-on": "Zagrać",
|
||||
"local-playback": "Local playback",
|
||||
"streaming": "Streaming (ang.)",
|
||||
"total-duration": "Czas trwania Totalna",
|
||||
"similar-albums": "Album",
|
||||
"playback-reporting": "Playback Reporting",
|
||||
"playback-reporting-description": "Z Playback Reporting wszystkie wydarzenia z playbacku zostały przekazane Jellyfinowi. Pozwala to na śledzenie najbardziej wysłuchanych piosenek, zwłaszcza z wtyczkami Jellyfin, takimi jak ListenBrainz.",
|
||||
"color-scheme": "Color Scheme",
|
||||
"color-scheme-description": "Domyślnie, Fintunes będzie podążał za kolorem systemu operacyjnego. Możesz jednak zdecydować, że Fintunes jest zawsze w trybie ciemnym lub lekkim.",
|
||||
"search": "Szukaj",
|
||||
"music": "Muzyka",
|
||||
"now-playing": "Teraz gra",
|
||||
"onboarding-cta": "Aby rozpocząć pracę, potrzebujesz serwera Jellyfin. Kliknij poniższy przycisk, aby wprowadzić adres serwera Jellyfin i zalogować się do niego.",
|
||||
"set-server-instruction": "Wprowadź adres URL serwera Jellyfin. Upewnij się, że podałeś protokół i port",
|
||||
"library": "Biblioteka Jellyfin",
|
||||
"server-url": "URL do serwera Jellyfin",
|
||||
"access-token": "Token dostępowy do Jellyfin",
|
||||
"user-id": "ID użytkownika Jellyfin",
|
||||
"setting-cache": "Pamięć podręczna",
|
||||
"setting-cache-description": "Jeśli zaktualizowałeś bibliotekę Jellyfin, ale aplikacja przechowuje zasoby w pamięci podręcznej, możesz wymusić wyczyszczenie pamięci podręcznej za pomocą tego przycisku. Zmusi to aplikację do pobrania biblioteki od zera.",
|
||||
"recent-albums": "Ostatnie Albumy",
|
||||
"error-reporting": "Reportowanie błędów",
|
||||
"error-reporting-description": "Podczas korzystania z tej aplikacji mogą wystąpić błędy. Zgłaszanie tych błędów pomaga w tworzeniu bezpieczniejszej i stabilniejszej aplikacji.",
|
||||
"error-reporting-rationale": "Po włączeniu raportowania błędów za każdym razem, gdy wystąpi błąd, raport jest automatycznie tworzony i wysyłany na serwer wraz z pomocnymi informacjami dotyczącymi debugowania, takimi jak urządzenia, wersje i konkretny błąd.",
|
||||
"why-use-tracking": "Dlaczego warto korzystać z funkcji śledzenia?",
|
||||
"why-use-tracking-description": "Śledzenie pomaga przyspieszyć rozwój tej aplikacji poprzez zgłaszanie dziwnych przypadków brzegowych i niedopatrzeń. Pomaga to uczynić aplikację bardziej stabilną i solidną, poprawiając w ten sposób wrażenia z korzystania z niej dla wszystkich.",
|
||||
"what-data-is-gathered": "Jakie dane są zbierane?",
|
||||
"what-data-is-gathered-description": "Rejestrujemy błąd, typ urządzenia, wersję systemu operacyjnego, wersję aplikacji i identyfikator urządzenia. W raportach o błędach nie jest wysyłany stan aplikacji. Identyfikator urządzenia jest unikalnym skrótem, który można zresetować w ustawieniach urządzenia i nie możemy wywnioskować z niego żadnych danych osobowych.",
|
||||
"where-is-data-stored": "Gdzie są przechowywane dane?",
|
||||
"where-is-data-stored-description": "Backend Sentry jest hostowany na naszej własnej infrastrukturze. Nikt poza nami nie ma dostępu do serwerów, baz danych, aplikacji i logów danych, a już w szczególności żaden personel Sentry. Infrastruktura jest hostowana na terenie Unii Europejskiej.",
|
||||
"enable-error-reporting": "Czy chcesz włączyć raportowanie błędów?",
|
||||
"enable-error-reporting-description": "Pomaga to ulepszyć działanie aplikacji, wysyłając do nas raporty o awariach i błędach.",
|
||||
"enable": "Włączony",
|
||||
"disable": "Wyłączony",
|
||||
"more-info": "Więcej Informacji",
|
||||
"track": "Utwór",
|
||||
"playlists": "Listy odtwarzania",
|
||||
"playlist": "Lista odtwarzania",
|
||||
"play-playlist": "Odtwórz listę odtwarzania",
|
||||
"shuffle-album": "Przemieszaj album",
|
||||
"shuffle-playlist": "Losuj listę odtwarzania",
|
||||
"downloads": "Pobrane",
|
||||
"download-track": "Pobrane Utwory",
|
||||
"download-album": "Pobrane Albumy",
|
||||
"download-playlist": "Pobrane listy odtwarzania",
|
||||
"no-downloads": "Nie pobrałeś jeszcze żadnych utworów",
|
||||
"delete-track": "Usuń utwór",
|
||||
"delete-all-tracks": "Usuń wszystkie utwory",
|
||||
"delete-album": "Usuń album",
|
||||
"delete-playlist": "Usuń listę odtwarzania",
|
||||
"total-download-size": "Całkowity rozmiar pobierania",
|
||||
"retry-failed-downloads": "Ponów nieudane pobieranie",
|
||||
"you-are-offline-message": "Aktualnie jesteś offline. Można odtwarzać tylko wcześniej pobraną muzykę.",
|
||||
"playing-on": "Gra dalej",
|
||||
"local-playback": "Odtwarzanie lokalne",
|
||||
"streaming": "Transmisja strumieniowa",
|
||||
"total-duration": "Całkowity czas trwania",
|
||||
"similar-albums": "Podobne albumy",
|
||||
"playback-reporting": "Raportowanie odtwarzania",
|
||||
"playback-reporting-description": "Dzięki raportowaniu odtwarzania wszystkie zdarzenia odtwarzania są przekazywane z powrotem do Jellyfin. Umożliwia to śledzenie najczęściej słuchanych utworów, szczególnie przy użyciu wtyczek Jellyfin, takich jak ListenBrainz.",
|
||||
"color-scheme": "Schemat kolorów",
|
||||
"color-scheme-description": "Domyślnie Fintunes będzie stosować się do schematu kolorów Twojego systemu operacyjnego. Możesz jednak pominąć tę opcję, aby mieć pewność, że Fintunes jest zawsze w trybie ciemnym lub jasnym.",
|
||||
"color-scheme-system": "System",
|
||||
"artists": "Artysta",
|
||||
"privacy-policy": "Polityka prywatna",
|
||||
"sleep-timer": "Sleep timer",
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"confirm-delete-all-tracks": "Czy chcesz usunąć wszystkie aktualne utwory?"
|
||||
"privacy-policy": "Polityka prywatności",
|
||||
"sleep-timer": "Timer uśpienia",
|
||||
"delete": "Usuń",
|
||||
"cancel": "Anuluj",
|
||||
"confirm-delete-all-tracks": "Czy na pewno chcesz usunąć wszystkie aktualnie pobrane utwory?",
|
||||
"disc": "Disc disc",
|
||||
"lyrics": "Tekst utworu"
|
||||
}
|
||||
|
||||
@@ -24,21 +24,21 @@
|
||||
"onboarding-welcome": "Boas-vindas!",
|
||||
"onboarding-intro": "O Fintunes permite a você fazer streaming de sua biblioteca de músicas de qualquer lugar, com suporte total para reprodução de áudio em segundo plano e transmissão de áudio.",
|
||||
"onboarding-cta": "Para usar o Fintunes, você precisa de um servidor Jellyfin. Clique no botão abaixo para inserir o endereço do servidor Jellyfin e fazer login.",
|
||||
"set-jellyfin-server": "Configurar o servidor Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Insira a URL do servidor Jellyfin. Certifique-se de incluir o protocolo e a porta",
|
||||
"set-server": "Configurar o servidor Jellyfin",
|
||||
"set-server-instruction": "Insira a URL do servidor Jellyfin. Certifique-se de incluir o protocolo e a porta",
|
||||
"settings": "Configurações",
|
||||
"error-reporting-description": "Durante o uso deste aplicativo, você pode encontrar erros. Relatar esses erros ajuda a criar uma experiência de aplicativo mais segura e estável.",
|
||||
"error-reporting-rationale": "Quando você ativa o relatório de erros, toda vez que ocorre um erro, um relatório é criado automaticamente e enviado a um servidor, juntamente com informações úteis de depuração, como dispositivos, versões e o erro específico.",
|
||||
"jellyfin-library": "Biblioteca do Jellyfin",
|
||||
"jellyfin-server-url": "URL do servidor Jellyfin",
|
||||
"jellyfin-access-token": "Token de acesso do Jellyfin",
|
||||
"jellyfin-user-id": "ID de usuário do Jellyfin",
|
||||
"library": "Biblioteca do Jellyfin",
|
||||
"server-url": "URL do servidor Jellyfin",
|
||||
"access-token": "Token de acesso do Jellyfin",
|
||||
"user-id": "ID de usuário do Jellyfin",
|
||||
"setting-cache": "Cache",
|
||||
"setting-cache-description": "Se você atualizou sua biblioteca do Jellyfin, mas o aplicativo continua usando os recursos em cache, você pode forçar a limpeza do cache usando este botão. Isso forçará o aplicativo a recuperar a biblioteca do zero.",
|
||||
"reset-cache": "Redefinir cache",
|
||||
"recent-albums": "Álbuns recentes",
|
||||
"error-reporting": "Relatório de erros",
|
||||
"why-use-tracking-description": "Tracing ajuda a acelerar o desenvolvimento para este aplicativo, relatando casos de borda estranha e supervisão. Isso ajuda a tornar o aplicativo mais estável e robusto, melhorando assim a experiência do aplicativo para todos.",
|
||||
"why-use-tracking-description": "O rastreamento de erros ajuda a acelerar o desenvolvimento deste aplicativo, relatando casos raros estranhos e descuidos. Isso ajuda a tornar o aplicativo mais estável e robusto, melhorando assim a experiência do aplicativo para todos.",
|
||||
"what-data-is-gathered": "Quais dados são coletados?",
|
||||
"what-data-is-gathered-description": "Registramos o erro, o tipo de dispositivo, a versão do sistema operacional, a versão do aplicativo e a identificação do dispositivo. Nenhum estado do aplicativo é enviado em nenhum relatório de erro. A ID do dispositivo é um hash exclusivo que pode ser redefinido nas configurações do seu dispositivo, e não podemos deduzir nenhuma informação pessoal desse identificador.",
|
||||
"enable-error-reporting-description": "Isso ajuda a melhorar a experiência do aplicativo, enviando relatórios de falhas e erros para nós.",
|
||||
@@ -72,7 +72,9 @@
|
||||
"artists": "Artistas",
|
||||
"privacy-policy": "Política de Privacidade",
|
||||
"sleep-timer": "Temporizador de sono",
|
||||
"confirm-delete-all-tracks": "Você tem certeza de que deseja excluir todas as faixas atualmente baixadas?",
|
||||
"confirm-delete-all-tracks": "Tem a certeza de que deseja excluir todas as faixas atualmente baixadas?",
|
||||
"delete": "Excluir",
|
||||
"cancel": "Cancelar"
|
||||
"cancel": "Cancelar",
|
||||
"disc": "Disco",
|
||||
"lyrics": "Letras"
|
||||
}
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
"onboarding-welcome": "Добро пожаловать!",
|
||||
"onboarding-intro": "Fintunes позволит вам транслировать вашу музыкальную библиотеку из любого места с полной поддержкой фонового воспроизведения и трансляций.",
|
||||
"onboarding-cta": "Для начала вам нужен сервер Jellyfin. Нажмите кнопку ниже, чтобы ввести адрес своего сервера Jellyfin и авторизоваться на нем.",
|
||||
"set-jellyfin-server": "Установить сервер Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Пожалуйста, введите URL вашего сервера Jellyfin. Обязательно укажите протокол и порт",
|
||||
"set-server": "Установить сервер Jellyfin",
|
||||
"set-server-instruction": "Пожалуйста, введите URL вашего сервера Jellyfin. Обязательно укажите протокол и порт",
|
||||
"settings": "Настройки",
|
||||
"jellyfin-library": "Библиотека Jellyfin",
|
||||
"jellyfin-server-url": "URL сервера Jellyfin",
|
||||
"jellyfin-access-token": "Токен доступа Jellyfin",
|
||||
"jellyfin-user-id": "Идентификатор пользователя Jellyfin",
|
||||
"library": "Библиотека Jellyfin",
|
||||
"server-url": "URL сервера Jellyfin",
|
||||
"access-token": "Токен доступа Jellyfin",
|
||||
"user-id": "Идентификатор пользователя Jellyfin",
|
||||
"setting-cache": "Кэш",
|
||||
"setting-cache-description": "Если вы обновили свою библиотеку Jellyfin, но приложение сохраняет кэшированные ресурсы, вы можете принудительно очистить кэш с помощью этой кнопки. Это заставит приложение обновить библиотеку.",
|
||||
"reset-cache": "Сбросить кеш",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Время сна",
|
||||
"confirm-delete-all-tracks": "Вы уверены, что хотите удалить все загруженные в настоящее время треки?",
|
||||
"delete": "Удалить",
|
||||
"cancel": "Отмена"
|
||||
"cancel": "Отмена",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Текст песни"
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
"search": "Iskanje",
|
||||
"music": "Glasba",
|
||||
"now-playing": "Zdaj igra",
|
||||
"set-jellyfin-server": "Nastavite strežnik Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Vnesite URL svojega strežnika Jellyfin. Ne pozabite vključiti protokola in vrat",
|
||||
"set-server": "Nastavite strežnik Jellyfin",
|
||||
"set-server-instruction": "Vnesite URL svojega strežnika Jellyfin. Ne pozabite vključiti protokola in vrat",
|
||||
"settings": "Nastavitve",
|
||||
"jellyfin-library": "Knjižnica Jellyfin",
|
||||
"jellyfin-server-url": "URL strežnika Jellyfin",
|
||||
"jellyfin-access-token": "Žeton za dostop Jellyfin",
|
||||
"jellyfin-user-id": "ID uporabnika Jellyfin",
|
||||
"library": "Knjižnica Jellyfin",
|
||||
"server-url": "URL strežnika Jellyfin",
|
||||
"access-token": "Žeton za dostop Jellyfin",
|
||||
"user-id": "ID uporabnika Jellyfin",
|
||||
"setting-cache": "Predpomnilnik",
|
||||
"setting-cache-description": "Če ste posodobili knjižnico Jellyfin, vendar aplikacija zadržuje predpomnjena sredstva, lahko s tem gumbom prisilno počistite predpomnilnik. To bo prisililo aplikacijo, da znova pridobi knjižnico.",
|
||||
"reset-cache": "Ponastavi predpomnilnik",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Časovnik za spanje",
|
||||
"cancel": "Prekliči",
|
||||
"confirm-delete-all-tracks": "Ali ste prepričani, da želite izbrisati vse trenutno prenesene skladbe?",
|
||||
"delete": "Izbriši"
|
||||
"delete": "Izbriši",
|
||||
"disc": "Disk",
|
||||
"lyrics": "Besedilo"
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"onboarding-welcome": "Välkommen!",
|
||||
"onboarding-intro": "Fintunes låter dig strömma ditt musikbibliotek var som helst, med fullt stöd för bakgrundsljud och casting.",
|
||||
"onboarding-cta": "För att komma igång behöver du en Jellyfin-server. Klicka på knappen nedan för att ange din Jellyfin-serveradress och logga in på den.",
|
||||
"set-jellyfin-server": "Ställ in Jellyfin-server",
|
||||
"set-jellyfin-server-instruction": "Ange din Jellyfin-server-URL. Se till att inkludera protokoll och port",
|
||||
"set-server": "Ställ in Jellyfin-server",
|
||||
"set-server-instruction": "Ange din Jellyfin-server-URL. Se till att inkludera protokoll och port",
|
||||
"settings": "inställningar",
|
||||
"jellyfin-library": "Jellyfin-bibliotek",
|
||||
"jellyfin-server-url": "Jellyfin Server URL",
|
||||
"jellyfin-access-token": "Jellyfin Access Token",
|
||||
"jellyfin-user-id": "Jellyfin användar-ID",
|
||||
"library": "Jellyfin-bibliotek",
|
||||
"server-url": "Jellyfin Server URL",
|
||||
"access-token": "Jellyfin Access Token",
|
||||
"user-id": "Jellyfin användar-ID",
|
||||
"setting-cache": "Cache",
|
||||
"setting-cache-description": "Om du har uppdaterat ditt Jellyfin-bibliotek men appen håller kvar cachelagrade tillgångar kan du tvångsrensa cachen med den här knappen. Detta kommer att tvinga appen att hämta biblioteket från början.",
|
||||
"reset-cache": "Töm cache",
|
||||
@@ -74,5 +74,7 @@
|
||||
"sleep-timer": "Sova timer",
|
||||
"delete": "Delete",
|
||||
"cancel": "Avbokning",
|
||||
"confirm-delete-all-tracks": "Är du säker på att du vill ta bort alla för närvarande nedladdade spår?"
|
||||
"confirm-delete-all-tracks": "Är du säker på att du vill ta bort alla för närvarande nedladdade spår?",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Låttext"
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
"onboarding-welcome": "Ласкаво просимо!",
|
||||
"onboarding-intro": "Fintunes дозволить вам транслювати свою музичну бібліотеку з будь-якого місця з повною підтримкою фонового звуку та трансляції.",
|
||||
"onboarding-cta": "Щоб почати, вам потрібен сервер Jellyfin. Натисніть кнопку нижче, щоб ввести адресу свого сервера Jellyfin і увійти на нього.",
|
||||
"set-jellyfin-server": "Встановити сервер Jellyfin",
|
||||
"set-jellyfin-server-instruction": "Будь ласка, введіть URL-адресу вашого сервера Jellyfin. Не забудьте вказати протокол і порт",
|
||||
"set-server": "Встановити сервер Jellyfin",
|
||||
"set-server-instruction": "Будь ласка, введіть URL-адресу вашого сервера Jellyfin. Не забудьте вказати протокол і порт",
|
||||
"settings": "Налаштування",
|
||||
"jellyfin-library": "Бібліотека Jellyfin",
|
||||
"jellyfin-server-url": "URL-адреса сервера Jellyfin",
|
||||
"jellyfin-access-token": "Токен доступу Jellyfin",
|
||||
"jellyfin-user-id": "ID користувача Jellyfin",
|
||||
"library": "Бібліотека Jellyfin",
|
||||
"server-url": "URL-адреса сервера Jellyfin",
|
||||
"access-token": "Токен доступу Jellyfin",
|
||||
"user-id": "ID користувача Jellyfin",
|
||||
"setting-cache": "Кеш",
|
||||
"setting-cache-description": "Якщо ви оновили свою бібліотеку Jellyfin, але застосунок зберігає кешовані ресурси, ви можете примусово очистити кеш за допомогою цієї кнопки. Це змусить застосунок отримати бібліотеку з нуля.",
|
||||
"reset-cache": "Скинути кеш",
|
||||
@@ -73,6 +73,8 @@
|
||||
"privacy-policy": "Політика конфіденційності",
|
||||
"sleep-timer": "Таймер сну",
|
||||
"confirm-delete-all-tracks": "Ви впевнені, що ви хочете видалити всі завантажені треки?",
|
||||
"delete": "Делет",
|
||||
"cancel": "Зареєструватися"
|
||||
"delete": "Видалити",
|
||||
"cancel": "Скасувати",
|
||||
"disc": "Диски",
|
||||
"lyrics": "Лірика"
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
"onboarding-welcome": "欢迎",
|
||||
"onboarding-intro": "Fintunes可以在任何地方播放Jellyfin库中的音乐。",
|
||||
"onboarding-cta": "在开始前,你需要一个Jellyfin服务器。点击下方的按钮连接到服务器并登录。",
|
||||
"set-jellyfin-server": "设置Jellyfin服务器",
|
||||
"set-jellyfin-server-instruction": "设置Jellyfin服务器的完整网址,包括HTTP/HTTS和端口。",
|
||||
"set-server": "设置Jellyfin服务器",
|
||||
"set-server-instruction": "设置Jellyfin服务器的完整网址,包括HTTP/HTTS和端口。",
|
||||
"settings": "设置",
|
||||
"jellyfin-library": "Jellyfin库",
|
||||
"jellyfin-server-url": "Jellyfin服务器网址",
|
||||
"jellyfin-access-token": "Jellyfin Access Token",
|
||||
"jellyfin-user-id": "Jellyfin用户ID",
|
||||
"library": "Jellyfin库",
|
||||
"server-url": "Jellyfin服务器网址",
|
||||
"access-token": "Jellyfin Access Token",
|
||||
"user-id": "Jellyfin用户ID",
|
||||
"setting-cache": "缓存",
|
||||
"setting-cache-description": "如果你更新了Jellyfin库,但软件仍显示之前的内容,你可以点击下面的按钮强制刷新。这将强制软件重新从服务器获取信息。",
|
||||
"reset-cache": "清除缓存",
|
||||
@@ -72,7 +72,9 @@
|
||||
"color-scheme-light": "灯光模式",
|
||||
"privacy-policy": "隐私政策",
|
||||
"sleep-timer": "睡眠定时器",
|
||||
"confirm-delete-all-tracks": "你们是否希望删除目前下载的所有轨道?",
|
||||
"confirm-delete-all-tracks": "您确定要删除所有当前下载的曲目吗?",
|
||||
"delete": "删除",
|
||||
"cancel": "取消"
|
||||
"cancel": "取消",
|
||||
"disc": "歧视",
|
||||
"lyrics": "歌词"
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ export type LocaleKeys = 'play-next'
|
||||
| 'onboarding-welcome'
|
||||
| 'onboarding-intro'
|
||||
| 'onboarding-cta'
|
||||
| 'set-jellyfin-server'
|
||||
| 'set-jellyfin-server-instruction'
|
||||
| 'set-server'
|
||||
| 'set-server-instruction'
|
||||
| 'settings'
|
||||
| 'jellyfin-library'
|
||||
| 'jellyfin-server-url'
|
||||
| 'jellyfin-access-token'
|
||||
| 'jellyfin-user-id'
|
||||
| 'library'
|
||||
| 'server-url'
|
||||
| 'access-token'
|
||||
| 'user-id'
|
||||
| 'setting-cache'
|
||||
| 'setting-cache-description'
|
||||
| 'reset-cache'
|
||||
@@ -73,4 +73,10 @@ export type LocaleKeys = 'play-next'
|
||||
| 'privacy-policy'
|
||||
| 'sleep-timer'
|
||||
| 'delete'
|
||||
| 'cancel'
|
||||
| 'cancel'
|
||||
| 'disc'
|
||||
| 'lyrics'
|
||||
| 'direct-play'
|
||||
| 'transcoded'
|
||||
| 'khz'
|
||||
| 'kbps'
|
||||
|
||||
@@ -12,7 +12,7 @@ import DownloadIcon from '@/components/DownloadIcon';
|
||||
import styled from 'styled-components/native';
|
||||
import { Text } from '@/components/Typography';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { SafeFlatList } from '@/components/SafeNavigatorView';
|
||||
import { t } from '@/localisation';
|
||||
|
||||
@@ -17,9 +17,11 @@ import { calculateProgressTranslation } from '@/components/Progresstrack';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const NOW_PLAYING_POPOVER_MARGIN = 6;
|
||||
const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN;
|
||||
export const NOW_PLAYING_POPOVER_MARGIN = 6;
|
||||
export const NOW_PLAYING_POPOVER_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN;
|
||||
export const NOW_PLAYING_POPOVER_HEIGHT = 58;
|
||||
|
||||
const PopoverPosition = css`
|
||||
position: absolute;
|
||||
@@ -34,6 +36,7 @@ const Container = styled.ScrollView`
|
||||
`;
|
||||
|
||||
const InnerContainer = styled.TouchableOpacity`
|
||||
height: ${NOW_PLAYING_POPOVER_HEIGHT}px;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
@@ -105,18 +108,19 @@ function SelectActionButton() {
|
||||
}
|
||||
}
|
||||
|
||||
function NowPlaying({ offset = 0 }: { offset?: number }) {
|
||||
function NowPlaying({ offset = 0, inset }: { offset?: number, inset?: boolean }) {
|
||||
const { index, track } = useCurrentTrack();
|
||||
const { buffered, position } = useProgress();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const tabBarHeight = useBottomTabBarHeight();
|
||||
const insets = useSafeAreaInsets();
|
||||
const previousBuffered = usePrevious(buffered);
|
||||
const previousPosition = usePrevious(position);
|
||||
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
|
||||
const bufferAnimation = useRef(new Animated.Value(0));
|
||||
const progressAnimation = useRef(new Animated.Value(0));
|
||||
const bufferAnimation = useRef(new Animated.Value(-1_000));
|
||||
const progressAnimation = useRef(new Animated.Value(-1_000));
|
||||
|
||||
const openNowPlayingModal = useCallback(() => {
|
||||
navigation.navigate('Player');
|
||||
@@ -163,7 +167,14 @@ function NowPlaying({ offset = 0 }: { offset?: number }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ bottom: tabBarHeight + NOW_PLAYING_POPOVER_MARGIN + offset }}>
|
||||
<Container
|
||||
style={{
|
||||
bottom: (tabBarHeight || 0)
|
||||
+ (inset ? insets.bottom : 0)
|
||||
+ NOW_PLAYING_POPOVER_MARGIN
|
||||
+ offset
|
||||
}}
|
||||
>
|
||||
{/** TODO: Fix shadow overflow on Android */}
|
||||
{Platform.OS === 'ios' ? (
|
||||
<ShadowOverlay pointerEvents='none'>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { t } from '@/localisation';
|
||||
import { NavigationProp, StackParams } from '@/screens/types';
|
||||
import { SubHeader, Text } from '@/components/Typography';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import styled from 'styled-components';
|
||||
import { Dimensions, Pressable } from 'react-native';
|
||||
import AlbumImage from './components/AlbumImage';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { SectionList, View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, ReactText } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { View } from 'react-native';
|
||||
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { SectionList, View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useRef, ReactText } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { Text, View, FlatList, ListRenderItem, RefreshControl } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { Text, SafeAreaView, StyleSheet } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
@@ -32,6 +32,7 @@ const HeaderContainer = styled.View`
|
||||
`;
|
||||
|
||||
const NavigationHeader: React.FC = () => {
|
||||
const type = useTypedSelector((state) => state.settings.credentials?.type);
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
|
||||
const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]);
|
||||
@@ -39,9 +40,17 @@ const NavigationHeader: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListButton onPress={handleAllAlbumsClick} testID="all-albums">{t('all-albums')}</ListButton>
|
||||
<ListButton onPress={handleArtistsClick} testID="artists">{t('artists')}</ListButton>
|
||||
<ListButton onPress={handlePlaylistsClick} testID="playlists">{t('playlists')}</ListButton>
|
||||
<ListButton onPress={handleAllAlbumsClick} testID="all-albums">
|
||||
{t('all-albums')}
|
||||
</ListButton>
|
||||
<ListButton onPress={handleArtistsClick} testID="artists">
|
||||
{t('artists')}
|
||||
</ListButton>
|
||||
{type === 'jellyfin' && (
|
||||
<ListButton onPress={handlePlaylistsClick} testID="playlists">
|
||||
{t('playlists')}
|
||||
</ListButton>
|
||||
)}
|
||||
<ListContainer>
|
||||
<HeaderContainer>
|
||||
<Header>{t('recent-albums')}</Header>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import FastImage, { FastImageProps } from 'react-native-fast-image';
|
||||
import { Dimensions, useColorScheme } from 'react-native';
|
||||
import { Dimensions } from 'react-native';
|
||||
import { useUserOrSystemScheme } from '@/components/Colors';
|
||||
|
||||
const Screen = Dimensions.get('screen');
|
||||
export const AlbumWidth = Screen.width / 2 - 24;
|
||||
@@ -23,11 +24,17 @@ const Container = styled(FastImage)`
|
||||
|
||||
function AlbumImage(props: FastImageProps) {
|
||||
const [hasError, setError] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const colorScheme = useUserOrSystemScheme();
|
||||
|
||||
if (!props.source || hasError) {
|
||||
return (
|
||||
<Container {...props} source={colorScheme === 'light' ? require('@/assets/images/empty-album-light.png') : require('@/assets/images/empty-album-dark.png')} />
|
||||
<Container
|
||||
{...props}
|
||||
source={colorScheme === 'light'
|
||||
? require('@/assets/images/empty-album-light.png')
|
||||
: require('@/assets/images/empty-album-dark.png')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { PropsWithChildren, useCallback, useMemo } from 'react';
|
||||
import { Platform, RefreshControl, StyleSheet, View } from 'react-native';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import TouchableHandler from '@/components/TouchableHandler';
|
||||
import useCurrentTrack from '@/utility/useCurrentTrack';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import Play from '@/assets/icons/play.svg';
|
||||
import Shuffle from '@/assets/icons/shuffle.svg';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
@@ -25,14 +24,23 @@ import CoverImage from '@/components/CoverImage';
|
||||
import ticksToDuration from '@/utility/ticksToDuration';
|
||||
import { t } from '@/localisation';
|
||||
import { SafeScrollView, useNavigationOffsets } from '@/components/SafeNavigatorView';
|
||||
import { groupBy } from 'lodash';
|
||||
import Divider from '@/components/Divider';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
index: {
|
||||
marginRight: 12
|
||||
marginRight: 12,
|
||||
textAlign: 'right',
|
||||
},
|
||||
activeText: {
|
||||
fontWeight: '500',
|
||||
},
|
||||
discContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 24,
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
}
|
||||
});
|
||||
|
||||
const AlbumImageContainer = styled.View`
|
||||
@@ -53,7 +61,7 @@ const TrackContainer = styled.View<{ isPlaying: boolean, small?: boolean }>`
|
||||
`}
|
||||
|
||||
${props => props.small && css`
|
||||
padding: ${Platform.select({ ios: '8px 4px', android: '4px'})};
|
||||
padding: ${Platform.select({ ios: '8px 4px', android: '4px' })};
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -98,6 +106,18 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
), 0)
|
||||
), [trackIds, tracks]);
|
||||
|
||||
// Split all tracks into trackgroups depending on their parent id (i.e. disc
|
||||
// number).
|
||||
const trackGroups: [string, string[]][] = useMemo(() => {
|
||||
// GUARD: Only apply this rendering style for albums
|
||||
if (listNumberingStyle !== 'album') {
|
||||
return [['0', trackIds]];
|
||||
}
|
||||
|
||||
const groups = groupBy(trackIds, (id) => tracks[id]?.ParentIndexNumber);
|
||||
return Object.entries(groups);
|
||||
}, [trackIds, tracks, listNumberingStyle]);
|
||||
|
||||
// Retrieve helpers
|
||||
const getImage = useGetImage();
|
||||
const playTracks = usePlayTracks();
|
||||
@@ -105,16 +125,34 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Visual helpers
|
||||
const { indexWidth } = useMemo(() => {
|
||||
// Retrieve the largest index in the current set of tracks
|
||||
const largestIndex = trackIds.reduce((max, trackId, i) => {
|
||||
// Retrieve the index for this trackid, depending on settings
|
||||
const index = listNumberingStyle === 'index'
|
||||
? i + 1
|
||||
: tracks[trackId]?.IndexNumber;
|
||||
|
||||
// Check that the current index is larger than the current max.
|
||||
return index > max ? index : max;
|
||||
}, 0);
|
||||
|
||||
// Retrieve the number of digits in the largest index
|
||||
const noDigits = largestIndex.toFixed(0).toString().length;
|
||||
|
||||
// Set a minWidth proportional to the largest amount of digits in an index
|
||||
return StyleSheet.create({ indexWidth: { minWidth: noDigits * 8 } });
|
||||
}, [trackIds, tracks, listNumberingStyle]);
|
||||
|
||||
// Setup callbacks
|
||||
const playEntity = useCallback(() => { playTracks(trackIds); }, [playTracks, trackIds]);
|
||||
const shuffleEntity = useCallback(() => { playTracks(trackIds, { shuffle: true }); }, [playTracks, trackIds]);
|
||||
const selectTrack = useCallback(async (index: number) => {
|
||||
await playTracks(trackIds, { play: false });
|
||||
await TrackPlayer.skip(index);
|
||||
await TrackPlayer.play();
|
||||
await playTracks(trackIds, { playIndex: index });
|
||||
}, [playTracks, trackIds]);
|
||||
const longPressTrack = useCallback((index: number) => {
|
||||
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() });
|
||||
const longPressTrack = useCallback((index: number) => {
|
||||
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() });
|
||||
}, [navigation, trackIds]);
|
||||
const downloadAllTracks = useCallback(() => {
|
||||
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId)));
|
||||
@@ -141,85 +179,96 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
<WrappableButton title={shuffleButtonText} icon={Shuffle} onPress={shuffleEntity} testID="shuffle-album" />
|
||||
</WrappableButtonRow>
|
||||
<View style={{ marginTop: 8 }}>
|
||||
{trackIds.map((trackId, i) =>
|
||||
<TouchableHandler
|
||||
key={trackId}
|
||||
id={i}
|
||||
onPress={selectTrack}
|
||||
onLongPress={longPressTrack}
|
||||
testID={`play-track-${trackId}`}
|
||||
>
|
||||
<TrackContainer
|
||||
isPlaying={currentTrack?.backendId === trackId || false}
|
||||
style={[
|
||||
defaultStyles.border,
|
||||
currentTrack?.backendId === trackId ? defaultStyles.activeBackground : null
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.index,
|
||||
defaultStyles.textQuarterOpacity,
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
{trackGroups.map(([discNo, groupTrackIds]) => (
|
||||
<View key={`disc_${discNo}`} style={{ marginBottom: 24 }}>
|
||||
{trackGroups.length > 1 && (
|
||||
<View style={styles.discContainer}>
|
||||
<SubHeader>{t('disc')} {discNo}</SubHeader>
|
||||
<Divider />
|
||||
</View>
|
||||
)}
|
||||
{groupTrackIds.map((trackId, i) =>
|
||||
<TouchableHandler
|
||||
key={trackId}
|
||||
id={i}
|
||||
onPress={selectTrack}
|
||||
onLongPress={longPressTrack}
|
||||
testID={`play-track-${trackId}`}
|
||||
>
|
||||
{listNumberingStyle === 'index'
|
||||
? i + 1
|
||||
: tracks[trackId]?.IndexNumber}
|
||||
</Text>
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Text
|
||||
<TrackContainer
|
||||
isPlaying={currentTrack?.backendId === trackId || false}
|
||||
style={[
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColor,
|
||||
{
|
||||
flexShrink: 1,
|
||||
marginRight: 4,
|
||||
}
|
||||
defaultStyles.border,
|
||||
currentTrack?.backendId === trackId ? defaultStyles.activeBackground : null
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tracks[trackId]?.Name}
|
||||
</Text>
|
||||
{itemDisplayStyle === 'playlist' && (
|
||||
<Text
|
||||
style={[
|
||||
styles.index,
|
||||
defaultStyles.textQuarterOpacity,
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColor,
|
||||
{
|
||||
flexShrink: 1,
|
||||
marginRight: 4,
|
||||
opacity: currentTrack?.backendId === trackId ? 0.5 : 0.25,
|
||||
}
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
|
||||
indexWidth,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tracks[trackId]?.Artists.join(', ')}
|
||||
{listNumberingStyle === 'index'
|
||||
? i + 1
|
||||
: tracks[trackId]?.IndexNumber}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}>
|
||||
<Text
|
||||
style={[
|
||||
{ marginRight: 12 },
|
||||
defaultStyles.textQuarterOpacity,
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)}
|
||||
</Text>
|
||||
<DownloadIcon
|
||||
trackId={trackId}
|
||||
fill={currentTrack?.backendId === trackId ? defaultStyles.themeColorQuarterOpacity.color : undefined}
|
||||
/>
|
||||
</View>
|
||||
</TrackContainer>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
<View style={{ flexShrink: 1 }}>
|
||||
<Text
|
||||
style={[
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColor,
|
||||
{
|
||||
flexShrink: 1,
|
||||
marginRight: 4,
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tracks[trackId]?.Name}
|
||||
</Text>
|
||||
{itemDisplayStyle === 'playlist' && (
|
||||
<Text
|
||||
style={[
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColor,
|
||||
{
|
||||
flexShrink: 1,
|
||||
marginRight: 4,
|
||||
opacity: currentTrack?.backendId === trackId ? 0.5 : 0.25,
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tracks[trackId]?.Artists.join(', ')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}>
|
||||
<Text
|
||||
style={[
|
||||
{ marginRight: 12 },
|
||||
defaultStyles.textQuarterOpacity,
|
||||
currentTrack?.backendId === trackId && styles.activeText,
|
||||
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)}
|
||||
</Text>
|
||||
<DownloadIcon
|
||||
trackId={trackId}
|
||||
fill={currentTrack?.backendId === trackId ? defaultStyles.themeColorQuarterOpacity.color : undefined}
|
||||
/>
|
||||
</View>
|
||||
</TrackContainer>
|
||||
</TouchableHandler>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
<Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}>
|
||||
{t('total-duration')}{': '}{ticksToDuration(totalDuration)}
|
||||
</Text>
|
||||
|
||||
@@ -38,7 +38,7 @@ const Logo = styled.Image`
|
||||
|
||||
function Onboarding() {
|
||||
// Get account from Redux and dispatcher
|
||||
const account = useTypedSelector(state => state.settings.jellyfin);
|
||||
const account = useTypedSelector(state => state.settings.credentials);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Also retrieve the navigation handler so that we can open the modal in
|
||||
@@ -71,7 +71,7 @@ function Onboarding() {
|
||||
</Text>
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
title={t('set-jellyfin-server')}
|
||||
title={t('set-server')}
|
||||
onPress={handleClick}/>
|
||||
</ButtonContainer>
|
||||
</TextContainer>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Album, AlbumTrack } from '@/store/music/types';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import TouchableHandler from '@/components/TouchableHandler';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { t } from '@/localisation';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
|
||||
@@ -27,7 +27,7 @@ export function SettingsList() {
|
||||
|
||||
return (
|
||||
<SafeScrollView>
|
||||
<ListButton onPress={handleLibraryClick}>{t('jellyfin-library')}</ListButton>
|
||||
<ListButton onPress={handleLibraryClick}>{t('library')}</ListButton>
|
||||
<ListButton onPress={handleCacheClick}>{t('setting-cache')}</ListButton>
|
||||
<ListButton onPress={handleSentryClick}>{t('error-reporting')}</ListButton>
|
||||
<ListButton onPress={handlePlaybackReportingClick}>{t('playback-reporting')}</ListButton>
|
||||
@@ -50,7 +50,7 @@ export default function Settings() {
|
||||
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
|
||||
}}>
|
||||
<Stack.Screen name="SettingList" component={SettingsList} options={{ headerTitle: t('settings') }} />
|
||||
<Stack.Screen name="Library" component={Library} options={{ headerTitle: t('jellyfin-library') }} />
|
||||
<Stack.Screen name="Library" component={Library} options={{ headerTitle: t('library') }} />
|
||||
<Stack.Screen name="Cache" component={Cache} options={{ headerTitle: t('setting-cache') }} />
|
||||
<Stack.Screen name="Sentry" component={Sentry} options={{ headerTitle: t('error-reporting') }} />
|
||||
<Stack.Screen name="Playback Reporting" component={PlaybackReporting} options={{ headerTitle: t('playback-reporting')}} />
|
||||
|
||||
@@ -11,25 +11,25 @@ import { InputContainer, Input } from '../components/Input';
|
||||
|
||||
export default function LibrarySettings() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const { jellyfin } = useTypedSelector(state => state.settings);
|
||||
const { credentials } = useTypedSelector(state => state.settings);
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const handleSetLibrary = useCallback(() => navigation.navigate('SetJellyfinServer'), [navigation]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<InputContainer>
|
||||
<Paragraph style={defaultStyles.text}>{t('jellyfin-server-url')}</Paragraph>
|
||||
<Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={defaultStyles.input} />
|
||||
<Paragraph style={defaultStyles.text}>{t('server-url')}</Paragraph>
|
||||
<Input placeholder="https://jellyfin.yourserver.com/" value={credentials?.uri} editable={false} style={defaultStyles.input} />
|
||||
</InputContainer>
|
||||
<InputContainer>
|
||||
<Paragraph style={defaultStyles.text}>{t('jellyfin-access-token')}</Paragraph>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={defaultStyles.input} />
|
||||
<Paragraph style={defaultStyles.text}>{t('access-token')}</Paragraph>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={credentials?.access_token} editable={false} style={defaultStyles.input} />
|
||||
</InputContainer>
|
||||
<InputContainer>
|
||||
<Paragraph style={defaultStyles.text}>{t('jellyfin-user-id')}</Paragraph>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={defaultStyles.input} />
|
||||
<Paragraph style={defaultStyles.text}>{t('user-id')}</Paragraph>
|
||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={credentials?.user_id} editable={false} style={defaultStyles.input} />
|
||||
</InputContainer>
|
||||
<Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} />
|
||||
<Button title={t('set-server')} onPress={handleSetLibrary} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
@@ -11,6 +12,7 @@ import Downloads from './Downloads';
|
||||
import Onboarding from './Onboarding';
|
||||
import TrackPopupMenu from './modals/TrackPopupMenu';
|
||||
import SetJellyfinServer from './modals/SetJellyfinServer';
|
||||
import ErrorReportingPopup from './modals/ErrorReportingPopup';
|
||||
|
||||
import SearchIcon from '@/assets/icons/magnifying-glass.svg';
|
||||
import NotesIcon from '@/assets/icons/notes.svg';
|
||||
@@ -19,11 +21,10 @@ import DownloadsIcon from '@/assets/icons/arrow-down-to-line.svg';
|
||||
import { useTypedSelector } from '@/store';
|
||||
import { t } from '@/localisation';
|
||||
import ErrorReportingAlert from '@/utility/ErrorReportingAlert';
|
||||
import ErrorReportingPopup from './modals/ErrorReportingPopup';
|
||||
import Player from './modals/Player';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import Player from './modals/Player';
|
||||
import { StackParams } from './types';
|
||||
import Lyrics from './modals/Lyrics';
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParams>();
|
||||
const Tab = createBottomTabNavigator();
|
||||
@@ -36,7 +37,7 @@ type Screens = {
|
||||
function Screens() {
|
||||
const styles = useDefaultStyles();
|
||||
const isOnboardingComplete = useTypedSelector(state => state.settings.isOnboardingComplete);
|
||||
|
||||
|
||||
// GUARD: If onboarding has not been completed, we instead render the
|
||||
// onboarding component, so that the user can get setup in the app.
|
||||
if (!isOnboardingComplete) {
|
||||
@@ -91,12 +92,16 @@ export default function Routes() {
|
||||
<Stack.Navigator screenOptions={{
|
||||
presentation: 'modal',
|
||||
headerShown: false,
|
||||
contentStyle: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}} id="MAIN">
|
||||
<Stack.Screen name="Screens" component={Screens} />
|
||||
<Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} />
|
||||
<Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet' }} />
|
||||
<Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} />
|
||||
<Stack.Screen name="Player" component={Player} />
|
||||
<Stack.Screen name="Lyrics" component={Lyrics} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -104,4 +109,4 @@ export default function Routes() {
|
||||
export type NavigationProp = CompositeNavigationProp<
|
||||
StackNavigationProp<Routes>,
|
||||
BottomTabNavigationProp<Screens>
|
||||
>;
|
||||
>;
|
||||
|
||||
72
src/screens/modals/Lyrics/components/LyricsLine.tsx
Normal file
72
src/screens/modals/Lyrics/components/LyricsLine.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React, { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import {LayoutChangeEvent, StyleProp, TextStyle, ViewProps} from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import Animated, { useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated';
|
||||
|
||||
const Container = styled(Animated.View)`
|
||||
|
||||
`;
|
||||
|
||||
const LyricsText = styled(Animated.Text)`
|
||||
flex: 1;
|
||||
font-size: 24px;
|
||||
`;
|
||||
|
||||
export interface LyricsLineProps extends Omit<ViewProps, 'onLayout'> {
|
||||
text?: string;
|
||||
start: number;
|
||||
end: number;
|
||||
position: number;
|
||||
index: number;
|
||||
onActive: (index: number) => void;
|
||||
onLayout: (index: number, event: LayoutChangeEvent) => void;
|
||||
size: 'small' | 'full';
|
||||
}
|
||||
|
||||
/**
|
||||
* A single lyric line
|
||||
*/
|
||||
function LyricsLine({
|
||||
text, start, end, position, size, onLayout, onActive, index, ...viewProps
|
||||
}: LyricsLineProps) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Pass on layout changes to the parent
|
||||
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
onLayout?.(index, e);
|
||||
}, [onLayout, index]);
|
||||
|
||||
// Determine whether the loader should be displayed
|
||||
const active = useMemo(() => (
|
||||
position > start && position < end
|
||||
), [start, end, position]);
|
||||
|
||||
// Call the parent when the active state changes
|
||||
useEffect(() => {
|
||||
if (active) onActive(index);
|
||||
}, [onActive, active, index]);
|
||||
|
||||
// Determine the current style for this line
|
||||
const lyricsTextStyle: StyleProp<TextStyle> = useMemo(() => ({
|
||||
color: active ? defaultStyles.themeColor.color : defaultStyles.text.color,
|
||||
opacity: active ? 1 : 0.7,
|
||||
transformOrigin: 'left center',
|
||||
fontSize: size === 'full' ? 24 : 18,
|
||||
}), [active, defaultStyles, size]);
|
||||
|
||||
const scale = useDerivedValue(() => withTiming(active ? 1.05 : 1));
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Container {...viewProps} onLayout={handleLayout} >
|
||||
<LyricsText style={[lyricsTextStyle, animatedStyle]}>
|
||||
{text}
|
||||
</LyricsText>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LyricsLine);
|
||||
88
src/screens/modals/Lyrics/components/LyricsProgress.tsx
Normal file
88
src/screens/modals/Lyrics/components/LyricsProgress.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import ProgressTrack, { calculateProgressTranslation, ProgressTrackContainer } from '@/components/Progresstrack';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { LayoutChangeEvent } from 'react-native';
|
||||
import { useDerivedValue, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
import { ViewProps } from 'react-native-svg/lib/typescript/fabric/utils';
|
||||
|
||||
export interface LyricsProgressProps extends Omit<ViewProps, 'onLayout'> {
|
||||
start: number;
|
||||
end: number;
|
||||
position: number;
|
||||
index: number;
|
||||
onActive: (index: number) => void;
|
||||
onLayout: (index: number, event: LayoutChangeEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a loading bar when there is a silence in the lyrics.
|
||||
*/
|
||||
export default function LyricsProgress({
|
||||
start, end, position, index, onLayout, onActive, style, ...props
|
||||
}: LyricsProgressProps) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
// Keep a reference to the width of the container
|
||||
const width = useSharedValue(0);
|
||||
|
||||
// Pass on layout changes to the parent
|
||||
const handleLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
onLayout?.(index, e);
|
||||
width.value = e.nativeEvent.layout.width;
|
||||
}, [onLayout, index, width]);
|
||||
|
||||
// Determine whether the loader should be displayed
|
||||
const active = useMemo(() => (
|
||||
position > start && position < end
|
||||
), [start, end, position]);
|
||||
|
||||
// Call the parent when the active state changes
|
||||
useEffect(() => {
|
||||
if (active) onActive(index);
|
||||
}, [onActive, active, index]);
|
||||
|
||||
// Determine the duration of the progress bar
|
||||
const duration = useMemo(() => (end - start), [end, start]);
|
||||
|
||||
// Calculate the progress animation
|
||||
const progressAnimation = useDerivedValue(() => {
|
||||
// GUARD: If the animatino is not active, hide the progress bar
|
||||
if (!active) return -1_000;
|
||||
|
||||
// Calculate how far along we are
|
||||
const progress = calculateProgressTranslation(position - start, end - start, width.value);
|
||||
|
||||
// Move to that position with easing
|
||||
return withTiming(progress, { duration: 200 });
|
||||
});
|
||||
|
||||
// Calculate the styles according to the progress
|
||||
const progressStyles = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{ translateX: progressAnimation.value }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
console.log(progressAnimation.value);
|
||||
|
||||
// GUARD: Only show durations if they last for more than 5 seconds.
|
||||
if (duration < 5e7) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProgressTrackContainer
|
||||
{...props}
|
||||
style={[
|
||||
defaultStyles.trackBackground,
|
||||
{ flexGrow: 0, marginVertical: 8 },
|
||||
style
|
||||
]}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
<ProgressTrack style={[progressStyles, defaultStyles.themeBackground]} />
|
||||
</ProgressTrackContainer>
|
||||
);
|
||||
}
|
||||
146
src/screens/modals/Lyrics/components/LyricsRenderer.tsx
Normal file
146
src/screens/modals/Lyrics/components/LyricsRenderer.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import { LayoutChangeEvent, LayoutRectangle, StyleSheet, View } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import { Lyrics } from '@/utility/JellyfinApi/lyrics';
|
||||
import { useProgress } from 'react-native-track-player';
|
||||
import useCurrentTrack from '@/utility/useCurrentTrack';
|
||||
import LyricsLine from './LyricsLine';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useTypedSelector } from '@/store';
|
||||
import { NOW_PLAYING_POPOVER_HEIGHT } from '@/screens/Music/overlays/NowPlaying';
|
||||
import LyricsProgress, { LyricsProgressProps } from './LyricsProgress';
|
||||
|
||||
type LyricsLine = Lyrics['Lyrics'][number];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
lyricsContainerFull: {
|
||||
padding: 40,
|
||||
paddingBottom: 40 + NOW_PLAYING_POPOVER_HEIGHT,
|
||||
gap: 12,
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
lyricsContainerSmall: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 80,
|
||||
gap: 8,
|
||||
},
|
||||
containerSmall: {
|
||||
maxHeight: 160,
|
||||
flex: 1,
|
||||
}
|
||||
});
|
||||
|
||||
// Always hit the changes this amount of microseconds early so that it appears
|
||||
// to follow the track a bit more accurate.
|
||||
const TIME_OFFSET = 2e6;
|
||||
|
||||
export interface LyricsRendererProps {
|
||||
size?: 'small' | 'full',
|
||||
}
|
||||
|
||||
export default function LyricsRenderer({ size = 'full' }: LyricsRendererProps) {
|
||||
const scrollViewRef = useRef<Animated.ScrollView>(null);
|
||||
const lineLayoutsRef = useRef(new Map<number, LayoutRectangle>());
|
||||
const { position } = useProgress(100);
|
||||
const { track: trackPlayerTrack } = useCurrentTrack();
|
||||
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
||||
const track = useMemo(() => tracks[trackPlayerTrack?.backendId], [trackPlayerTrack?.backendId, tracks]);
|
||||
const navigation = useNavigation();
|
||||
|
||||
// We will be using isUserScrolling to prevent lyrics controller scroll lyrics view
|
||||
// while user is scrolling
|
||||
const isUserScrolling = useRef(false);
|
||||
|
||||
// We will be using containerHeight to make sure active lyrics line is in the center
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
|
||||
// Calculate current ime
|
||||
const currentTime = useMemo(() => {
|
||||
return position * 10_000_000;
|
||||
}, [position]);
|
||||
|
||||
// Handler for saving line positions
|
||||
const handleLayoutChange = useCallback((index: number, event: LayoutChangeEvent) => {
|
||||
lineLayoutsRef.current.set(index, event.nativeEvent.layout);
|
||||
}, []);
|
||||
|
||||
const handleActive = useCallback((index: number) => {
|
||||
const lineLayout = lineLayoutsRef.current.get(index);
|
||||
if (!containerHeight || isUserScrolling.current || !lineLayout) return;
|
||||
|
||||
scrollViewRef.current?.scrollTo({
|
||||
y: lineLayout.y - containerHeight / 2 + lineLayout.height / 2,
|
||||
animated: true,
|
||||
});
|
||||
}, [containerHeight, isUserScrolling]);
|
||||
|
||||
// Calculate current container height
|
||||
const handleContainerLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
setContainerHeight(event.nativeEvent.layout.height);
|
||||
}, []);
|
||||
|
||||
// Handlers for user scroll handling
|
||||
const handleScrollBeginDrag = useCallback(() => isUserScrolling.current = true, []);
|
||||
const handleScrollEndDrag = useCallback(() => isUserScrolling.current = false, []);
|
||||
|
||||
if (!track) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// GUARD: If the track has no lyrics, close the modal
|
||||
if (!track.HasLyrics || !track.Lyrics) {
|
||||
navigation.goBack();
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={size === 'small' && styles.containerSmall}>
|
||||
<Animated.ScrollView
|
||||
contentContainerStyle={size === 'full'
|
||||
? styles.lyricsContainerFull
|
||||
: styles.lyricsContainerSmall
|
||||
}
|
||||
ref={scrollViewRef}
|
||||
onLayout={handleContainerLayout}
|
||||
onScrollBeginDrag={handleScrollBeginDrag}
|
||||
onScrollEndDrag={handleScrollEndDrag}
|
||||
>
|
||||
<LyricsProgress
|
||||
start={0}
|
||||
end={track.Lyrics.Lyrics[0].Start - TIME_OFFSET}
|
||||
position={currentTime}
|
||||
index={-1}
|
||||
onActive={handleActive}
|
||||
onLayout={handleLayoutChange}
|
||||
/>
|
||||
{track.Lyrics.Lyrics.map((lyrics, i) => {
|
||||
const props: LyricsProgressProps = {
|
||||
start: lyrics.Start - TIME_OFFSET,
|
||||
end: track.Lyrics!.Lyrics.length === i + 1
|
||||
? track.RunTimeTicks
|
||||
: track.Lyrics!.Lyrics[i + 1]?.Start - TIME_OFFSET
|
||||
,
|
||||
position: currentTime,
|
||||
onLayout: handleLayoutChange,
|
||||
onActive: handleActive,
|
||||
index: i,
|
||||
};
|
||||
|
||||
return lyrics.Text ? (
|
||||
<LyricsLine
|
||||
key={`lyric_${i}`}
|
||||
{...props}
|
||||
text={lyrics.Text}
|
||||
size={size}
|
||||
/>
|
||||
) : (
|
||||
<LyricsProgress
|
||||
key={`lyric_${i}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Animated.ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
19
src/screens/modals/Lyrics/index.tsx
Normal file
19
src/screens/modals/Lyrics/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import LyricsRenderer from './components/LyricsRenderer';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { Platform } from 'react-native';
|
||||
import BackButton from '../Player/components/Backbutton';
|
||||
import { ColoredBlurView } from '@/components/Colors';
|
||||
import NowPlaying from '@/screens/Music/overlays/NowPlaying';
|
||||
|
||||
export default function Lyrics() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ColoredBlurView style={{ flex: 1 }}>
|
||||
{Platform.OS === 'android' && (<BackButton />)}
|
||||
<LyricsRenderer />
|
||||
<NowPlaying inset />
|
||||
</ColoredBlurView>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
127
src/screens/modals/Player/components/LyricsPreview.tsx
Normal file
127
src/screens/modals/Player/components/LyricsPreview.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import useCurrentTrack from '@/utility/useCurrentTrack';
|
||||
import styled from 'styled-components/native';
|
||||
import LyricsIcon from '@/assets/icons/lyrics.svg';
|
||||
import { t } from '@/localisation';
|
||||
import LyricsRenderer from '../../Lyrics/components/LyricsRenderer';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { LayoutChangeEvent } from 'react-native';
|
||||
import { Defs, LinearGradient, Rect, Stop, Svg } from 'react-native-svg';
|
||||
|
||||
const Container = styled.TouchableOpacity`
|
||||
border-radius: 8px;
|
||||
margin-top: 24px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Header = styled.View`
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
z-index: 3;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const HeaderInnerContainer = styled(ColoredBlurView)`
|
||||
padding: 8px;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const Label = styled.Text`
|
||||
|
||||
`;
|
||||
|
||||
const HeaderBackground = styled.View`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 60px;
|
||||
z-index: 2;
|
||||
background-color: transparent;
|
||||
`;
|
||||
|
||||
function InnerLyricsPreview() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
const handleLayoutChange = useCallback((e: LayoutChangeEvent) => {
|
||||
setWidth(e.nativeEvent.layout.width);
|
||||
}, []);
|
||||
|
||||
const handleShowLyrics = useCallback(() => {
|
||||
navigation.navigate('Lyrics');
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<Container
|
||||
style={defaultStyles.trackBackground}
|
||||
onPress={handleShowLyrics}
|
||||
onLayout={handleLayoutChange}
|
||||
>
|
||||
<Header style={defaultStyles.activeBackground}>
|
||||
<HeaderInnerContainer>
|
||||
<LyricsIcon fill={defaultStyles.themeColor.color} />
|
||||
<Label style={defaultStyles.themeColor}>
|
||||
{t('lyrics')}
|
||||
</Label>
|
||||
</HeaderInnerContainer>
|
||||
</Header>
|
||||
<HeaderBackground>
|
||||
<Svg width={width} height={60} viewBox={`0 0 ${width} 60`}>
|
||||
<Defs>
|
||||
<LinearGradient
|
||||
id="lyrics-label-gradient"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<Stop
|
||||
offset="0"
|
||||
stopColor={defaultStyles.trackBackground.backgroundColor}
|
||||
stopOpacity={1}
|
||||
/>
|
||||
<Stop
|
||||
offset="0.75"
|
||||
stopColor={defaultStyles.trackBackground.backgroundColor}
|
||||
stopOpacity={0.7}
|
||||
/>
|
||||
<Stop
|
||||
offset="1"
|
||||
stopColor={defaultStyles.trackBackground.backgroundColor}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</LinearGradient>
|
||||
</Defs>
|
||||
<Rect x={0} y={0} height={60} width={width} fill="url(#lyrics-label-gradient)" />
|
||||
</Svg>
|
||||
</HeaderBackground>
|
||||
<LyricsRenderer size="small" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper for LyricsPreview, so we only render the component if the current
|
||||
* track has lyrics.
|
||||
*/
|
||||
export default function LyricsPreview() {
|
||||
const { albumTrack } = useCurrentTrack();
|
||||
|
||||
if (!albumTrack?.HasLyrics) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InnerLyricsPreview />
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import TrackPlayer, { State, usePlaybackState } from 'react-native-track-player';
|
||||
import { TouchableOpacity, useColorScheme } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { useHasNextQueue, useHasPreviousQueue } from '@/utility/useQueue';
|
||||
import ForwardIcon from '@/assets/icons/forward-end.svg';
|
||||
import BackwardIcon from '@/assets/icons/backward-end.svg';
|
||||
import PlayIcon from '@/assets/icons/play.svg';
|
||||
import PauseIcon from '@/assets/icons/pause.svg';
|
||||
import { useUserOrSystemScheme } from '@/components/Colors';
|
||||
|
||||
const BUTTON_SIZE = 40;
|
||||
|
||||
@@ -18,6 +19,7 @@ const previous = () => TrackPlayer.skipToPrevious();
|
||||
const Container = styled.View`
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 52px;
|
||||
`;
|
||||
|
||||
const Buttons = styled.View`
|
||||
@@ -33,7 +35,7 @@ const Button = styled.View`
|
||||
`;
|
||||
|
||||
export default function MediaControls() {
|
||||
const scheme = useColorScheme();
|
||||
const scheme = useUserOrSystemScheme();
|
||||
const fill = scheme === 'dark' ? '#ffffff' : '#000000';
|
||||
|
||||
return (
|
||||
|
||||
79
src/screens/modals/Player/components/MediaInformation.tsx
Normal file
79
src/screens/modals/Player/components/MediaInformation.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Text } from '@/components/Typography';
|
||||
import useCurrentTrack from '@/utility/useCurrentTrack';
|
||||
import React from 'react-native';
|
||||
import WaveformIcon from '@/assets/icons/waveform.svg';
|
||||
import useDefaultStyles from '@/components/Colors';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import { useMemo } from 'react';
|
||||
import { t } from '@/localisation';
|
||||
|
||||
const Container = styled.View`
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const Info = styled.View`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
`;
|
||||
|
||||
const Label = styled(Text)<{ overflow?: boolean }>`
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
|
||||
${(props) => props?.overflow && css`
|
||||
flex: 0 1 auto;
|
||||
`}
|
||||
`;
|
||||
|
||||
/**
|
||||
* This component displays information about the media that is being played
|
||||
* back, such as the bitrate, sample rate, codec and whether it's transcoded.
|
||||
*/
|
||||
export default function MediaInformation() {
|
||||
const styles = useDefaultStyles();
|
||||
const { track, albumTrack } = useCurrentTrack();
|
||||
|
||||
const mediaStream = useMemo(() => (
|
||||
albumTrack?.MediaStreams?.find((d) => d.Type === 'Audio')
|
||||
), [albumTrack]);
|
||||
|
||||
if (!albumTrack || !track) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<WaveformIcon fill={styles.icon.color} height={16} width={16} />
|
||||
<Info>
|
||||
<Label numberOfLines={1} overflow>
|
||||
{track.isDirectPlay ? t('direct-play') : t('transcoded')}
|
||||
</Label>
|
||||
<Label numberOfLines={1}>
|
||||
{track.isDirectPlay
|
||||
? mediaStream?.Codec.toUpperCase()
|
||||
: track.contentType?.replace('audio/', '').toUpperCase()
|
||||
}
|
||||
</Label>
|
||||
{mediaStream && (
|
||||
<>
|
||||
<Label numberOfLines={1}>
|
||||
{((track.isDirectPlay ? mediaStream.BitRate : track.bitRate) / 1000)
|
||||
.toFixed(0)}
|
||||
{t('kbps')}
|
||||
</Label>
|
||||
<Label numberOfLines={1}>
|
||||
{(mediaStream.SampleRate / 1000).toFixed(1)}
|
||||
{t('khz')}
|
||||
</Label>
|
||||
</>
|
||||
)}
|
||||
</Info>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -187,7 +187,7 @@ function ProgressBar() {
|
||||
<ProgressTrack
|
||||
style={[
|
||||
styles.themeBackground,
|
||||
bufferStyles
|
||||
bufferStyles,
|
||||
]}
|
||||
opacity={0.15}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { t } from '@/localisation';
|
||||
const Container = styled.View`
|
||||
align-self: flex-start;
|
||||
align-items: flex-start;
|
||||
margin-top: 52px;
|
||||
padding: 8px;
|
||||
margin-left: -8px;
|
||||
flex: 0 1 auto;
|
||||
|
||||
@@ -6,24 +6,39 @@ import Queue from './components/Queue';
|
||||
import ConnectionNotice from './components/ConnectionNotice';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import StreamStatus from './components/StreamStatus';
|
||||
import { Platform } from 'react-native';
|
||||
import {Platform} from 'react-native';
|
||||
import BackButton from './components/Backbutton';
|
||||
import Timer from './components/Timer';
|
||||
import styled from 'styled-components/native';
|
||||
import { ColoredBlurView } from '@/components/Colors.tsx';
|
||||
import LyricsPreview from './components/LyricsPreview.tsx';
|
||||
import MediaInformation from './components/MediaInformation';
|
||||
|
||||
export default function Player() {
|
||||
const Group = styled.View`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export default function Player() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
{Platform.OS === 'android' && (<BackButton />)}
|
||||
<Queue header={(
|
||||
<>
|
||||
<NowPlaying />
|
||||
<ConnectionNotice />
|
||||
<StreamStatus />
|
||||
<ProgressBar />
|
||||
<MediaControls />
|
||||
<Timer />
|
||||
</>
|
||||
)} />
|
||||
<ColoredBlurView>
|
||||
{Platform.OS === 'android' && (<BackButton />)}
|
||||
<Queue header={(
|
||||
<>
|
||||
<NowPlaying />
|
||||
<ConnectionNotice />
|
||||
<StreamStatus />
|
||||
<MediaInformation />
|
||||
<ProgressBar />
|
||||
<MediaControls />
|
||||
<Group>
|
||||
<Timer />
|
||||
</Group>
|
||||
<LyricsPreview />
|
||||
</>
|
||||
)} />
|
||||
</ColoredBlurView>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,50 @@ import { AppState } from '@/store';
|
||||
|
||||
interface Props {
|
||||
serverUrl: string;
|
||||
onCredentialsRetrieved: (credentials: AppState['settings']['jellyfin']) => void;
|
||||
onCredentialsRetrieved: (credentials: AppState['settings']['credentials']) => void;
|
||||
}
|
||||
|
||||
type CredentialEventData = {
|
||||
credentials: {
|
||||
Servers: {
|
||||
ManualAddress: string,
|
||||
ManualAddressOnly: boolean,
|
||||
IsLocalServer: boolean,
|
||||
DateLastAccessed: number,
|
||||
LastConnectionMode: number,
|
||||
Type: string,
|
||||
Name: string,
|
||||
Id: string,
|
||||
UserId: string | null,
|
||||
AccessToken: string | null,
|
||||
Users: {
|
||||
UserId: string,
|
||||
AccessToken: string,
|
||||
}[]
|
||||
LocalAddress: string,
|
||||
RemoteAddress: string,
|
||||
}[]
|
||||
},
|
||||
deviceId: string,
|
||||
type: 'emby',
|
||||
} | {
|
||||
credentials: {
|
||||
Servers: {
|
||||
ManualAddress: string,
|
||||
manualAddressOnly: boolean,
|
||||
DateLastAccessed: number,
|
||||
LastConnectionMode: number,
|
||||
Name: string,
|
||||
Id: string,
|
||||
UserId: string | null,
|
||||
AccessToken: string | null,
|
||||
LocalAddress: string,
|
||||
}[]
|
||||
},
|
||||
deviceId: string,
|
||||
type: 'jellyfin',
|
||||
} | undefined;
|
||||
|
||||
class CredentialGenerator extends Component<Props> {
|
||||
ref = createRef<WebView>();
|
||||
|
||||
@@ -18,12 +59,18 @@ class CredentialGenerator extends Component<Props> {
|
||||
|
||||
checkIfCredentialsAreThere = debounce(() => {
|
||||
// Inject some javascript to check if the credentials can be extracted
|
||||
// from localstore
|
||||
// from localstore. We simultaneously attempt to extract credentials for
|
||||
// any back-end.
|
||||
this.ref.current?.injectJavaScript(`
|
||||
try {
|
||||
let credentials = JSON.parse(window.localStorage.getItem('jellyfin_credentials'));
|
||||
let deviceId = window.localStorage.getItem('_deviceId2');
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({ credentials, deviceId }))
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({ credentials, deviceId, type: 'jellyfin' }))
|
||||
} catch(e) { }; true;
|
||||
try {
|
||||
let credentials = JSON.parse(window.localStorage.getItem('servercredentials3'));
|
||||
let deviceId = window.localStorage.getItem('_deviceId2');
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({ credentials, deviceId, type: 'emby' }))
|
||||
} catch(e) { }; true;
|
||||
`);
|
||||
}, 500);
|
||||
@@ -35,36 +82,73 @@ class CredentialGenerator extends Component<Props> {
|
||||
}
|
||||
|
||||
// Parse the content
|
||||
const data = JSON.parse(event.nativeEvent.data);
|
||||
const data = JSON.parse(event.nativeEvent.data) as CredentialEventData;
|
||||
if (__DEV__) {
|
||||
console.log('Received credential event data: ', JSON.stringify(data));
|
||||
}
|
||||
|
||||
if (!data.deviceId
|
||||
|| !data.credentials?.Servers?.length
|
||||
|| !data.credentials?.Servers[0]?.UserId
|
||||
|| !data.credentials?.Servers[0]?.AccessToken) {
|
||||
// Since Jellyfin and Emby are similar, we'll attempt to extract the
|
||||
// credentials in a generic way.
|
||||
let userId: string | undefined, accessToken: string | undefined;
|
||||
|
||||
// GUARD: Attempt to extract emby format credentials
|
||||
if (data?.type === 'emby'
|
||||
&& data.credentials?.Servers?.length
|
||||
&& data.credentials?.Servers[0]?.Users?.length
|
||||
) {
|
||||
userId = data.credentials.Servers[0].Users[0].UserId;
|
||||
accessToken = data.credentials.Servers[0].Users[0].AccessToken;
|
||||
// GUARD: Attempt to extract jellyfin format credentials
|
||||
} else if (data?.type === 'jellyfin'
|
||||
&& data.credentials?.Servers?.length
|
||||
) {
|
||||
userId = data.credentials.Servers[0].UserId || undefined;
|
||||
accessToken = data.credentials.Servers[0].AccessToken || undefined;
|
||||
}
|
||||
|
||||
// We can extract the deviceId and server address in the same way for
|
||||
// both Jellyfin and Emby.
|
||||
const deviceId = data?.deviceId;
|
||||
const address = data?.credentials?.Servers?.length
|
||||
&& data?.credentials.Servers[0].ManualAddress;
|
||||
|
||||
// GUARD: log extract credentials in dev
|
||||
if (__DEV__) {
|
||||
console.log('Extracted the following credentials:', { userId, accessToken, deviceId, address });
|
||||
}
|
||||
|
||||
// GUARD: Check that all the required credentials are available
|
||||
if (!userId || !accessToken || !deviceId || !address) {
|
||||
if (__DEV__) {
|
||||
console.error('Failed to extract credentials from event');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { credentials: { Servers: [ credentials ] }, deviceId } = data;
|
||||
|
||||
// Attempt to perform a request using the credentials to see if they're
|
||||
// good
|
||||
const response = await fetch(`${credentials.ManualAddress}/Users/Me`, {
|
||||
const response = await fetch(`${address}/Users/${userId}`, {
|
||||
headers: {
|
||||
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${credentials.AccessToken}"`
|
||||
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${accessToken}"`
|
||||
}
|
||||
});
|
||||
|
||||
// GUARD: The request must succeed
|
||||
if (response.status !== 200) {
|
||||
if (__DEV__) {
|
||||
const body = await response.text();
|
||||
console.error('Failed to retrieve user object using credentials:', response.status, body);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If a message is received, the credentials should be there
|
||||
this.props.onCredentialsRetrieved({
|
||||
uri: credentials.ManualAddress,
|
||||
user_id: credentials.UserId,
|
||||
access_token: credentials.AccessToken,
|
||||
uri: address,
|
||||
user_id: userId,
|
||||
access_token: accessToken,
|
||||
device_id: deviceId,
|
||||
type: data.type,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function SetJellyfinServer() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
// Save creedentials to store and close the modal
|
||||
const saveCredentials = useCallback((credentials: AppState['settings']['jellyfin']) => {
|
||||
const saveCredentials = useCallback((credentials: AppState['settings']['credentials']) => {
|
||||
if (credentials) {
|
||||
dispatch(setJellyfinCredentials(credentials));
|
||||
navigation.dispatch(StackActions.popToTop());
|
||||
@@ -39,7 +39,7 @@ export default function SetJellyfinServer() {
|
||||
) : (
|
||||
<View style={{ padding: 20, flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text>
|
||||
{t('set-jellyfin-server-instruction')}
|
||||
{t('set-server-instruction')}
|
||||
</Text>
|
||||
<Input
|
||||
placeholder="https://jellyfin.yourserver.io/"
|
||||
@@ -51,7 +51,7 @@ export default function SetJellyfinServer() {
|
||||
style={[ defaultStyles.input, { width: '100%' } ]}
|
||||
/>
|
||||
<Button
|
||||
title={t('set-jellyfin-server')}
|
||||
title={t('set-server')}
|
||||
onPress={() => setIsLogginIn(true)}
|
||||
disabled={!serverUrl?.length}
|
||||
color={defaultStyles.themeColor.color}
|
||||
|
||||
@@ -15,7 +15,7 @@ import CoverImage from '@/components/CoverImage';
|
||||
import { queueTrackForDownload, removeDownloadedTrack } from '@/store/downloads/actions';
|
||||
import usePlayTracks from '@/utility/usePlayTracks';
|
||||
import { selectIsDownloaded } from '@/store/downloads/selectors';
|
||||
import { useGetImage } from '@/utility/JellyfinApi';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
|
||||
type Route = RouteProp<StackParams, 'TrackPopupMenu'>;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export type StackParams = {
|
||||
Search: undefined;
|
||||
SetJellyfinServer: undefined;
|
||||
TrackPopupMenu: { trackId: string };
|
||||
Lyrics: undefined;
|
||||
};
|
||||
|
||||
export type NavigationProp = StackNavigationProp<StackParams>;
|
||||
export type NavigationProp = StackNavigationProp<StackParams>;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createAction, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { AppState } from '@/store';
|
||||
import { generateTrackUrl } from '@/utility/JellyfinApi';
|
||||
import { downloadFile, unlink, DocumentDirectoryPath, exists } from 'react-native-fs';
|
||||
import { DownloadEntity } from './types';
|
||||
import MimeTypes from '@/utility/MimeTypes';
|
||||
import { generateTrackUrl } from '@/utility/JellyfinApi/track';
|
||||
|
||||
export const downloadAdapter = createEntityAdapter<DownloadEntity>();
|
||||
|
||||
@@ -15,12 +15,9 @@ export const failDownload = createAction<{ id: string }>('download/fail');
|
||||
|
||||
export const downloadTrack = createAsyncThunk(
|
||||
'/downloads/track',
|
||||
async (id: string, { dispatch, getState }) => {
|
||||
// Get the credentials from the store
|
||||
const { settings: { jellyfin: credentials } } = (getState() as AppState);
|
||||
|
||||
async (id: string, { dispatch }) => {
|
||||
// Generate the URL we can use to download the file
|
||||
const url = generateTrackUrl(id as string, credentials);
|
||||
const url = generateTrackUrl(id);
|
||||
|
||||
// Get the content-type from the URL by doing a HEAD-only request
|
||||
const contentType = (await fetch(url, { method: 'HEAD' })).headers.get('Content-Type');
|
||||
|
||||
@@ -64,6 +64,23 @@ const persistConfig: PersistConfig<Omit<AppState, '_persist'>> = {
|
||||
}
|
||||
};
|
||||
},
|
||||
// @ts-expect-error migrations are poorly typed
|
||||
5: (state: AppState) => {
|
||||
// @ts-expect-error
|
||||
const credentials = state.settings.jellyfin && {
|
||||
// @ts-expect-error
|
||||
...(state.settings.jellyfin as AppState['settings']['credentials']),
|
||||
type: 'jellyfin',
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
credentials,
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -80,13 +97,13 @@ const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) => (
|
||||
getDefaultMiddleware({ serializableCheck: false, immutableCheck: false })
|
||||
.concat(__DEV__ ? [require('redux-flipper').default()] : [])
|
||||
),
|
||||
});
|
||||
|
||||
export type AppState = ReturnType<typeof reducers> & { _persist: PersistState };
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
|
||||
export type Store = typeof store;
|
||||
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
|
||||
import { Album, AlbumTrack, Playlist } from './types';
|
||||
import { AsyncThunkAPI } from '..';
|
||||
import { retrieveAllAlbums, retrieveAlbumTracks, retrieveRecentAlbums, searchItem, retrieveAlbum, retrieveAllPlaylists, retrievePlaylistTracks } from '@/utility/JellyfinApi';
|
||||
import { retrieveAllAlbums, retrieveRecentAlbums, retrieveAlbumTracks, retrieveAlbum } from '@/utility/JellyfinApi/album';
|
||||
import { retrieveAllPlaylists, retrievePlaylistTracks } from '@/utility/JellyfinApi/playlist';
|
||||
import { searchItem } from '@/utility/JellyfinApi/search';
|
||||
|
||||
export const albumAdapter = createEntityAdapter<Album, string>({
|
||||
selectId: album => album.Id,
|
||||
@@ -13,10 +15,7 @@ export const albumAdapter = createEntityAdapter<Album, string>({
|
||||
*/
|
||||
export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI>(
|
||||
'/albums/all',
|
||||
async (empty, thunkAPI) => {
|
||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||
return retrieveAllAlbums(credentials) as Promise<Album[]>;
|
||||
}
|
||||
retrieveAllAlbums,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -24,10 +23,7 @@ export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI
|
||||
*/
|
||||
export const fetchRecentAlbums = createAsyncThunk<Album[], number | undefined, AsyncThunkAPI>(
|
||||
'/albums/recent',
|
||||
async (numberOfAlbums, thunkAPI) => {
|
||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||
return retrieveRecentAlbums(credentials, numberOfAlbums) as Promise<Album[]>;
|
||||
}
|
||||
retrieveRecentAlbums,
|
||||
);
|
||||
|
||||
export const trackAdapter = createEntityAdapter<AlbumTrack, string>({
|
||||
@@ -40,18 +36,12 @@ export const trackAdapter = createEntityAdapter<AlbumTrack, string>({
|
||||
*/
|
||||
export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>(
|
||||
'/tracks/byAlbum',
|
||||
async (ItemId, thunkAPI) => {
|
||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||
return retrieveAlbumTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
|
||||
}
|
||||
retrieveAlbumTracks,
|
||||
);
|
||||
|
||||
export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>(
|
||||
'/albums/single',
|
||||
async (ItemId, thunkAPI) => {
|
||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||
return retrieveAlbum(credentials, ItemId) as Promise<Album>;
|
||||
}
|
||||
retrieveAlbum,
|
||||
);
|
||||
|
||||
type SearchAndFetchResults = {
|
||||
@@ -67,16 +57,17 @@ AsyncThunkAPI
|
||||
'/search',
|
||||
async ({ term, limit = 24 }, thunkAPI) => {
|
||||
const state = thunkAPI.getState();
|
||||
const results = await searchItem(state.settings.jellyfin, term, limit);
|
||||
const results = await searchItem(term, limit);
|
||||
|
||||
const albums = await Promise.all(results.filter((item) => (
|
||||
!state.music.albums.ids.includes(item.Type === 'MusicAlbum' ? item.Id : item.AlbumId)
|
||||
&& (item.Type === 'Audio' ? item.AlbumId : true)
|
||||
)).map(async (item) => {
|
||||
if (item.Type === 'MusicAlbum') {
|
||||
return item;
|
||||
}
|
||||
|
||||
return retrieveAlbum(state.settings.jellyfin, item.AlbumId);
|
||||
return retrieveAlbum(item.AlbumId);
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -96,10 +87,7 @@ export const playlistAdapter = createEntityAdapter<Playlist, string>({
|
||||
*/
|
||||
export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncThunkAPI>(
|
||||
'/playlists/all',
|
||||
async (empty, thunkAPI) => {
|
||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||
return retrieveAllPlaylists(credentials) as Promise<Playlist[]>;
|
||||
}
|
||||
retrieveAllPlaylists,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -107,8 +95,5 @@ export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncTh
|
||||
*/
|
||||
export const fetchTracksByPlaylist = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>(
|
||||
'/tracks/byPlaylist',
|
||||
async (ItemId, thunkAPI) => {
|
||||
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||
return retrievePlaylistTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
|
||||
}
|
||||
retrievePlaylistTracks,
|
||||
);
|
||||
@@ -15,8 +15,8 @@ export function useRecentAlbums(amount: number) {
|
||||
const sorted = [...albumIds].sort((a, b) => {
|
||||
const albumA = albums[a];
|
||||
const albumB = albums[b];
|
||||
const dateA = albumA ? parseISO(albumA.DateCreated).getTime() : 0;
|
||||
const dateB = albumB ? parseISO(albumB.DateCreated).getTime() : 0;
|
||||
const dateA = albumA && albumA.DateCreated ? parseISO(albumA.DateCreated).getTime() : 0;
|
||||
const dateB = albumB && albumB.DateCreated ? parseISO(albumB.DateCreated).getTime() : 0;
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import {Lyrics} from '@/utility/JellyfinApi/lyrics.ts';
|
||||
|
||||
export interface UserData {
|
||||
PlaybackPositionTicks: number;
|
||||
PlayCount: number;
|
||||
@@ -6,6 +8,29 @@ export interface UserData {
|
||||
Key: string;
|
||||
}
|
||||
|
||||
export interface MediaStream {
|
||||
Codec: string
|
||||
TimeBase: string
|
||||
VideoRange: string
|
||||
VideoRangeType: string
|
||||
AudioSpatialFormat: string
|
||||
DisplayTitle: string
|
||||
IsInterlaced: boolean
|
||||
ChannelLayout: string
|
||||
BitRate: number
|
||||
Channels: number
|
||||
SampleRate: number
|
||||
IsDefault: boolean
|
||||
IsForced: boolean
|
||||
IsHearingImpaired: boolean
|
||||
Type: string
|
||||
Index: number
|
||||
IsExternal: boolean
|
||||
IsTextSubtitleStream: boolean
|
||||
SupportsExternalStream: boolean
|
||||
Level: number
|
||||
}
|
||||
|
||||
export interface ArtistItem {
|
||||
Name: string;
|
||||
Id: string;
|
||||
@@ -52,6 +77,7 @@ export interface AlbumTrack {
|
||||
RunTimeTicks: number;
|
||||
ProductionYear: number;
|
||||
IndexNumber: number;
|
||||
ParentIndexNumber: number;
|
||||
IsFolder: boolean;
|
||||
Type: 'Audio';
|
||||
UserData: UserData;
|
||||
@@ -66,6 +92,9 @@ export interface AlbumTrack {
|
||||
BackdropImageTags: any[];
|
||||
LocationType: string;
|
||||
MediaType: string;
|
||||
HasLyrics: boolean;
|
||||
Lyrics: Lyrics | null;
|
||||
MediaStreams: MediaStream[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -98,4 +127,4 @@ export interface Playlist {
|
||||
|
||||
export interface SimilarAlbum {
|
||||
Id: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { ColorScheme } from './types';
|
||||
|
||||
export const setJellyfinCredentials = createAction<{ access_token: string, user_id: string, uri: string, device_id: string; }>('SET_JELLYFIN_CREDENTIALS');
|
||||
export const setJellyfinCredentials = createAction<{ access_token: string, user_id: string, uri: string, device_id: string; type: 'jellyfin' | 'emby' }>('SET_JELLYFIN_CREDENTIALS');
|
||||
export const setBitrate = createAction<number>('SET_BITRATE');
|
||||
export const setOnboardingStatus = createAction<boolean>('SET_ONBOARDING_STATUS');
|
||||
export const setReceivedErrorReportingAlert = createAction<void>('SET_RECEIVED_ERROR_REPORTING_ALERT');
|
||||
|
||||
@@ -3,11 +3,12 @@ import { setReceivedErrorReportingAlert, setBitrate, setJellyfinCredentials, set
|
||||
import { ColorScheme } from './types';
|
||||
|
||||
interface State {
|
||||
jellyfin?: {
|
||||
credentials?: {
|
||||
uri: string;
|
||||
user_id: string;
|
||||
access_token: string;
|
||||
device_id: string;
|
||||
type: 'jellyfin' | 'emby';
|
||||
}
|
||||
bitrate: number;
|
||||
isOnboardingComplete: boolean;
|
||||
@@ -27,7 +28,7 @@ const initialState: State = {
|
||||
const settings = createReducer(initialState, builder => {
|
||||
builder.addCase(setJellyfinCredentials, (state, action) => ({
|
||||
...state,
|
||||
jellyfin: action.payload,
|
||||
credentials: action.payload,
|
||||
}));
|
||||
builder.addCase(setBitrate, (state, action) => ({
|
||||
...state,
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player';
|
||||
import { AppState, useTypedSelector } from '@/store';
|
||||
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
type Credentials = AppState['settings']['jellyfin'];
|
||||
|
||||
/**
|
||||
* This is a convenience function that converts a set of Jellyfin credentials
|
||||
* from the Redux store to a HTTP Header that authenticates the user against the
|
||||
* Jellyfin server.
|
||||
*/
|
||||
function generateConfig(credentials: Credentials): RequestInit {
|
||||
return {
|
||||
headers: {
|
||||
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${credentials?.access_token}"`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const trackOptionsOsOverrides: Record<typeof Platform.OS, Record<string, string>> = {
|
||||
ios: {
|
||||
Container: 'mp3,aac,m4a|aac,m4b|aac,flac,alac,m4a|alac,m4b|alac,wav,m4a,aiff,aif',
|
||||
},
|
||||
android: {
|
||||
Container: 'mp3,aac,flac,wav,ogg,ogg|vorbis,ogg|opus,mka|mp3,mka|opus,mka|mp3',
|
||||
},
|
||||
macos: {},
|
||||
web: {},
|
||||
windows: {},
|
||||
};
|
||||
|
||||
const baseTrackOptions: Record<string, string> = {
|
||||
TranscodingProtocol: 'http',
|
||||
TranscodingContainer: 'aac',
|
||||
AudioCodec: 'aac',
|
||||
Container: 'mp3,aac',
|
||||
...trackOptionsOsOverrides[Platform.OS],
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a track object from a Jellyfin ItemId so that
|
||||
* react-native-track-player can easily consume it.
|
||||
*/
|
||||
export function generateTrack(track: AlbumTrack, credentials: Credentials): Track {
|
||||
// Also construct the URL for the stream
|
||||
const url = generateTrackUrl(track.Id, credentials);
|
||||
|
||||
return {
|
||||
url,
|
||||
backendId: track.Id,
|
||||
title: track.Name,
|
||||
artist: track.Artists.join(', '),
|
||||
album: track.Album,
|
||||
duration: track.RunTimeTicks,
|
||||
artwork: track.AlbumId
|
||||
? getImage(track.AlbumId, credentials)
|
||||
: getImage(track.Id, credentials),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the track streaming url from the trackId
|
||||
*/
|
||||
export function generateTrackUrl(trackId: string, credentials: Credentials) {
|
||||
const trackOptions = {
|
||||
...baseTrackOptions,
|
||||
UserId: credentials?.user_id || '',
|
||||
api_key: credentials?.access_token || '',
|
||||
DeviceId: credentials?.device_id || '',
|
||||
};
|
||||
|
||||
const trackParams = new URLSearchParams(trackOptions).toString();
|
||||
const url = encodeURI(`${credentials?.uri}/Audio/${trackId}/universal?`) + trackParams;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
const albumOptions = {
|
||||
SortBy: 'AlbumArtist,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
ImageTypeLimit: '1',
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
};
|
||||
|
||||
const albumParams = new URLSearchParams(albumOptions).toString();
|
||||
|
||||
/**
|
||||
* Retrieve all albums that are available on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveAllAlbums(credentials: Credentials) {
|
||||
const config = generateConfig(credentials);
|
||||
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${albumParams}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return albums.Items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single album
|
||||
*/
|
||||
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
|
||||
const config = generateConfig(credentials);
|
||||
|
||||
const Similar = await fetch(`${credentials?.uri}/Items/${id}/Similar?userId=${credentials?.user_id}&limit=12`, config)
|
||||
.then(response => response.json() as Promise<{ Items: SimilarAlbum[] }>)
|
||||
.then((albums) => albums.Items.map((a) => a.Id));
|
||||
|
||||
return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config)
|
||||
.then(response => response.json() as Promise<Album>)
|
||||
.then(album => ({ ...album, Similar }));
|
||||
}
|
||||
|
||||
const latestAlbumsOptions = {
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
Fields: 'DateCreated',
|
||||
SortOrder: 'Ascending',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the most recently added albums on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveRecentAlbums(credentials: Credentials, numberOfAlbums = 24) {
|
||||
const config = generateConfig(credentials);
|
||||
|
||||
// Generate custom config based on function input
|
||||
const options = {
|
||||
...latestAlbumsOptions,
|
||||
Limit: numberOfAlbums.toString(),
|
||||
};
|
||||
const params = new URLSearchParams(options).toString();
|
||||
|
||||
// Retrieve albums
|
||||
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/Latest?${params}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return albums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single album from the Emby server
|
||||
*/
|
||||
export async function retrieveAlbumTracks(ItemId: string, credentials: Credentials) {
|
||||
const singleAlbumOptions = {
|
||||
ParentId: ItemId,
|
||||
SortBy: 'SortName',
|
||||
};
|
||||
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();
|
||||
|
||||
const config = generateConfig(credentials);
|
||||
const album = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${singleAlbumParams}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return album.Items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an image URL for a given ItemId
|
||||
*/
|
||||
export function getImage(ItemId: string, credentials: Credentials): string {
|
||||
return encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hook that can convert ItemIds to image URLs
|
||||
*/
|
||||
export function useGetImage() {
|
||||
const credentials = useTypedSelector((state) => state.settings.jellyfin);
|
||||
return (ItemId: string) => getImage(ItemId, credentials);
|
||||
}
|
||||
|
||||
const trackParams = {
|
||||
SortBy: 'AlbumArtist,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Audio',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve all possible tracks that can be found in Jellyfin
|
||||
*/
|
||||
export async function retrieveAllTracks(credentials: Credentials) {
|
||||
const config = generateConfig(credentials);
|
||||
const tracks = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${trackParams}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return tracks.Items;
|
||||
}
|
||||
|
||||
const searchParams = {
|
||||
IncludeItemTypes: 'Audio,MusicAlbum',
|
||||
SortBy: 'Album,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
Recursive: 'true',
|
||||
};
|
||||
|
||||
/**
|
||||
* Remotely search the Jellyfin library for a particular search term
|
||||
*/
|
||||
export async function searchItem(
|
||||
credentials: Credentials,
|
||||
term: string, limit = 24
|
||||
): Promise<(Album | AlbumTrack)[]> {
|
||||
const config = generateConfig(credentials);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
...searchParams,
|
||||
SearchTerm: term,
|
||||
Limit: limit.toString(),
|
||||
}).toString();
|
||||
|
||||
const results = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${params}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return results.Items;
|
||||
}
|
||||
|
||||
const playlistOptions = {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Playlist',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
ImageTypeLimit: '1',
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
MediaTypes: 'Audio',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve all albums that are available on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveAllPlaylists(credentials: Credentials) {
|
||||
const config = generateConfig(credentials);
|
||||
const playlistParams = new URLSearchParams(playlistOptions).toString();
|
||||
|
||||
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${playlistParams}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return albums.Items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all albums that are available on the Jellyfin server
|
||||
*/
|
||||
export async function retrievePlaylistTracks(ItemId: string, credentials: Credentials) {
|
||||
const singlePlaylistOptions = {
|
||||
SortBy: 'SortName',
|
||||
UserId: credentials?.user_id || '',
|
||||
};
|
||||
const singlePlaylistParams = new URLSearchParams(singlePlaylistOptions).toString();
|
||||
|
||||
const config = generateConfig(credentials);
|
||||
const playlists = await fetch(`${credentials?.uri}/Playlists/${ItemId}/Items?${singlePlaylistParams}`, config)
|
||||
.then(response => response.json());
|
||||
|
||||
return playlists.Items;
|
||||
}
|
||||
|
||||
/**
|
||||
* This maps the react-native-track-player RepeatMode to a RepeatMode that is
|
||||
* expected by Jellyfin when reporting playback events.
|
||||
*/
|
||||
const RepeatModeMap: Record<RepeatMode, string> = {
|
||||
[RepeatMode.Off]: 'RepeatNone',
|
||||
[RepeatMode.Track]: 'RepeatOne',
|
||||
[RepeatMode.Queue]: 'RepeatAll',
|
||||
};
|
||||
|
||||
/**
|
||||
* This will generate the payload that is required for playback events and send
|
||||
* it to the supplied path.
|
||||
*/
|
||||
export async function sendPlaybackEvent(path: string, credentials: Credentials, trackIndex?: number) {
|
||||
// Extract all data from react-native-track-player
|
||||
const [
|
||||
currentTrack, position, repeatMode, volume, queue, state,
|
||||
] = await Promise.all([
|
||||
TrackPlayer.getCurrentTrack(),
|
||||
TrackPlayer.getPosition(),
|
||||
TrackPlayer.getRepeatMode(),
|
||||
TrackPlayer.getVolume(),
|
||||
TrackPlayer.getQueue(),
|
||||
TrackPlayer.getState(),
|
||||
]);
|
||||
|
||||
// Switch between overriden track index and current track
|
||||
const track = trackIndex !== undefined ? trackIndex : currentTrack;
|
||||
|
||||
// Generate a payload from the gathered data
|
||||
const payload = {
|
||||
VolumeLevel: volume * 100,
|
||||
IsMuted: false,
|
||||
IsPaused: state === State.Paused,
|
||||
RepeatMode: RepeatModeMap[repeatMode],
|
||||
ShuffleMode: 'Sorted',
|
||||
PositionTicks: Math.round(position * 10_000_000),
|
||||
PlaybackRate: 1,
|
||||
PlayMethod: 'transcode',
|
||||
MediaSourceId: track !== null ? queue[track].backendId : null,
|
||||
ItemId: track !== null ? queue[track].backendId : null,
|
||||
CanSeek: true,
|
||||
PlaybackStartTimeTicks: null,
|
||||
};
|
||||
|
||||
// Generate a config from the credentials and dispatch the request
|
||||
const config = generateConfig(credentials);
|
||||
await fetch(`${credentials?.uri}${path}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...config.headers,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
// Swallow and errors from the request
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
73
src/utility/JellyfinApi/album.ts
Normal file
73
src/utility/JellyfinApi/album.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types';
|
||||
import { fetchApi } from './lib';
|
||||
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts';
|
||||
|
||||
const albumOptions = {
|
||||
SortBy: 'AlbumArtist,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
ImageTypeLimit: '1',
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
};
|
||||
|
||||
const albumParams = new URLSearchParams(albumOptions).toString();
|
||||
|
||||
/**
|
||||
* Retrieve all albums that are available on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveAllAlbums() {
|
||||
return fetchApi<{ Items: Album[] }>(({ user_id }) => `/Users/${user_id}/Items?${albumParams}`)
|
||||
.then((data) => data!.Items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single album
|
||||
*/
|
||||
export async function retrieveAlbum(id: string): Promise<Album> {
|
||||
const Similar = await fetchApi<{ Items: SimilarAlbum[] }>(({ user_id }) => `/Items/${id}/Similar?userId=${user_id}&limit=12`)
|
||||
.then((albums) => albums!.Items.map((a) => a.Id));
|
||||
|
||||
return fetchApi<Album>(({ user_id }) => `/Users/${user_id}/Items/${id}`)
|
||||
.then(album => ({ ...album!, Similar }));
|
||||
}
|
||||
|
||||
const latestAlbumsOptions = {
|
||||
IncludeItemTypes: 'MusicAlbum',
|
||||
Fields: 'DateCreated',
|
||||
SortOrder: 'Descending',
|
||||
SortBy: 'DateCreated',
|
||||
Recursive: 'true',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the most recently added albums on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveRecentAlbums(numberOfAlbums = 24) {
|
||||
// Generate custom config based on function input
|
||||
const options = {
|
||||
...latestAlbumsOptions,
|
||||
Limit: numberOfAlbums.toString(),
|
||||
};
|
||||
const params = new URLSearchParams(options).toString();
|
||||
|
||||
// Retrieve albums
|
||||
return fetchApi<{ Items: Album[] }>(({ user_id }) => `/Users/${user_id}/Items?${params}`)
|
||||
.then((d) => d.Items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a single album from the Emby server
|
||||
*/
|
||||
export async function retrieveAlbumTracks(ItemId: string) {
|
||||
const singleAlbumOptions = {
|
||||
ParentId: ItemId,
|
||||
SortBy: 'ParentIndexNumber,IndexNumber,SortName',
|
||||
Fields: 'MediaStreams',
|
||||
};
|
||||
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();
|
||||
|
||||
return fetchApi<{ Items: AlbumTrack[] }>(({ user_id }) => `/Users/${user_id}/Items?${singleAlbumParams}`)
|
||||
.then((data) => retrieveAndInjectLyricsToTracks(data.Items));
|
||||
}
|
||||
119
src/utility/JellyfinApi/lib.ts
Normal file
119
src/utility/JellyfinApi/lib.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { AppState, Store } from '@/store';
|
||||
import { Platform } from 'react-native';
|
||||
import { version } from '../../../package.json';
|
||||
|
||||
type Credentials = AppState['settings']['credentials'];
|
||||
|
||||
/** Map the output of `Platform.OS`, so that Jellyfin can understand it. */
|
||||
const deviceMap: Record<typeof Platform['OS'], string> = {
|
||||
ios: 'iOS',
|
||||
android: 'Android',
|
||||
macos: 'macOS',
|
||||
web: 'Web',
|
||||
windows: 'Windows',
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a convenience function that converts a set of Jellyfin credentials
|
||||
* from the Redux store to a HTTP Header that authenticates the user against the
|
||||
* Jellyfin server.
|
||||
*/
|
||||
function generateConfig(credentials: Credentials): RequestInit {
|
||||
return {
|
||||
headers: {
|
||||
'X-Emby-Authorization': `MediaBrowser Client="Fintunes", Device="${deviceMap[Platform.OS]}", DeviceId="${credentials?.device_id}", Version="${version}", Token="${credentials?.access_token}"`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a copy of the store without getting caught in import cycles.
|
||||
*/
|
||||
export function asyncFetchStore() {
|
||||
return require('@/store').default as Store;
|
||||
}
|
||||
|
||||
export type PathOrCredentialInserter = string | ((credentials: NonNullable<Credentials>) => string);
|
||||
|
||||
/**
|
||||
* A convenience function that accepts a request for fetch, injects it with the
|
||||
* proper Jellyfin credentials and attempts to catch any errors along the way.
|
||||
*/
|
||||
export async function fetchApi<T>(path: PathOrCredentialInserter, providedConfig?: RequestInit, parseResponse?: true): Promise<T>;
|
||||
export async function fetchApi(path: PathOrCredentialInserter, providedConfig: RequestInit, parseResponse: false): Promise<null>;
|
||||
export async function fetchApi<T>(
|
||||
path: PathOrCredentialInserter,
|
||||
providedConfig?: RequestInit,
|
||||
parseResponse = true
|
||||
) {
|
||||
// Retrieve the latest credentials from the Redux store
|
||||
const credentials = asyncFetchStore().getState().settings.credentials;
|
||||
|
||||
// GUARD: Check that the credentials are present
|
||||
if (!credentials) {
|
||||
throw new Error('Missing Jellyfin credentials when attempting API request');
|
||||
}
|
||||
|
||||
// Create the URL from the path and the credentials
|
||||
const resolvedPath = typeof path === 'function' ? path(credentials) : path;
|
||||
const url = `${credentials.uri}${resolvedPath.startsWith('/') ? '' : '/'}${resolvedPath}`;
|
||||
|
||||
// Create config
|
||||
const config = {
|
||||
...providedConfig,
|
||||
headers: {
|
||||
...providedConfig?.headers,
|
||||
...generateConfig(credentials).headers,
|
||||
}
|
||||
};
|
||||
|
||||
// Actually perform the request
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (__DEV__) {
|
||||
console.log(`%c[HTTP] → [${response.status}] ${url}`, 'font-weight:bold;');
|
||||
console.log('\t', config);
|
||||
}
|
||||
|
||||
// GUARD: Check if the response is as expected
|
||||
if (!response.ok) {
|
||||
if (response.status === 403 || response.status === 401) {
|
||||
throw new Error('AuthenticationFailed');
|
||||
} else if (response.status === 404) {
|
||||
throw new Error('ResourceNotFound');
|
||||
}
|
||||
|
||||
// Attempt to parse the error message
|
||||
try {
|
||||
const data = await response.json();
|
||||
throw data;
|
||||
} catch {
|
||||
throw new Error('FailedRequest');
|
||||
}
|
||||
}
|
||||
|
||||
if (parseResponse) {
|
||||
// Parse body as JSON
|
||||
const data = await response.json() as Promise<T>;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an image URL for a given ItemId
|
||||
*/
|
||||
export function getImage(ItemId: string): string {
|
||||
const credentials = asyncFetchStore().getState().settings.credentials;
|
||||
const uri = encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hook that can convert ItemIds to image URLs
|
||||
*/
|
||||
export function useGetImage() {
|
||||
return (ItemId: string) => getImage(ItemId);
|
||||
}
|
||||
48
src/utility/JellyfinApi/lyrics.ts
Normal file
48
src/utility/JellyfinApi/lyrics.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { fetchApi } from './lib';
|
||||
import {AlbumTrack} from '@/store/music/types.ts';
|
||||
|
||||
interface Metadata {
|
||||
Artist: string
|
||||
Album: string
|
||||
Title: string
|
||||
Author: string
|
||||
Length: number
|
||||
By: string
|
||||
Offset: number
|
||||
Creator: string
|
||||
Version: string
|
||||
IsSynced: boolean
|
||||
}
|
||||
|
||||
interface LyricData {
|
||||
Text: string
|
||||
Start: number
|
||||
}
|
||||
|
||||
export interface Lyrics {
|
||||
Metadata: Metadata
|
||||
Lyrics: LyricData[]
|
||||
}
|
||||
|
||||
async function retrieveTrackLyrics(trackId: string): Promise<Lyrics | null> {
|
||||
return fetchApi<Lyrics>(`/Audio/${trackId}/Lyrics`)
|
||||
.catch((e) => {
|
||||
console.error('Error on fetching track lyrics: ', e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function retrieveAndInjectLyricsToTracks(tracks: AlbumTrack[]): Promise<AlbumTrack[]> {
|
||||
return Promise.all(tracks.map(async (track) => {
|
||||
if (!track.HasLyrics) {
|
||||
track.Lyrics = null;
|
||||
return track;
|
||||
}
|
||||
|
||||
track.Lyrics = await retrieveTrackLyrics(track.Id);
|
||||
|
||||
return track;
|
||||
|
||||
}));
|
||||
}
|
||||
65
src/utility/JellyfinApi/playback.ts
Normal file
65
src/utility/JellyfinApi/playback.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player';
|
||||
import { fetchApi } from './lib';
|
||||
|
||||
/**
|
||||
* This maps the react-native-track-player RepeatMode to a RepeatMode that is
|
||||
* expected by Jellyfin when reporting playback events.
|
||||
*/
|
||||
const RepeatModeMap: Record<RepeatMode, string> = {
|
||||
[RepeatMode.Off]: 'RepeatNone',
|
||||
[RepeatMode.Track]: 'RepeatOne',
|
||||
[RepeatMode.Queue]: 'RepeatAll',
|
||||
};
|
||||
|
||||
/**
|
||||
* This will generate the payload that is required for playback events and send
|
||||
* it to the supplied path.
|
||||
*/
|
||||
export async function sendPlaybackEvent(
|
||||
path: string,
|
||||
track?: Track,
|
||||
lastPosition?: number,
|
||||
) {
|
||||
// Extract all data from react-native-track-player
|
||||
const [
|
||||
activeTrack, { position: currentPosition }, repeatMode, volume, { state },
|
||||
] = await Promise.all([
|
||||
track || TrackPlayer.getActiveTrack(),
|
||||
TrackPlayer.getProgress(),
|
||||
TrackPlayer.getRepeatMode(),
|
||||
TrackPlayer.getVolume(),
|
||||
TrackPlayer.getPlaybackState(),
|
||||
]);
|
||||
|
||||
// GUARD: Ensure that no empty events are sent out
|
||||
if (!activeTrack?.backendId) return;
|
||||
|
||||
// Generate a payload from the gathered data
|
||||
const payload = {
|
||||
VolumeLevel: volume * 100,
|
||||
IsMuted: false,
|
||||
IsPaused: state === State.Paused,
|
||||
RepeatMode: RepeatModeMap[repeatMode],
|
||||
ShuffleMode: 'Sorted',
|
||||
PositionTicks: Math.round((lastPosition || currentPosition) * 10_000_000),
|
||||
PlaybackRate: 1,
|
||||
PlayMethod: 'transcode',
|
||||
MediaSourceId: activeTrack.backendId,
|
||||
ItemId: activeTrack.backendId,
|
||||
CanSeek: true,
|
||||
PlaybackStartTimeTicks: null,
|
||||
PlaySessionId: activeTrack?.backendId || 'fintunes',
|
||||
};
|
||||
|
||||
// Generate a config from the credentials and dispatch the request
|
||||
await fetchApi(path, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
// Swallow and errors from the request
|
||||
}, false).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
39
src/utility/JellyfinApi/playlist.ts
Normal file
39
src/utility/JellyfinApi/playlist.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { AlbumTrack, Playlist } from '@/store/music/types';
|
||||
import { asyncFetchStore, fetchApi } from './lib';
|
||||
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts';
|
||||
|
||||
const playlistOptions = {
|
||||
SortBy: 'SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Playlist',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
ImageTypeLimit: '1',
|
||||
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
||||
MediaTypes: 'Audio',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve all albums that are available on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveAllPlaylists() {
|
||||
const playlistParams = new URLSearchParams(playlistOptions).toString();
|
||||
|
||||
return fetchApi<{ Items: Playlist[] }>(({ user_id }) => `/Users/${user_id}/Items?${playlistParams}`)
|
||||
.then((d) => d!.Items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all albums that are available on the Jellyfin server
|
||||
*/
|
||||
export async function retrievePlaylistTracks(ItemId: string) {
|
||||
const credentials = asyncFetchStore().getState().settings.credentials;
|
||||
const singlePlaylistOptions = {
|
||||
SortBy: 'IndexNumber,SortName',
|
||||
UserId: credentials?.user_id || '',
|
||||
};
|
||||
const singlePlaylistParams = new URLSearchParams(singlePlaylistOptions).toString();
|
||||
|
||||
return fetchApi<{ Items: AlbumTrack[] }>(`/Playlists/${ItemId}/Items?${singlePlaylistParams}`)
|
||||
.then((d) => retrieveAndInjectLyricsToTracks(d.Items));
|
||||
}
|
||||
30
src/utility/JellyfinApi/search.ts
Normal file
30
src/utility/JellyfinApi/search.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Album, AlbumTrack } from '@/store/music/types';
|
||||
import { fetchApi } from './lib';
|
||||
|
||||
const searchParams = {
|
||||
IncludeItemTypes: 'Audio,MusicAlbum',
|
||||
SortBy: 'SearchScore,Album,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
Recursive: 'true',
|
||||
};
|
||||
|
||||
/**
|
||||
* Remotely search the Jellyfin library for a particular search term
|
||||
*/
|
||||
export async function searchItem(
|
||||
term: string, limit = 24
|
||||
) {
|
||||
const params = new URLSearchParams({
|
||||
...searchParams,
|
||||
SearchTerm: term,
|
||||
Limit: limit.toString(),
|
||||
}).toString();
|
||||
|
||||
const results = await fetchApi<{ Items: (Album | AlbumTrack)[]}>(({ user_id }) => `/Users/${user_id}/Items?${params}`);
|
||||
|
||||
return results!.Items
|
||||
.filter((item) => (
|
||||
// GUARD: Ensure that we're either dealing with an album or a track from an album.
|
||||
item.Type === 'MusicAlbum' || (item.Type === 'Audio' && item.AlbumId)
|
||||
));
|
||||
}
|
||||
90
src/utility/JellyfinApi/track.ts
Normal file
90
src/utility/JellyfinApi/track.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { AlbumTrack } from '@/store/music/types';
|
||||
import { Platform } from 'react-native';
|
||||
import { Track } from 'react-native-track-player';
|
||||
import { fetchApi, getImage } from './lib';
|
||||
import store from '@/store';
|
||||
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics';
|
||||
|
||||
const trackOptionsOsOverrides: Record<typeof Platform.OS, Record<string, string>> = {
|
||||
ios: {
|
||||
Container: 'mp3,aac,m4a|aac,m4b|aac,flac,alac,m4a|alac,m4b|alac,wav,m4a,aiff,aif',
|
||||
},
|
||||
android: {
|
||||
Container: 'mp3,aac,flac,wav,ogg,ogg|vorbis,ogg|opus,mka|mp3,mka|opus,mka|mp3',
|
||||
},
|
||||
macos: {},
|
||||
web: {},
|
||||
windows: {},
|
||||
};
|
||||
|
||||
const baseTrackOptions: Record<string, string> = {
|
||||
TranscodingProtocol: 'http',
|
||||
TranscodingContainer: 'aac',
|
||||
AudioCodec: 'aac',
|
||||
Container: 'mp3,aac',
|
||||
audioBitRate: '320000',
|
||||
...trackOptionsOsOverrides[Platform.OS],
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Generate the track streaming url from the trackId
|
||||
*/
|
||||
export function generateTrackUrl(trackId: string) {
|
||||
const credentials = store.getState().settings.credentials;
|
||||
const trackOptions = {
|
||||
...baseTrackOptions,
|
||||
UserId: credentials?.user_id || '',
|
||||
api_key: credentials?.access_token || '',
|
||||
DeviceId: credentials?.device_id || '',
|
||||
};
|
||||
|
||||
const trackParams = new URLSearchParams(trackOptions).toString();
|
||||
const url = encodeURI(`${credentials?.uri}/Audio/${trackId}/universal?`) + trackParams;
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a track object from a Jellyfin ItemId so that
|
||||
* react-native-track-player can easily consume it.
|
||||
*/
|
||||
export async function generateTrack(track: AlbumTrack): Promise<Track> {
|
||||
// Also construct the URL for the stream
|
||||
const url = generateTrackUrl(track.Id);
|
||||
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
|
||||
return {
|
||||
url,
|
||||
backendId: track.Id,
|
||||
title: track.Name,
|
||||
artist: track.Artists.join(', '),
|
||||
album: track.Album,
|
||||
duration: track.RunTimeTicks,
|
||||
artwork: track.AlbumId
|
||||
? getImage(track.AlbumId)
|
||||
: getImage(track.Id),
|
||||
hasLyrics: track.HasLyrics,
|
||||
lyrics: track.Lyrics,
|
||||
contentType: response.headers.get('Content-Type') || undefined,
|
||||
isDirectPlay: response.headers.has('Content-Length'),
|
||||
bitRate: baseTrackOptions.audioBitRate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const trackParams = {
|
||||
SortBy: 'AlbumArtist,SortName',
|
||||
SortOrder: 'Ascending',
|
||||
IncludeItemTypes: 'Audio',
|
||||
Recursive: 'true',
|
||||
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve all possible tracks that can be found in Jellyfin
|
||||
*/
|
||||
export async function retrieveAllTracks() {
|
||||
return fetchApi<{ Items: AlbumTrack[] }>(({ user_id }) => `/Users/${user_id}/Items?${trackParams}`)
|
||||
.then((d) => retrieveAndInjectLyricsToTracks(d.Items));
|
||||
}
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
import TrackPlayer, { Event, State } from 'react-native-track-player';
|
||||
import store from '@/store';
|
||||
import { sendPlaybackEvent } from './JellyfinApi';
|
||||
import { setTimerDate } from '@/store/sleep-timer';
|
||||
import { sendPlaybackEvent } from './JellyfinApi/playback';
|
||||
|
||||
export default async function() {
|
||||
TrackPlayer.addEventListener(Event.RemotePlay, () => {
|
||||
@@ -37,18 +37,19 @@ export default async function() {
|
||||
TrackPlayer.seekTo(event.position);
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener(Event.PlaybackTrackChanged, async (e) => {
|
||||
TrackPlayer.addEventListener(Event.PlaybackActiveTrackChanged, async (e) => {
|
||||
// Retrieve the current settings from the Redux store
|
||||
const settings = store.getState().settings;
|
||||
console.log('TrackChanged', e?.track?.title);
|
||||
|
||||
// GUARD: Only report playback when the settings is enabled
|
||||
if (settings.enablePlaybackReporting && 'track' in e) {
|
||||
// GUARD: End the previous track if it's about to end
|
||||
if ('nextTrack' in e && typeof e.track === 'number') {
|
||||
sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin, e.track);
|
||||
if (e.lastTrack) {
|
||||
sendPlaybackEvent('/Sessions/Playing/Stopped', e.lastTrack, e.lastPosition);
|
||||
}
|
||||
|
||||
sendPlaybackEvent('/Sessions/Playing', settings.jellyfin);
|
||||
sendPlaybackEvent('/Sessions/Playing', e.track);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,7 +59,7 @@ export default async function() {
|
||||
|
||||
// GUARD: Only report playback when the settings is enabled
|
||||
if (settings.enablePlaybackReporting) {
|
||||
sendPlaybackEvent('/Sessions/Playing/Progress', settings.jellyfin);
|
||||
sendPlaybackEvent('/Sessions/Playing/Progress');
|
||||
}
|
||||
|
||||
// check if timerDate is undefined, otherwise start timer
|
||||
@@ -69,14 +70,16 @@ export default async function() {
|
||||
});
|
||||
|
||||
TrackPlayer.addEventListener(Event.PlaybackState, (event) => {
|
||||
// GUARD: Only respond to stopped events
|
||||
if (event.state === State.Stopped) {
|
||||
// Retrieve the current settings from the Redux store
|
||||
const settings = store.getState().settings;
|
||||
// Retrieve the current settings from the Redux store
|
||||
const settings = store.getState().settings;
|
||||
|
||||
// GUARD: Only report playback when the settings is enabled
|
||||
if (settings.enablePlaybackReporting) {
|
||||
sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin);
|
||||
// GUARD: Only report playback when the settings is enabled
|
||||
if (settings.enablePlaybackReporting) {
|
||||
// GUARD: Only respond to stopped events
|
||||
if (event.state === State.Stopped) {
|
||||
sendPlaybackEvent('/Sessions/Playing/Stopped');
|
||||
} else if (event.state === State.Paused) {
|
||||
sendPlaybackEvent('/Sessions/Playing/Progress');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-track-player';
|
||||
import { useTypedSelector } from '@/store';
|
||||
import { AlbumTrack } from '@/store/music/types';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import TrackPlayer, { Event, useTrackPlayerEvents, Track } from 'react-native-track-player';
|
||||
|
||||
interface CurrentTrackResponse {
|
||||
track: Track | undefined;
|
||||
albumTrack: AlbumTrack | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
@@ -13,11 +16,19 @@ export default function useCurrentTrack(): CurrentTrackResponse {
|
||||
const [track, setTrack] = useState<Track | undefined>();
|
||||
const [index, setIndex] = useState<number | undefined>();
|
||||
|
||||
// Retrieve entities from the store
|
||||
const entities = useTypedSelector((state) => state.music.tracks.entities);
|
||||
|
||||
// Attempt to extract the track from the store
|
||||
const albumTrack = useMemo(() => (
|
||||
entities[track?.backendId]
|
||||
), [track?.backendId, entities]);
|
||||
|
||||
// Retrieve the current track from the queue using the index
|
||||
const retrieveCurrentTrack = useCallback(async () => {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
const currentTrackIndex = await TrackPlayer.getCurrentTrack();
|
||||
if (currentTrackIndex !== null) {
|
||||
const currentTrackIndex = await TrackPlayer.getActiveTrackIndex();
|
||||
if (currentTrackIndex !== undefined) {
|
||||
setTrack(queue[currentTrackIndex]);
|
||||
setIndex(currentTrackIndex);
|
||||
} else {
|
||||
@@ -28,7 +39,7 @@ export default function useCurrentTrack(): CurrentTrackResponse {
|
||||
|
||||
// Then execute the function on component mount and track changes
|
||||
useEffect(() => { retrieveCurrentTrack(); }, [retrieveCurrentTrack]);
|
||||
useTrackPlayerEvents([ Event.PlaybackTrackChanged, Event.PlaybackState ], retrieveCurrentTrack);
|
||||
|
||||
return { track, index };
|
||||
}
|
||||
useTrackPlayerEvents([ Event.PlaybackActiveTrackChanged, Event.PlaybackState ], retrieveCurrentTrack);
|
||||
|
||||
return { track, index, albumTrack };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { useTypedSelector } from '@/store';
|
||||
import { useCallback } from 'react';
|
||||
import TrackPlayer, { Track } from 'react-native-track-player';
|
||||
import { generateTrack } from './JellyfinApi';
|
||||
import { shuffle as shuffleArray } from 'lodash';
|
||||
import { generateTrack } from './JellyfinApi/track';
|
||||
|
||||
interface PlayOptions {
|
||||
play: boolean;
|
||||
shuffle: boolean;
|
||||
method: 'add-to-end' | 'add-after-currently-playing' | 'replace';
|
||||
/**
|
||||
* The index for the track that should start out playing. This ensures that
|
||||
* no intermediate tracks are played (however briefly) while the queue skips
|
||||
* to this index.
|
||||
*
|
||||
* NOTE: This option is only available with the `replace` method.
|
||||
*/
|
||||
playIndex?: number;
|
||||
}
|
||||
|
||||
const defaults: PlayOptions = {
|
||||
@@ -21,7 +29,6 @@ const defaults: PlayOptions = {
|
||||
* supplied id.
|
||||
*/
|
||||
export default function usePlayTracks() {
|
||||
const credentials = useTypedSelector(state => state.settings.jellyfin);
|
||||
const tracks = useTypedSelector(state => state.music.tracks.entities);
|
||||
const downloads = useTypedSelector(state => state.downloads.entities);
|
||||
|
||||
@@ -42,7 +49,7 @@ export default function usePlayTracks() {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
|
||||
// Convert all trackIds to the relevant format for react-native-track-player
|
||||
const generatedTracks = trackIds.map((trackId) => {
|
||||
const generatedTracks = (await Promise.all(trackIds.map(async (trackId) => {
|
||||
const track = tracks[trackId];
|
||||
|
||||
// GUARD: Check that the track actually exists in Redux
|
||||
@@ -51,7 +58,7 @@ export default function usePlayTracks() {
|
||||
}
|
||||
|
||||
// Retrieve the generated track from Jellyfin
|
||||
const generatedTrack = generateTrack(track, credentials);
|
||||
const generatedTrack = await generateTrack(track);
|
||||
|
||||
// Check if a downloaded version exists, and if so rewrite the URL
|
||||
const download = downloads[trackId];
|
||||
@@ -60,7 +67,7 @@ export default function usePlayTracks() {
|
||||
}
|
||||
|
||||
return generatedTrack;
|
||||
}).filter((t): t is Track => typeof t !== 'undefined');
|
||||
}))).filter((t): t is Track => typeof t !== 'undefined');
|
||||
|
||||
// Potentially shuffle all tracks
|
||||
const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;
|
||||
@@ -104,15 +111,33 @@ export default function usePlayTracks() {
|
||||
break;
|
||||
}
|
||||
case 'replace': {
|
||||
// Reset the queue first
|
||||
await TrackPlayer.reset();
|
||||
await TrackPlayer.add(newTracks);
|
||||
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
// GUARD: Check if we need to skip to a particular index
|
||||
if (options.playIndex) {
|
||||
// If so, we'll split the tracks into tracks before the
|
||||
// index that should be played, and the queue of tracks that
|
||||
// will start playing
|
||||
const before = newTracks.slice(0, options.playIndex);
|
||||
const current = newTracks.slice(options.playIndex);
|
||||
|
||||
// First, we'll add the current queue and (optionally) force
|
||||
// it to start playing.
|
||||
await TrackPlayer.add(current);
|
||||
if (play) await TrackPlayer.play();
|
||||
|
||||
// Then, we'll insert the "previous" tracks after the queue
|
||||
// has started playing. This ensures that these tracks won't
|
||||
// trigger any events on the track player.
|
||||
await TrackPlayer.add(before, options.playIndex);
|
||||
} else {
|
||||
await TrackPlayer.add(newTracks);
|
||||
if (play) await TrackPlayer.play();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [credentials, downloads, tracks]);
|
||||
}, [downloads, tracks]);
|
||||
}
|
||||
Reference in New Issue
Block a user