Compare commits

...

92 Commits

Author SHA1 Message Date
Lei Nelissen
e601b0d258 fix: only overflow direct play 2024-07-25 17:13:27 +02:00
Lei Nelissen
065515c25b chore: translation 2024-07-25 17:12:23 +02:00
Lei Nelissen
0cd6d5d05c fix: redundant console.log 2024-07-25 16:58:22 +02:00
Lei Nelissen
92c82b9f0a feat: add base codec info to player 2024-07-25 16:58:22 +02:00
Lei Nelissen
189491b90a chore: release testflight beta 2024-07-25 16:46:15 +02:00
Lei Nelissen
c9f7f71194 fix: further limit extraneous events from playback reporting 2024-07-25 15:45:26 +02:00
Lei Nelissen
0b13e69854 fix: make progress bars initialise out of view 2024-07-25 14:06:06 +02:00
Lei Nelissen
83b890192e chore: upgrade to react native 0.74 2024-07-25 13:55:00 +02:00
Abubakr Khabebulloev
c5b1406e16 Lyrics implementation prototype (#224)
* Lyrics implementation prototype

* feat: update lyrics view

* chore: add docs

* chore: cleanup

* feat: animate active text

* fix: hide lyrics button when there are none

* feat: create lyrics preview in now playing modal

* fix: header overlay color

Closes #224 
Closes #151 
Closes #100 

---------

Co-authored-by: Lei Nelissen <lei@codified.nl>
2024-07-25 13:07:23 +02:00
Lei Nelissen
a64f52c4f9 fix: dark mode inconsistencies
fixes #226
fixes #198
2024-07-22 13:17:26 +02:00
Lei Nelissen
87b08050e4 Merge pull request #197 from weblate/weblate-fintunes-app
chore: Translations update from Hosted Weblate
2024-07-21 23:56:44 +02:00
LibreTranslate
c34d7a8e71 Translated using Weblate (Czech)
Currently translated at 80.5% (62 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/cs/
2024-07-21 23:55:21 +02:00
Lei Nelissen
7e165d3bcc Translated using Weblate (Bulgarian)
Currently translated at 98.7% (76 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/bg/
2024-07-21 23:55:20 +02:00
LibreTranslate
0944128290 Translated using Weblate (Danish)
Currently translated at 48.0% (37 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/da/
2024-07-21 23:55:20 +02:00
LibreTranslate
1b40f7257e Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.4% (75 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pt_BR/
2024-07-21 23:55:20 +02:00
LibreTranslate
e815dc89dc Translated using Weblate (Catalan)
Currently translated at 49.3% (38 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ca/
2024-07-21 23:55:20 +02:00
LibreTranslate
94593859e2 Translated using Weblate (Russian)
Currently translated at 92.2% (71 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ru/
2024-07-21 23:55:20 +02:00
LibreTranslate
30f410ee5e Translated using Weblate (German)
Currently translated at 93.5% (72 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/de/
2024-07-21 23:55:20 +02:00
LibreTranslate
0357cc61ef Translated using Weblate (Swedish)
Currently translated at 93.5% (72 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/sv/
2024-07-21 23:55:20 +02:00
LibreTranslate
1e9dd2577b Translated using Weblate (Italian)
Currently translated at 92.2% (71 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/it/
2024-07-21 23:55:20 +02:00
LibreTranslate
7b02b72e24 Translated using Weblate (Polish)
Currently translated at 98.7% (76 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pl/
2024-07-21 23:55:20 +02:00
LibreTranslate
72f7c2cf5e Translated using Weblate (Ukrainian)
Currently translated at 98.7% (76 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/uk/
2024-07-21 23:55:20 +02:00
LibreTranslate
0d2e502f2d Translated using Weblate (Chinese (Simplified))
Currently translated at 98.7% (76 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/zh_Hans/
2024-07-21 23:55:20 +02:00
Lei Nelissen
14f56685df Translated using Weblate (Dutch)
Currently translated at 100.0% (77 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/nl/
2024-07-21 23:55:20 +02:00
LibreTranslate
10a9e60574 Translated using Weblate (Japanese)
Currently translated at 97.4% (75 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ja/
2024-07-21 23:55:20 +02:00
LibreTranslate
65e630025b Translated using Weblate (French)
Currently translated at 98.7% (76 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/fr/
2024-07-21 23:55:20 +02:00
LibreTranslate
9410c26212 Translated using Weblate (Spanish)
Currently translated at 98.7% (76 of 77 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/es/
2024-07-21 23:55:20 +02:00
V M
f505f68981 Translated using Weblate (French)
Currently translated at 100.0% (76 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/fr/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
ce0bacbfd6 Translated using Weblate (Czech)
Currently translated at 81.5% (62 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/cs/
2024-07-21 23:48:39 +02:00
LibreTranslate
4820f80cfa Translated using Weblate (Danish)
Currently translated at 48.6% (37 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/da/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
038b1bf437 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pt_BR/
2024-07-21 23:48:39 +02:00
LibreTranslate
7646a796b2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pt_BR/
2024-07-21 23:48:39 +02:00
LibreTranslate
ff55f807cf Translated using Weblate (Catalan)
Currently translated at 50.0% (38 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ca/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
a3d266f01d Translated using Weblate (Russian)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ru/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
cd82377373 Translated using Weblate (German)
Currently translated at 94.7% (72 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/de/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
415fbdc528 Translated using Weblate (Italian)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/it/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
981a45fceb Translated using Weblate (Japanese)
Currently translated at 98.6% (75 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ja/
2024-07-21 23:48:39 +02:00
Weblate Translation Memory
b51a8a4caf Translated using Weblate (French)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/fr/
2024-07-21 23:48:38 +02:00
mict213
71deca818d Translated using Weblate (Japanese)
Currently translated at 98.6% (75 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ja/
2024-07-21 23:48:38 +02:00
dasisteinvielzulangername
d6d21c0206 Translated using Weblate (German)
Currently translated at 94.7% (72 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/de/
2024-07-21 23:48:38 +02:00
LibreTranslate
aa575605c0 Translated using Weblate (Danish)
Currently translated at 48.6% (37 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/da/
2024-07-21 23:48:38 +02:00
LibreTranslate
15ffcfb0aa Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pt_BR/
2024-07-21 23:48:38 +02:00
LibreTranslate
62e5309498 Translated using Weblate (Catalan)
Currently translated at 50.0% (38 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ca/
2024-07-21 23:48:38 +02:00
LibreTranslate
d3eb839ea2 Translated using Weblate (Russian)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ru/
2024-07-21 23:48:38 +02:00
LibreTranslate
0e9c3e116a Translated using Weblate (German)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/de/
2024-07-21 23:48:38 +02:00
LibreTranslate
8beefb4180 Translated using Weblate (Swedish)
Currently translated at 94.7% (72 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/sv/
2024-07-21 23:48:38 +02:00
LibreTranslate
9131a601e8 Translated using Weblate (Italian)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/it/
2024-07-21 23:48:38 +02:00
LibreTranslate
5c4d8ea214 Translated using Weblate (Japanese)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ja/
2024-07-21 23:48:38 +02:00
LibreTranslate
0a8997588d Translated using Weblate (French)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/fr/
2024-07-21 23:48:38 +02:00
LibreTranslate
621cf73e50 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pt_BR/
2024-07-21 23:48:38 +02:00
LibreTranslate
02ff4dda3a Translated using Weblate (Russian)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ru/
2024-07-21 23:48:38 +02:00
LibreTranslate
03355c138c Translated using Weblate (German)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/de/
2024-07-21 23:48:38 +02:00
LibreTranslate
2d17bd7872 Translated using Weblate (Swedish)
Currently translated at 94.7% (72 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/sv/
2024-07-21 23:48:38 +02:00
LibreTranslate
c975455e94 Translated using Weblate (Italian)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/it/
2024-07-21 23:48:38 +02:00
LibreTranslate
746d67ed36 Translated using Weblate (Japanese)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ja/
2024-07-21 23:48:38 +02:00
LibreTranslate
1249857196 Translated using Weblate (French)
Currently translated at 93.4% (71 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/fr/
2024-07-21 23:48:38 +02:00
Wolenzi
c7d8ec3151 Translated using Weblate (Polish)
Currently translated at 100.0% (76 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pl/
2024-07-21 23:48:38 +02:00
Mistify
97b10a4faf Translated using Weblate (Polish)
Currently translated at 100.0% (3 of 3 strings)

Translation: Fintunes/Play Store & F-Droid
Translate-URL: https://hosted.weblate.org/projects/fintunes/play-store-f-droid/pl/
2024-07-21 23:48:38 +02:00
Mistify
7af98d0983 Translated using Weblate (Polish)
Currently translated at 42.1% (32 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pl/
2024-07-21 23:48:38 +02:00
Сергій
47c1782652 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (76 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/uk/
2024-07-21 23:48:38 +02:00
gallegonovato
cb5ad032c5 Translated using Weblate (Spanish)
Currently translated at 100.0% (76 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/es/
2024-07-21 23:48:38 +02:00
Mistify
752dec5bc2 Translated using Weblate (Polish)
Currently translated at 66.6% (2 of 3 strings)

Translation: Fintunes/Play Store & F-Droid
Translate-URL: https://hosted.weblate.org/projects/fintunes/play-store-f-droid/pl/
2024-07-21 23:48:38 +02:00
LibreTranslate
0d5f4bd4e0 Translated using Weblate (Danish)
Currently translated at 48.6% (37 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/da/
2024-07-21 23:48:38 +02:00
LibreTranslate
cca8f43ada Translated using Weblate (Catalan)
Currently translated at 50.0% (38 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/ca/
2024-07-21 23:48:38 +02:00
Mistify
a8e024dd00 Translated using Weblate (Polish)
Currently translated at 10.0% (1 of 10 strings)

Translation: Fintunes/App Store
Translate-URL: https://hosted.weblate.org/projects/fintunes/app-store/pl/
2024-07-21 23:48:38 +02:00
LibreTranslate
902178c48b Translated using Weblate (Polish)
Currently translated at 13.1% (10 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pl/
2024-07-21 23:48:38 +02:00
Mistify
f73cec2097 Translated using Weblate (Polish)
Currently translated at 13.1% (10 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/pl/
2024-07-21 23:48:38 +02:00
無情天
f3388132c1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (76 of 76 strings)

Translation: Fintunes/App
Translate-URL: https://hosted.weblate.org/projects/fintunes/app/zh_Hans/
2024-07-21 23:48:38 +02:00
Lei Nelissen
ec4a2b6831 feat: separate discs in album view when multiple are available
fixes #179
2024-07-21 23:48:33 +02:00
Lei Nelissen
7cdd01e713 fix: align all album titles in track list view
fixes #172
2024-07-21 23:02:02 +02:00
Lei Nelissen
38ce9986e5 fix: fine-tune default orderings
fixes #229
2024-07-21 22:45:07 +02:00
Lei Nelissen
9952b39044 chore: fix typescript api call overload 2024-07-21 22:37:42 +02:00
Lei Nelissen
e0177fb89b chore: fix typescript with optional return from api call 2024-07-21 22:30:06 +02:00
Lei Nelissen
68c8808188 fix: send last position for Stopped event
instead of the current position, which is `0` when we move to a new track
2024-07-21 22:10:10 +02:00
Lei Nelissen
746c96d459 fix: don't attempt to parse playback reporting responses
also: log all http requests on dev
2024-07-21 22:03:39 +02:00
Lei Nelissen
a97611c0ad chore: release v2.3.3 2024-06-15 23:23:15 +02:00
Lei Nelissen
e511f744ad chore: default xcode scheme to debug 2024-05-26 23:53:43 +02:00
Lei Nelissen
a6a306b5be fix: refactor JellyfinApi to be less burdensome to implement
Also, automatically catch errors
2024-05-26 23:53:29 +02:00
Lei Nelissen
881ab95029 fix: double-check albums have dates 2024-05-26 22:20:14 +02:00
Lei Nelissen
968e98d8df fix: react-native-screens android setup 2024-05-26 22:20:05 +02:00
Lei Nelissen
b01470bde8 fix: actually send out /Playing events as session updates.
This should more consistently result in output data in your play back reporting modules.

fixes #218
2024-05-26 18:00:05 +02:00
Lei Nelissen
823f7b59e8 Merge pull request #199 from leinelissen/dependabot/npm_and_yarn/ip-1.1.9
chore(deps): bump ip from 1.1.8 to 1.1.9
2024-05-26 17:07:09 +02:00
Lei Nelissen
16162d8e35 fix: throw errors when requests do not yield 200 OKs 2024-05-26 00:34:57 +02:00
Lei Nelissen
ea817025e1 fix: hermes version in cocoapods 2024-05-26 00:24:04 +02:00
Lei Nelissen
00675bbbd3 fix: do extra checks for album ids in 2024-05-26 00:23:29 +02:00
Lei Nelissen
24b5a47a7c Merge pull request #211 from Krafting/patch-1
Add spaces to privacy-policy.md
2024-04-19 17:27:18 +02:00
Krafting
bb655cb719 Add spaces to privacy-policy.md 2024-04-01 14:19:13 +02:00
Lei Nelissen
be0c7002ff chore: also release v2.3.2 for Android 2024-03-10 23:12:10 +01:00
dependabot[bot]
e472d043cf chore(deps): bump ip from 1.1.8 to 1.1.9
Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9.
- [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-10 22:09:29 +00:00
Lei Nelissen
366d16c485 chore: release v2.3.2 2024-03-10 23:08:34 +01:00
Lei Nelissen
845eac70a0 fix: build with xcode 15.3 2024-03-10 22:54:02 +01:00
Lei Nelissen
c9662769fa fix: remove conflicting app transport properties 2024-03-10 22:53:46 +01:00
91 changed files with 3221 additions and 2175 deletions

View File

@@ -58,7 +58,8 @@ module.exports = {
{ {
ignoreProps: true ignoreProps: true
} }
] ],
'react/react-in-jsx-scope': 'off',
}, },
settings: { settings: {
react: { react: {

View File

@@ -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) # [2.3.0](https://github.com/leinelissen/jellyfin-audio-player/compare/v2.2.0...v2.3.0) (2024-02-11)

View File

@@ -85,8 +85,8 @@ android {
applicationId "nl.moeilijkedingen.jellyfinaudioplayer" applicationId "nl.moeilijkedingen.jellyfinaudioplayer"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 26 versionCode 29
versionName "2.3.1" versionName "2.3.3"
} }
signingConfigs { signingConfigs {
@@ -126,7 +126,6 @@ android {
dependencies { dependencies {
// The version of react-native is set by the React Native Gradle Plugin // The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android") implementation("com.facebook.react:react-android")
implementation("com.facebook.react:flipper-integration")
if (hermesEnabled.toBoolean()) { if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android") implementation("com.facebook.react:hermes-android")

View File

@@ -5,6 +5,8 @@ import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate import com.facebook.react.defaults.DefaultReactActivityDelegate
import android.os.Bundle;
class MainActivity : ReactActivity() { class MainActivity : ReactActivity() {
/** /**
@@ -19,4 +21,8 @@ class MainActivity : ReactActivity() {
*/ */
override fun createReactActivityDelegate(): ReactActivityDelegate = override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
} }

View File

@@ -9,7 +9,6 @@ import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.flipper.ReactNativeFlipper
import com.facebook.soloader.SoLoader import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication { class MainApplication : Application(), ReactApplication {
@@ -31,7 +30,7 @@ class MainApplication : Application(), ReactApplication {
} }
override val reactHost: ReactHost override val reactHost: ReactHost
get() = getDefaultReactHost(this.applicationContext, reactNativeHost) get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() { override fun onCreate() {
super.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. // If you opted-in for the New Architecture, we load the native entry point for this app.
load() load()
} }
ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager)
} }
} }

View File

@@ -5,11 +5,11 @@ import org.apache.tools.ant.taskdefs.condition.Os
buildscript { buildscript {
ext { ext {
buildToolsVersion = "34.0.0" buildToolsVersion = "34.0.0"
minSdkVersion = 21 minSdkVersion = 23
compileSdkVersion = 34 compileSdkVersion = 34
targetSdkVersion = 34 targetSdkVersion = 34
ndkVersion = "25.1.8937393" ndkVersion = "26.1.10909125"
kotlinVersion = "1.8.0" kotlinVersion = "1.9.22"
} }
repositories { repositories {
google() google()

View File

@@ -38,6 +38,4 @@ newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine. # Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead. # If set to false, you will be using JSC instead.
hermesEnabled=true hermesEnabled=true
FLIPPER_VERSION=0.201.0

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

14
android/gradlew vendored
View File

@@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

20
android/gradlew.bat vendored
View File

@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@@ -1,10 +1,10 @@
Privacy policy for Fintunes 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. 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, 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 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 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 controlled by the first party. No third parties can access this data in any
form. No personal data is included in the analytics data. form. No personal data is included in the analytics data.

View File

@@ -1,5 +1,6 @@
package_name("nl.moeilijkedingen.jellyfinaudioplayer") package_name("nl.moeilijkedingen.jellyfinaudioplayer")
app_identifier("nl.moeilijkedingen.jellyfinaudioplayer") app_identifier("nl.moeilijkedingen.jellyfinaudioplayer")
apple_id("lei@moeilijkedingen.nl") apple_id("lei@codified.nl")
team_id("238P3C58WC") team_id("HD2D35G9Y4")
json_key_file("./fastlane/play-store-credentials.json") json_key_file("./fastlane/play-store-credentials.json")
itc_team_id("127114471")

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

View File

@@ -0,0 +1 @@
Strumieniowy odtwarzacz audio dla Jellyfin

View File

@@ -0,0 +1 @@
Fintunes

View File

@@ -1 +1 @@
jellyfin, audio, odtwarzacz, przesyłanie strumieniowe, pobieranie, muzyka jellyfin, audio, odtwarzacz, streaming, pobrane, muzyka

View File

@@ -7,16 +7,17 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 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 */; }; 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 */; }; 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 */; }; AB393FCA2857CC8400773469 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB393FC92857CC8400773469 /* SnapshotHelper.swift */; };
AB4A8DFE2857C8DA005A1ED0 /* FintunesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB4A8DFD2857C8DA005A1ED0 /* FintunesUITests.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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -38,8 +39,6 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
00E356EE1AD99517003FC87E /* FintunesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FintunesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; };
7D43C7610851B9666193E3F6 /* libPods-Fintunes-tvOSTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-tvOSTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 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>"; };
8EBC468D2DE6EB8FF02B72B7 /* libPods-Fintunes-FintunesTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-FintunesTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
AB393FC92857CC8400773469 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; 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; }; 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>"; }; 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>"; }; 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>"; };
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>"; }; E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes.a"; sourceTree = BUILT_PRODUCTS_DIR; };
E35451F7979C52C1692C4C9F /* libPods-Fintunes-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Fintunes-tvOS.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; }; 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; }; 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 */ /* End PBXFileReference section */
@@ -68,7 +68,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
38B3606A2D29107567360ACF /* libPods-Fintunes-FintunesTests.a in Frameworks */, 96A76B2DA812E1F2E353959C /* libPods-Fintunes-FintunesTests.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -76,7 +76,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
D7439709FB704B4FE23C538F /* libPods-Fintunes.a in Frameworks */, 01DDB50991998A6D20A1A5CD /* libPods-Fintunes.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -101,6 +101,7 @@
13B07FB71A68108700A75B9A /* main.m */, 13B07FB71A68108700A75B9A /* main.m */,
4FA1B23C2550A94C007A035E /* File.swift */, 4FA1B23C2550A94C007A035E /* File.swift */,
4FA1B23B2550A94B007A035E /* Fintunes-Bridging-Header.h */, 4FA1B23B2550A94B007A035E /* Fintunes-Bridging-Header.h */,
3896494129CBC30258D9BB1C /* PrivacyInfo.xcprivacy */,
); );
name = Fintunes; name = Fintunes;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -110,10 +111,8 @@
children = ( children = (
ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */,
ED2971642150620600B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */,
E35451F7979C52C1692C4C9F /* libPods-Fintunes-tvOS.a */, E22EC545298DA9F9017776C0 /* libPods-Fintunes.a */,
7D43C7610851B9666193E3F6 /* libPods-Fintunes-tvOSTests.a */, 406012091F4F831F72DFB5D2 /* libPods-Fintunes-FintunesTests.a */,
12335560B9820FD5AD98AB8F /* libPods-Fintunes.a */,
8EBC468D2DE6EB8FF02B72B7 /* libPods-Fintunes-FintunesTests.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -121,10 +120,10 @@
46001D7383D71A837AAF6E07 /* Pods */ = { 46001D7383D71A837AAF6E07 /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5370B45C5DDCD952C6569B8D /* Pods-Fintunes.debug.xcconfig */, 55063C1C8FC150384B504BD6 /* Pods-Fintunes.debug.xcconfig */,
0973197F4BDB99413C326AD0 /* Pods-Fintunes.release.xcconfig */, 5892110C5BD456492E65B0FC /* Pods-Fintunes.release.xcconfig */,
B20CBCFF11E124551F286B84 /* Pods-Fintunes-FintunesTests.debug.xcconfig */, E9A22426CA08309D7A874468 /* Pods-Fintunes-FintunesTests.debug.xcconfig */,
AFAE700A256C6B0ED0D20FE3 /* Pods-Fintunes-FintunesTests.release.xcconfig */, BB181C2EAAC2E99F00A27B5F /* Pods-Fintunes-FintunesTests.release.xcconfig */,
); );
path = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -186,12 +185,12 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "FintunesTests" */; buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "FintunesTests" */;
buildPhases = ( buildPhases = (
BBD71961640F29097BE9932A /* [CP] Check Pods Manifest.lock */, 4C89666008ED1AECA3700F1B /* [CP] Check Pods Manifest.lock */,
00E356EA1AD99517003FC87E /* Sources */, 00E356EA1AD99517003FC87E /* Sources */,
00E356EB1AD99517003FC87E /* Frameworks */, 00E356EB1AD99517003FC87E /* Frameworks */,
00E356EC1AD99517003FC87E /* Resources */, 00E356EC1AD99517003FC87E /* Resources */,
BDE784ECF29EF861DBFF49D7 /* [CP] Copy Pods Resources */, 105D0C84EA50AA33C7A575A2 /* [CP] Embed Pods Frameworks */,
A02366876E56A727F566EC3A /* [CP] Embed Pods Frameworks */, D9A3A76E8CEBB2167393A14A /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -207,13 +206,13 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Fintunes" */; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Fintunes" */;
buildPhases = ( buildPhases = (
E68FAF43791AC236CF4BF8CB /* [CP] Check Pods Manifest.lock */, 3A2533CC05843338D35BF11A /* [CP] Check Pods Manifest.lock */,
13B07F871A680F5B00A75B9A /* Sources */, 13B07F871A680F5B00A75B9A /* Sources */,
13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8C1A680F5B00A75B9A /* Frameworks */,
13B07F8E1A680F5B00A75B9A /* Resources */, 13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
B9FB8FC65CEFF9AFAC71127E /* [CP] Copy Pods Resources */, 062FA28CBD13CA4E60734D70 /* [CP] Embed Pods Frameworks */,
2917566AA57EE087FC9FCCE9 /* [CP] Embed Pods Frameworks */, EDBDE27F51B5399CA455AD4D /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -253,19 +252,19 @@
TargetAttributes = { TargetAttributes = {
00E356ED1AD99517003FC87E = { 00E356ED1AD99517003FC87E = {
CreatedOnToolsVersion = 6.2; CreatedOnToolsVersion = 6.2;
DevelopmentTeam = 238P3C58WC; DevelopmentTeam = HD2D35G9Y4;
ProvisioningStyle = Manual; ProvisioningStyle = Manual;
TestTargetID = 13B07F861A680F5B00A75B9A; TestTargetID = 13B07F861A680F5B00A75B9A;
}; };
13B07F861A680F5B00A75B9A = { 13B07F861A680F5B00A75B9A = {
DevelopmentTeam = 238P3C58WC; DevelopmentTeam = HD2D35G9Y4;
LastSwiftMigration = 1210; LastSwiftMigration = 1210;
ProvisioningStyle = Manual; ProvisioningStyle = Automatic;
}; };
AB4A8DFA2857C8DA005A1ED0 = { AB4A8DFA2857C8DA005A1ED0 = {
CreatedOnToolsVersion = 13.4.1; CreatedOnToolsVersion = 13.4.1;
DevelopmentTeam = 238P3C58WC; DevelopmentTeam = HD2D35G9Y4;
ProvisioningStyle = Manual; ProvisioningStyle = Automatic;
TestTargetID = 13B07F861A680F5B00A75B9A; TestTargetID = 13B07F861A680F5B00A75B9A;
}; };
}; };
@@ -305,6 +304,7 @@
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */, 4C04FC6E055249ABB204D3BC /* Inter-VariableFont_slnt,wght.ttf in Resources */,
FA01635F2599C28FC19F2EC3 /* PrivacyInfo.xcprivacy in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -332,25 +332,19 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh", "${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputPaths = ( outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -358,23 +352,17 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
A02366876E56A727F566EC3A /* [CP] Embed Pods Frameworks */ = { 105D0C84EA50AA33C7A575A2 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-frameworks.sh", "${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-DoubleConversion/double-conversion.framework/double-conversion",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/Flipper-Glog/glog.framework/glog",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL-Universal/OpenSSL.framework/OpenSSL",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
); );
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputPaths = ( outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/double-conversion.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/glog.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -382,27 +370,29 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
B9FB8FC65CEFF9AFAC71127E /* [CP] Copy Pods Resources */ = { 3A2533CC05843338D35BF11A /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-resources.sh", );
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", inputPaths = (
"${PODS_ROOT}/Sentry/Sources/Resources/PrivacyInfo.xcprivacy", "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
); );
name = "[CP] Copy Pods Resources";
outputPaths = ( outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", "$(DERIVED_FILE_DIR)/Pods-Fintunes-checkManifestLockResult.txt",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/PrivacyInfo.xcprivacy",
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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; showEnvVarsInLog = 0;
}; };
BBD71961640F29097BE9932A /* [CP] Check Pods Manifest.lock */ = { 4C89666008ED1AECA3700F1B /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -424,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"; 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; showEnvVarsInLog = 0;
}; };
BDE784ECF29EF861DBFF49D7 /* [CP] Copy Pods Resources */ = { D9A3A76E8CEBB2167393A14A /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -432,38 +422,36 @@
inputPaths = ( inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-resources.sh", "${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", "${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"; name = "[CP] Copy Pods Resources";
outputPaths = ( outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", "${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; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Fintunes-FintunesTests/Pods-Fintunes-FintunesTests-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
E68FAF43791AC236CF4BF8CB /* [CP] Check Pods Manifest.lock */ = { EDBDE27F51B5399CA455AD4D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = (
);
inputPaths = ( inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Target Support Files/Pods-Fintunes/Pods-Fintunes-resources.sh",
"${PODS_ROOT}/Manifest.lock", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle",
); "${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
); );
name = "[CP] Copy Pods Resources";
outputPaths = ( 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; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; 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; showEnvVarsInLog = 0;
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@@ -525,12 +513,12 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
00E356F61AD99517003FC87E /* Debug */ = { 00E356F61AD99517003FC87E /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = B20CBCFF11E124551F286B84 /* Pods-Fintunes-FintunesTests.debug.xcconfig */; baseConfigurationReference = E9A22426CA08309D7A874468 /* Pods-Fintunes-FintunesTests.debug.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = HD2D35G9Y4;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1", "DEBUG=1",
"$(inherited)", "$(inherited)",
@@ -555,13 +543,13 @@
}; };
00E356F71AD99517003FC87E /* Release */ = { 00E356F71AD99517003FC87E /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = AFAE700A256C6B0ED0D20FE3 /* Pods-Fintunes-FintunesTests.release.xcconfig */; baseConfigurationReference = BB181C2EAAC2E99F00A27B5F /* Pods-Fintunes-FintunesTests.release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = HD2D35G9Y4;
INFOPLIST_FILE = FintunesTests/Info.plist; INFOPLIST_FILE = FintunesTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
@@ -584,15 +572,14 @@
}; };
13B07F941A680F5B00A75B9A /* Debug */ = { 13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 5370B45C5DDCD952C6569B8D /* Pods-Fintunes.debug.xcconfig */; baseConfigurationReference = 55063C1C8FC150384B504BD6 /* Pods-Fintunes.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = 94;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = HD2D35G9Y4;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = ( GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)", "$(inherited)",
@@ -613,7 +600,6 @@
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer; PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer;
PRODUCT_NAME = Fintunes; PRODUCT_NAME = Fintunes;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1707846041";
SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -623,16 +609,16 @@
}; };
13B07F951A680F5B00A75B9A /* Release */ = { 13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 0973197F4BDB99413C326AD0 /* Pods-Fintunes.release.xcconfig */; baseConfigurationReference = 5892110C5BD456492E65B0FC /* Pods-Fintunes.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = 94;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = HD2D35G9Y4;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 238P3C58WC; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = HD2D35G9Y4;
INFOPLIST_FILE = Fintunes/Info.plist; INFOPLIST_FILE = Fintunes/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
@@ -649,7 +635,7 @@
PRODUCT_NAME = Fintunes; PRODUCT_NAME = Fintunes;
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb"; PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
PROVISIONING_PROFILE_SPECIFIER = ""; 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_OBJC_BRIDGING_HEADER = "Fintunes-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -660,6 +646,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CC = "";
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -687,6 +674,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CXX = "";
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
@@ -707,6 +695,8 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD = "";
LDPLUSPLUS = "";
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/usr/lib/swift",
@@ -729,6 +719,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
CC = "";
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_CXX_LANGUAGE_STANDARD = "c++20"; CLANG_CXX_LANGUAGE_STANDARD = "c++20";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -756,6 +747,7 @@
CODE_SIGN_IDENTITY = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)"; CODE_SIGN_IDENTITY = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution: Bureau Moeilijke Dingen BV (238P3C58WC)";
COPY_PHASE_STRIP = YES; COPY_PHASE_STRIP = YES;
CXX = "";
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
@@ -772,6 +764,8 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD = "";
LDPLUSPLUS = "";
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/usr/lib/swift",
@@ -800,10 +794,11 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = 94;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = HD2D35G9Y4;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.5; IPHONEOS_DEPLOYMENT_TARGET = 15.5;
@@ -813,6 +808,7 @@
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests; PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb"; PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -832,11 +828,12 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Manual; CODE_SIGN_STYLE = Manual;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 83; CURRENT_PROJECT_VERSION = 94;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 238P3C58WC; DEVELOPMENT_TEAM = HD2D35G9Y4;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.5; IPHONEOS_DEPLOYMENT_TARGET = 15.5;
@@ -845,6 +842,7 @@
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests; PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.FintunesUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb"; PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@@ -55,7 +55,7 @@
</Testables> </Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Release" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0" launchStyle = "0"

View File

@@ -16,10 +16,10 @@
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{ {
return [self getBundleURL]; return [self bundleURL];
} }
- (NSURL *)getBundleURL - (NSURL *)bundleURL
{ {
#if DEBUG #if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];

View File

@@ -17,19 +17,19 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.3.1</string> <string>2.4.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>83</string> <string>94</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string></string>
@@ -42,11 +42,13 @@
<string>audio</string> <string>audio</string>
<string>fetch</string> <string>fetch</string>
</array> </array>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>armv7</string> <string>arm64</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
@@ -54,13 +56,9 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UISupportsDocumentBrowser</key>
<true/>
</dict> </dict>
</plist> </plist>

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

View File

@@ -8,17 +8,6 @@ require Pod::Executable.execute_command('node', ['-p',
platform :ios, min_ios_version_supported platform :ios, min_ios_version_supported
prepare_react_native_project! 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'] linkage = ENV['USE_FRAMEWORKS']
if linkage != nil if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
@@ -30,11 +19,6 @@ target 'Fintunes' do
use_react_native!( use_react_native!(
:path => config[:reactNativePath], :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. # An absolute path to your application root.
:app_path => "#{Pod::Config.instance.installation_root}/.." :app_path => "#{Pod::Config.instance.installation_root}/.."
) )
@@ -49,7 +33,8 @@ target 'Fintunes' do
react_native_post_install( react_native_post_install(
installer, installer,
config[:reactNativePath], config[:reactNativePath],
:mac_catalyst_enabled => false :mac_catalyst_enabled => false,
# :ccache_enabled => true
) )
end end
end end

File diff suppressed because it is too large Load Diff

1676
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "fintunes", "name": "fintunes",
"version": "2.3.1", "version": "2.4.0",
"main": "src/index.js", "main": "src/index.js",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -30,7 +30,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "^18.2.0", "react": "^18.2.0",
"react-airplay": "^1.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-accessibility-settings": "^0.1.2",
"react-native-collapsible": "^1.6.1", "react-native-collapsible": "^1.6.1",
"react-native-dotenv": "^3.4.9", "react-native-dotenv": "^3.4.9",
@@ -39,8 +39,8 @@
"react-native-gesture-handler": "^2.15.0", "react-native-gesture-handler": "^2.15.0",
"react-native-localize": "^3.0.6", "react-native-localize": "^3.0.6",
"react-native-modal-datetime-picker": "^17.1.0", "react-native-modal-datetime-picker": "^17.1.0",
"react-native-reanimated": "^3.7.0", "react-native-reanimated": "^3.14.0",
"react-native-safe-area-context": "^4.9.0", "react-native-safe-area-context": "^4.10.8",
"react-native-screens": "^3.29.0", "react-native-screens": "^3.29.0",
"react-native-shadow-2": "^7.0.8", "react-native-shadow-2": "^7.0.8",
"react-native-svg": "^14.1.0", "react-native-svg": "^14.1.0",
@@ -48,24 +48,21 @@
"react-native-webview": "^13.7.1", "react-native-webview": "^13.7.1",
"react-redux": "^9.1.0", "react-redux": "^9.1.0",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-flipper": "^2.0.2",
"redux-logger": "^3.0.6",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"styled-components": "^6.1.8" "styled-components": "^6.1.8"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.2", "@babel/core": "^7.20.2",
"@babel/runtime": "^7.20.1", "@babel/runtime": "^7.20.1",
"@react-native/babel-preset": "^0.73.21", "@react-native/babel-preset": "0.74.85",
"@react-native/metro-config": "^0.73.5", "@react-native/metro-config": "0.74.85",
"@react-native/typescript-config": "^0.74.0", "@react-native/typescript-config": "0.74.85",
"@sentry/cli": "^2.28.0", "@sentry/cli": "^2.33.0",
"@sentry/react-native": "^5.18.0", "@sentry/react-native": "^5.26.0",
"@types/i18n-js": "^3.8.9", "@types/i18n-js": "^3.8.9",
"@types/lodash": "^4.14.202", "@types/lodash": "^4.14.202",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/react": "^18.2.55", "@types/react": "^18.2.55",
"@types/redux-logger": "^3.0.13",
"@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0", "@typescript-eslint/parser": "^6.21.0",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "^5.0.0",
@@ -74,7 +71,6 @@
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"metro-react-native-babel-transformer": "^0.77.0", "metro-react-native-babel-transformer": "^0.77.0",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react-native-flipper": "^0.212.0",
"react-native-svg-transformer": "^1.3.0", "react-native-svg-transformer": "^1.3.0",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.3.3" "typescript": "^5.3.3"

View File

@@ -0,0 +1,13 @@
diff --git a/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m b/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m
index c139440..4ff3362 100644
--- a/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m
+++ b/node_modules/@react-native-community/datetimepicker/ios/RNDateTimePickerShadowView.m
@@ -41,7 +41,7 @@
YGNodeMarkDirty(self.yogaNode);
}
-static YGSize RNDateTimePickerShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
+static YGSize RNDateTimePickerShadowViewMeasure(YGNodeConstRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
{
RNDateTimePickerShadowView *shadowPickerView = (__bridge RNDateTimePickerShadowView *)YGNodeGetContext(node);

View File

@@ -4,12 +4,4 @@ module.exports = {
android: {} android: {}
}, },
assets: ['./src/assets/fonts/'], 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,
},
},
}
}; };

View 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

View 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

View File

@@ -3,16 +3,14 @@ import { Provider } from 'react-redux';
import TrackPlayer, { Capability } from 'react-native-track-player'; import TrackPlayer, { Capability } from 'react-native-track-player';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
import Routes from '../screens'; import Routes from '../screens';
import store, { persistedStore, useTypedSelector } from '@/store'; import store, { persistedStore } from '@/store';
import { import {
NavigationContainer, NavigationContainer,
DefaultTheme, DefaultTheme,
DarkTheme as BaseDarkTheme, DarkTheme as BaseDarkTheme,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { ColorSchemeProvider, themes } from './Colors'; import { ColorSchemeProvider, themes, useUserOrSystemScheme } from './Colors';
import DownloadManager from './DownloadManager'; import DownloadManager from './DownloadManager';
import { useColorScheme } from 'react-native';
import { ColorScheme } from '@/store/settings/types';
const LightTheme = { const LightTheme = {
...DefaultTheme, ...DefaultTheme,
@@ -35,9 +33,7 @@ const DarkTheme = {
* right theme is selected based on OS color scheme settings along with user preferences. * right theme is selected based on OS color scheme settings along with user preferences.
*/ */
function ThemedNavigationContainer({ children }: PropsWithChildren<{}>) { function ThemedNavigationContainer({ children }: PropsWithChildren<{}>) {
const systemScheme = useColorScheme(); const scheme = useUserOrSystemScheme();
const userScheme = useTypedSelector((state) => state.settings.colorScheme);
const scheme = userScheme === ColorScheme.System ? systemScheme : userScheme;
return ( return (
<NavigationContainer <NavigationContainer

View File

@@ -62,6 +62,9 @@ function generateStyles(scheme: ColorSchemeName, highContrast: boolean) {
backgroundColor: scheme === 'dark' ? '#191919' : '#f3f3f3', backgroundColor: scheme === 'dark' ? '#191919' : '#f3f3f3',
color: scheme === 'dark' ? '#fff' : '#000', color: scheme === 'dark' ? '#fff' : '#000',
}, },
trackBackground: {
backgroundColor: scheme === 'dark' ? '#111' : '#fff',
},
stackHeader: { stackHeader: {
color: scheme === 'dark' ? 'white' : 'black' 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 // Create context for supplying the theming information
export const ColorSchemeContext = React.createContext(themes.dark); 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. * This provider contains the logic for settings the right theme on the ColorSchemeContext.
*/ */
export function ColorSchemeProvider({ children }: PropsWithChildren<{}>) { export function ColorSchemeProvider({ children }: PropsWithChildren<{}>) {
const systemScheme = useColorScheme();
const highContrast = useAccessibilitySetting('darkerSystemColors'); const highContrast = useAccessibilitySetting('darkerSystemColors');
const userScheme = useTypedSelector((state) => state.settings.colorScheme); const scheme = useUserOrSystemScheme();
const scheme = userScheme === ColorScheme.System ? systemScheme : userScheme;
const theme = highContrast const theme = highContrast
? themes[`${scheme || 'light'}-highcontrast`] ? themes[`${scheme || 'light'}-highcontrast`]
: themes[scheme || 'light']; : themes[scheme || 'light'];

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'; 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 { 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'; import styled from 'styled-components/native';
const Screen = Dimensions.get('screen'); const Screen = Dimensions.get('screen');
@@ -45,10 +45,13 @@ function CoverImage({
src, src,
}: Props) { }: Props) {
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const colorScheme = useColorScheme(); const colorScheme = useUserOrSystemScheme();
const image = useImage(src || null); 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 { canvasSize, imageSize } = useMemo(() => {
const imageSize = Screen.width - margin; const imageSize = Screen.width - margin;
const canvasSize = imageSize + blurRadius * 2; const canvasSize = imageSize + blurRadius * 2;

View File

@@ -20,8 +20,8 @@ export function calculateProgressTranslation(
const completion = position / reference; const completion = position / reference;
// GUARD: Check whether the calculated number is valid and not infinite // GUARD: Check whether the calculated number is valid and not infinite
if (Number.isNaN(completion) || !Number.isFinite(completion)) { if (Number.isNaN(completion) || !Number.isFinite(completion) || !width) {
return 0; return -1_000;
} }
const output = (1 - completion) * -1 * width; const output = (1 - completion) * -1 * width;
@@ -29,6 +29,7 @@ export function calculateProgressTranslation(
return output; return output;
} }
// Progress track did not show up on Lyrics screen if min height is not set
export const ProgressTrackContainer = styled.View` export const ProgressTrackContainer = styled.View`
overflow: hidden; overflow: hidden;
height: 5px; height: 5px;
@@ -37,6 +38,7 @@ export const ProgressTrackContainer = styled.View`
align-items: center; align-items: center;
position: relative; position: relative;
border-radius: 6px; border-radius: 6px;
min-height: 5px;
`; `;
export interface ProgressTrackProps { export interface ProgressTrackProps {

View File

@@ -74,5 +74,6 @@
"sleep-timer": "Таймер за заспиване", "sleep-timer": "Таймер за заспиване",
"confirm-delete-all-tracks": "Сигурни ли сте, че искате да изтриете всички текущо изтеглени песни?", "confirm-delete-all-tracks": "Сигурни ли сте, че искате да изтриете всички текущо изтеглени песни?",
"cancel": "Отказ", "cancel": "Отказ",
"delete": "Изтрий" "delete": "Изтрий",
"disc": "диск"
} }

View File

@@ -26,7 +26,7 @@
"streaming": "Transmissió", "streaming": "Transmissió",
"total-duration": "Durada total", "total-duration": "Durada total",
"similar-albums": "Àlbums similars", "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": "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-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", "color-scheme-system": "Sistema",
@@ -63,10 +63,10 @@
"setting-cache-description": "Si heu actualitzat la vostra biblioteca de Jellyfin, però laplicació està subjectant a actius amagats, podeu esborrar la memòria cau amb aquest botó. Això obligarà laplicació a fer una cerca de la biblioteca des de zero.", "setting-cache-description": "Si heu actualitzat la vostra biblioteca de Jellyfin, però laplicació està subjectant a actius amagats, podeu esborrar la memòria cau amb aquest botó. Això obligarà laplicació a fer una cerca de la biblioteca des de zero.",
"recent-albums": "Afegit Recentment", "recent-albums": "Afegit Recentment",
"error-reporting": "Informe d'errors", "error-reporting": "Informe d'errors",
"error-reporting-description": "Durant l'ús d'aquesta aplicació, es poden trobar errors. Lestudi daquests errors ajuda a crear una experiència daplicació més segura i estable.", "error-reporting-description": "Durant l'ús d'aquesta aplicació, es poden trobar errors. Laplicació é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.", "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": "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 laplicació sigui més estable i robusta, millorant així lexperiència de laplicació 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": "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.", "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 semmagatzemen les dades?", "where-is-data-stored": "On semmagatzemen les dades?",
@@ -74,5 +74,6 @@
"sleep-timer": "Temporitzador son", "sleep-timer": "Temporitzador son",
"confirm-delete-all-tracks": "Estàs segur que vols eliminar totes les pistes descarregades actualment?", "confirm-delete-all-tracks": "Estàs segur que vols eliminar totes les pistes descarregades actualment?",
"delete": "Esborrar", "delete": "Esborrar",
"cancel": "Cancel·lar" "cancel": "Cancel·lar",
"disc": "Disc"
} }

View File

@@ -29,7 +29,7 @@
"set-jellyfin-server": "Nastavit Jellyfin Server", "set-jellyfin-server": "Nastavit Jellyfin Server",
"similar-albums": "Podobná alba", "similar-albums": "Podobná alba",
"albums": "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.", "why-use-tracking-description": "Sledování pomáhá urychlit vývoj to aplikace hlášením podivných okrajových chyb a přehlédnutí. To pomáhá, aby byla aplikace stabilnější a robustnější, a tím se zlepšuje zážitek z aplikace pro každého.",
"onboarding-welcome": "Vítejte!", "onboarding-welcome": "Vítejte!",
"reset-cache": "Resetovat mezipaměť", "reset-cache": "Resetovat mezipaměť",
"album": "Album", "album": "Album",
@@ -74,5 +74,6 @@
"sleep-timer": "Časovač spánku", "sleep-timer": "Časovač spánku",
"confirm-delete-all-tracks": "Přejete si skutečně odstranit všechny stažené skladby?", "confirm-delete-all-tracks": "Přejete si skutečně odstranit všechny stažené skladby?",
"delete": "Čeština", "delete": "Čeština",
"cancel": "Hledat" "cancel": "Hledat",
"disc": "Čeština"
} }

View File

@@ -42,7 +42,7 @@
"where-is-data-stored": "Hvor gemmes data?", "where-is-data-stored": "Hvor gemmes data?",
"enable-error-reporting": "Vil du aktivere fejlrapportering?", "enable-error-reporting": "Vil du aktivere fejlrapportering?",
"playlists": "Spillelister", "playlists": "Spillelister",
"playlist": "Spilleliste", "playlist": "Playlist",
"play-playlist": "Playlist", "play-playlist": "Playlist",
"shuffle-playlist": "Shuffle Playlist", "shuffle-playlist": "Shuffle Playlist",
"streaming": "Streaming", "streaming": "Streaming",
@@ -51,8 +51,8 @@
"color-scheme-system": "Systemsystem", "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.", "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", "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-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 server adresse og logge ind på den.", "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-jellyfin-server-instruction": "Indtast venligst din Jellyfin server URL. Sørg for at inkludere protokollen og havnen", "set-jellyfin-server-instruction": "Indtast venligst din Jellyfin server URL. Sørg for at inkludere protokollen og havnen",
"jellyfin-access-token": "Jellyfin Access Token", "jellyfin-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.", "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.",
@@ -66,7 +66,7 @@
"download-playlist": "Download Playlist", "download-playlist": "Download Playlist",
"no-downloads": "Du har endnu ikke downloadet nogen spor", "no-downloads": "Du har endnu ikke downloadet nogen spor",
"delete-playlist": "Slet Playlist", "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", "retry-failed-downloads": "Detaljerede downloads",
"you-are-offline-message": "Du er i øjeblikket offline. Du kan kun afspille tidligere downloadet musik.", "you-are-offline-message": "Du er i øjeblikket offline. Du kan kun afspille tidligere downloadet musik.",
"playing-on": "At spille på", "playing-on": "At spille på",
@@ -74,5 +74,6 @@
"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.", "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", "delete": "Slet",
"cancel": "Annuller", "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"
} }

View File

@@ -41,7 +41,7 @@
"error-reporting-description": "Bei der Nutzung dieser App können Fehler auftreten. Das Melden dieser Fehler trägt dazu bei, ein sichereres und stabileres App-Erlebnis zu schaffen.", "error-reporting-description": "Bei der Nutzung dieser App können Fehler auftreten. Das Melden dieser Fehler trägt dazu bei, ein sichereres und stabileres App-Erlebnis zu schaffen.",
"error-reporting-rationale": "Wenn Sie die Fehlerberichterstattung aktivieren, wird jedes Mal, wenn ein Fehler auftritt, automatisch ein Bericht erstellt und an einen Server gesendet, zusammen mit hilfreichen Debugging-Informationen wie Geräte, Versionen und der spezifische Fehler.", "error-reporting-rationale": "Wenn Sie die Fehlerberichterstattung aktivieren, wird jedes Mal, wenn ein Fehler auftritt, automatisch ein Bericht erstellt und an einen Server gesendet, zusammen mit hilfreichen Debugging-Informationen wie Geräte, Versionen und der spezifische Fehler.",
"why-use-tracking": "Warum werden Tracker verwendet?", "why-use-tracking": "Warum werden Tracker verwendet?",
"why-use-tracking-description": "Die Verfolgung hilft, die Entwicklung für diese App zu beschleunigen, indem sie seltsame Randfälle und Aufsichten meldet. Dies hilft, die App stabiler und robuster zu machen und so das App-Erlebnis für jeden zu verbessern.", "why-use-tracking-description": "Tracker helfen, die Entwicklung dieser App zu beschleunigen, indem merkwürdige Sonderfälle oder übersehende Fehler direkt an uns gemeldet werden können. Dies hilft uns die App stabiler, robuster und nutzerfreundlicher für alle zu machen.",
"what-data-is-gathered": "Welche Daten werden gesammelt?", "what-data-is-gathered": "Welche Daten werden gesammelt?",
"what-data-is-gathered-description": "Wir protokollieren den Fehler, den Gerätetyp, die Betriebssystemversion, die App-Version und die Geräte-ID. In den Fehlerberichten wird kein Anwendungsstatus gesendet. Die Geräte-ID ist ein eindeutiger Hash, der in Ihren Geräteeinstellungen zurückgesetzt werden kann, und wir können aus dieser Kennung keine persönlichen Informationen ableiten.", "what-data-is-gathered-description": "Wir protokollieren den Fehler, den Gerätetyp, die Betriebssystemversion, die App-Version und die Geräte-ID. In den Fehlerberichten wird kein Anwendungsstatus gesendet. Die Geräte-ID ist ein eindeutiger Hash, der in Ihren Geräteeinstellungen zurückgesetzt werden kann, und wir können aus dieser Kennung keine persönlichen Informationen ableiten.",
"where-is-data-stored": "Wo werden die Daten gespeichert?", "where-is-data-stored": "Wo werden die Daten gespeichert?",
@@ -71,8 +71,9 @@
"playback-reporting": "Wiedergabeberichte", "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.", "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", "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?", "confirm-delete-all-tracks": "Sind Sie sicher, dass Sie alle aktuell heruntergeladenen Titel löschen möchten?",
"delete": "Löschen", "delete": "Löschen",
"cancel": "Abbrechen" "cancel": "Abbrechen",
"disc": "Scheiben"
} }

View File

@@ -74,5 +74,11 @@
"privacy-policy": "Privacy Policy", "privacy-policy": "Privacy Policy",
"sleep-timer": "Sleep timer", "sleep-timer": "Sleep timer",
"delete": "Delete", "delete": "Delete",
"cancel": "Cancel" "cancel": "Cancel",
"disc": "Disc",
"lyrics": "Lyrics",
"direct-play": "Direct play",
"transcoded": "Transcoded",
"khz": "kHz",
"kbps": "kbps"
} }

View File

@@ -72,7 +72,8 @@
"color-scheme-system": "Sistema", "color-scheme-system": "Sistema",
"privacy-policy": "Política de privacidad", "privacy-policy": "Política de privacidad",
"sleep-timer": "Temporizador", "sleep-timer": "Temporizador",
"confirm-delete-all-tracks": "¿Estás seguro de que quieres eliminar todas las pistas descargadas actualmente?", "confirm-delete-all-tracks": "¿Estás seguro de que quieres borrar todas las pistas descargadas actualmente?",
"delete": "Suprimir", "delete": "Borrar",
"cancel": "Cancelar" "cancel": "Cancelar",
"disc": "Disco"
} }

View File

@@ -4,7 +4,7 @@
"queue": "File d'attente", "queue": "File d'attente",
"add-to-queue": "Ajouter à la file d'attente", "add-to-queue": "Ajouter à la file d'attente",
"clear-queue": "Vider la file d'attente", "clear-queue": "Vider la file d'attente",
"no-results": "Pas de résultats…", "no-results": "Aucun résultat…",
"album": "Album", "album": "Album",
"albums": "Albums", "albums": "Albums",
"all-albums": "Tous les Albums", "all-albums": "Tous les Albums",
@@ -25,9 +25,9 @@
"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 lintégralité de bibliothèque.", "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 lintégralité de bibliothèque.",
"reset-cache": "Réinitialiser le cache", "reset-cache": "Réinitialiser le cache",
"recent-albums": "Albums récents", "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.", "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.", "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", "more-info": "Plus d'informations",
"play-playlist": "Lire la liste de lecture", "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.", "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", "similar-albums": "Albums similaires",
"playback-reporting": "Rapport de lecture", "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.", "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 remplacer cela pour vous assurer que Fintunes est toujours en mode sombre ou en mode clair.", "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 ?", "why-use-tracking": "Pourquoi utiliser le suivi?",
"where-is-data-stored": "Où sont stockées les données ?", "where-is-data-stored": "Où sont stockées les données?",
"enable-error-reporting": "Voulez-vous activer le rapport d'erreur ?", "enable-error-reporting": "Voulez-vous activer le rapport d'erreur?",
"local-playback": "Lecture locale", "local-playback": "Lecture locale",
"streaming": "Streaming", "streaming": "Streaming",
"color-scheme": "Schéma de couleur", "color-scheme": "Schéma de couleur",
"color-scheme-system": "Système", "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-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", "enable": "Activer",
"disable": "Désactiver", "disable": "Désactiver",
@@ -68,11 +68,12 @@
"playlists": "Listes de lecture", "playlists": "Listes de lecture",
"downloads": "Téléchargements", "downloads": "Téléchargements",
"track": "Piste", "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", "error-reporting": "Rapport d'erreur",
"privacy-policy": "Politique de confidentialité", "privacy-policy": "Politique de confidentialité",
"sleep-timer": "Temporaire de sommeil", "sleep-timer": "Minuterie de veille",
"confirm-delete-all-tracks": "Êtes-vous sûr de vouloir supprimer toutes les pistes actuellement téléchargées?", "confirm-delete-all-tracks": "Êtes-vous sûr de vouloir supprimer toutes les pistes actuellement téléchargées?",
"delete": "Supprimer", "delete": "Supprimer",
"cancel": "Annuler" "cancel": "Annuler",
"disc": "Disc"
} }

View File

@@ -35,7 +35,7 @@
"error-reporting": "Segnalazione Errori", "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-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.", "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": "Il monitoraggio aiuta ad accelerare lo sviluppo di questa app segnalando casi strani e sviste. Ciò ci aiuta a rendere l'app più stabile e robusta, aumentando così l'esperienza dell'app per tutti.",
"what-data-is-gathered": "Quali dati vengono raccolti?", "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.", "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?", "where-is-data-stored": "Dove vengono archiviati i dati?",
@@ -74,5 +74,6 @@
"sleep-timer": "Tempo di sonno", "sleep-timer": "Tempo di sonno",
"delete": "Cancella", "delete": "Cancella",
"cancel": "Annulla", "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"
} }

View File

@@ -29,7 +29,7 @@
"error-reporting-description": "このアプリを使用中に、エラーの可能性があります。これらのエラーを報告することで、よりセキュアで安定したアプリ体験を実現することができます。", "error-reporting-description": "このアプリを使用中に、エラーの可能性があります。これらのエラーを報告することで、よりセキュアで安定したアプリ体験を実現することができます。",
"error-reporting-rationale": "エラーレポートを有効にすると、エラーが発生するたびにレポートが自動的に作成され、デバイス、バージョン、特定のエラーなど、デバッグに役立つ情報とともにサーバーに送信されます。", "error-reporting-rationale": "エラーレポートを有効にすると、エラーが発生するたびにレポートが自動的に作成され、デバイス、バージョン、特定のエラーなど、デバッグに役立つ情報とともにサーバーに送信されます。",
"why-use-tracking": "なぜトラッキングを使うか?", "why-use-tracking": "なぜトラッキングを使うか?",
"why-use-tracking-description": "トレンドは、奇妙なエッジケースやオーバーサイトを報告することで、このアプリの開発をスピードアップするのに役立ちます。 これにより、アプリがより安定して堅牢になり、誰もがアプリ体験向上させることができます.", "why-use-tracking-description": "トラッキングは、奇妙なエッジケースや見落としを報告することで、このアプリの開発を促進します。これにより、アプリの安定性と堅牢性が向上し、すべての人のアプリ体験向上ます",
"what-data-is-gathered": "どのデータが集まりますか?", "what-data-is-gathered": "どのデータが集まりますか?",
"what-data-is-gathered-description": "エラー、デバイスタイプ、OSバージョン、アプリバージョン、デバイスIDが記録されます。いかなるエラー報告においても、アプリケーションの状態は送信されません。デバイスIDは、デバイスの設定でリセット可能な一意のハッシュであり、この識別子から個人情報を推測することはできません。", "what-data-is-gathered-description": "エラー、デバイスタイプ、OSバージョン、アプリバージョン、デバイスIDが記録されます。いかなるエラー報告においても、アプリケーションの状態は送信されません。デバイスIDは、デバイスの設定でリセット可能な一意のハッシュであり、この識別子から個人情報を推測することはできません。",
"where-is-data-stored": "データはどこに保存されていますか?", "where-is-data-stored": "データはどこに保存されていますか?",
@@ -71,8 +71,9 @@
"local-playback": "ローカル再生", "local-playback": "ローカル再生",
"color-scheme-system": "システム", "color-scheme-system": "システム",
"privacy-policy": "プライバシーポリシー", "privacy-policy": "プライバシーポリシー",
"sleep-timer": "睡眠タイマー", "sleep-timer": "スリープタイマー",
"confirm-delete-all-tracks": "現在ダウンロードしたすべてのトラックを削除したいですか?", "confirm-delete-all-tracks": "ダウンロードしたすべてのトラックを削除したいですか?",
"delete": "削除", "delete": "削除",
"cancel": "キャンセル" "cancel": "キャンセル",
"disc": "ディスク"
} }

View File

@@ -74,5 +74,10 @@
"privacy-policy": "Privacybeleid", "privacy-policy": "Privacybeleid",
"sleep-timer": "Slaaptimer", "sleep-timer": "Slaaptimer",
"delete": "Verwijder", "delete": "Verwijder",
"cancel": "Annuleer" "cancel": "Annuleer",
"disc": "Schijf",
"direct-play": "Direct afgespeeld",
"transcoded": "Getranscodeerd",
"khz": "kHz",
"kbps": "kbps"
} }

View File

@@ -1,78 +1,79 @@
{ {
"onboarding-welcome": "Welcome!", "onboarding-welcome": "Witaj!",
"onboarding-intro": "Fintuny pozwolą ściągać swoją bibliotekę muzyczną z nigdzie, z pełnym wsparciem dla dźwięku i odlewu.", "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-jellyfin-server": "Set Jellyfin Server (ang.)", "set-jellyfin-server": "Ustaw Serwer Jellyfin",
"settings": "Setting", "settings": "Ustawienia",
"reset-cache": "Reset Cache", "reset-cache": "Zresetuj pamięć podręczną",
"color-scheme-light": "Przełomowy", "color-scheme-light": "Jasny motyw",
"color-scheme-dark": "Dark Mode", "color-scheme-dark": "Ciemny motyw",
"play-next": "Playować", "play-next": "Odtwórz następne",
"play-album": "Play Album", "play-album": "Odtwórz Album",
"queue": "Queue", "queue": "Kolejka",
"add-to-queue": "Addd", "add-to-queue": "Dodaj do kolejki",
"clear-queue": "Clear Queue", "clear-queue": "Wyczyść kolejkę",
"no-results": "Nie ma rezultatów.", "no-results": "Brak wyników",
"album": "Albumy", "album": "Album",
"albums": "Album", "albums": "Albumy",
"all-albums": "Wszystkie albumy", "all-albums": "Wszystkie albumy",
"search": "Search", "search": "Szukaj",
"music": "Music", "music": "Muzyka",
"now-playing": "Teraz grasz", "now-playing": "Teraz gra",
"onboarding-cta": "Aby rozpocząć pracę, potrzebuje serwera Jellyfin. Click the button below to enter your Jellyfin server address and login to it.", "onboarding-cta": "Aby rozpocząć pracę, potrzebujesz serwera Jellyfin. Kliknij poniższy przycisk, aby wprowadzić adres serwera Jellyfin i zalogować się do niego.",
"set-jellyfin-server-instruction": "Wstęp do serwera Jellyfin URL. Umożliwia to łączenie protokołu i portu", "set-jellyfin-server-instruction": "Wprowadź adres URL serwera Jellyfin. Upewnij się, że podałeś protokół i port",
"jellyfin-library": "Biblioteka Jellyfin", "jellyfin-library": "Biblioteka Jellyfin",
"jellyfin-server-url": "Jellyfin Server URL", "jellyfin-server-url": "URL do serwera Jellyfin",
"jellyfin-access-token": "Jellyfin Access Token (ang.)", "jellyfin-access-token": "Token dostępowy do Jellyfin",
"jellyfin-user-id": "Jellyfin User ID", "jellyfin-user-id": "ID użytkownika Jellyfin",
"setting-cache": "Kache", "setting-cache": "Pamięć podręczna",
"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.", "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": "Recent Album", "recent-albums": "Ostatnie Albumy",
"error-reporting": "Error Reporting (ang.)", "error-reporting": "Reportowanie błędów",
"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-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": "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.", "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 użyczasz?", "why-use-tracking": "Dlaczego warto korzystać z funkcji śledzenia?",
"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.", "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ą zebrane?", "what-data-is-gathered": "Jakie dane są zbierane?",
"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.", "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 jest przechowywane dane?", "where-is-data-stored": "Gdzie 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.", "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 pozwolić na błędy?", "enable-error-reporting": "Czy chcesz włączyć raportowanie błędów?",
"enable-error-reporting-description": "Umożliwia to poprawę doświadczenia aplikacji poprzez wysłanie raportów zderzenia i 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": "Pełny", "enable": "Włączony",
"disable": "Dyskretny", "disable": "Wyłączony",
"more-info": "Info", "more-info": "Więcej Informacji",
"track": "Track", "track": "Utwór",
"playlists": "Playlista", "playlists": "Listy odtwarzania",
"playlist": "Playlista", "playlist": "Lista odtwarzania",
"play-playlist": "Playlista", "play-playlist": "Odtwórz listę odtwarzania",
"shuffle-album": "Album Shuffle Album", "shuffle-album": "Przemieszaj album",
"shuffle-playlist": "Oficjalna strona Shuffle Play", "shuffle-playlist": "Losuj listę odtwarzania",
"downloads": "Download", "downloads": "Pobrane",
"download-track": "Download Track", "download-track": "Pobrane Utwory",
"download-album": "Download Album", "download-album": "Pobrane Albumy",
"download-playlist": "Download Playlist", "download-playlist": "Pobrane listy odtwarzania",
"no-downloads": "Nie można jeszcze pobrać żadnych utworów", "no-downloads": "Nie pobrałeś jeszcze żadnych utworów",
"delete-track": "Trasa Delete", "delete-track": "Usuń utwór",
"delete-all-tracks": "Delete All Tracks", "delete-all-tracks": "Usuń wszystkie utwory",
"delete-album": "Album Delete", "delete-album": "Usuń album",
"delete-playlist": "Delete Playlista", "delete-playlist": "Usuń listę odtwarzania",
"total-download-size": "Total Download Size", "total-download-size": "Całkowity rozmiar pobierania",
"retry-failed-downloads": "Retry Failed Download (ang.)", "retry-failed-downloads": "Ponów nieudane pobieranie",
"you-are-offline-message": "Obecnie są one offline. Mogą grać tylko wcześniej.", "you-are-offline-message": "Aktualnie jesteś offline. Można odtwarzać tylko wcześniej pobraną muzykę.",
"playing-on": "Zagrać", "playing-on": "Gra dalej",
"local-playback": "Local playback", "local-playback": "Odtwarzanie lokalne",
"streaming": "Streaming (ang.)", "streaming": "Transmisja strumieniowa",
"total-duration": "Czas trwania Totalna", "total-duration": "Całkowity czas trwania",
"similar-albums": "Album", "similar-albums": "Podobne albumy",
"playback-reporting": "Playback Reporting", "playback-reporting": "Raportowanie odtwarzania",
"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.", "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": "Color Scheme", "color-scheme": "Schemat kolorów",
"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.", "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", "color-scheme-system": "System",
"artists": "Artysta", "artists": "Artysta",
"privacy-policy": "Polityka prywatna", "privacy-policy": "Polityka prywatności",
"sleep-timer": "Sleep timer", "sleep-timer": "Timer uśpienia",
"delete": "Delete", "delete": "Usuń",
"cancel": "Cancel", "cancel": "Anuluj",
"confirm-delete-all-tracks": "Czy chcesz usunąć wszystkie aktualne utwory?" "confirm-delete-all-tracks": "Czy na pewno chcesz usunąć wszystkie aktualnie pobrane utwory?",
"disc": "Disc disc"
} }

View File

@@ -38,7 +38,7 @@
"reset-cache": "Redefinir cache", "reset-cache": "Redefinir cache",
"recent-albums": "Álbuns recentes", "recent-albums": "Álbuns recentes",
"error-reporting": "Relatório de erros", "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": "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.", "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.", "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,8 @@
"artists": "Artistas", "artists": "Artistas",
"privacy-policy": "Política de Privacidade", "privacy-policy": "Política de Privacidade",
"sleep-timer": "Temporizador de sono", "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", "delete": "Excluir",
"cancel": "Cancelar" "cancel": "Cancelar",
"disc": "Disco"
} }

View File

@@ -42,7 +42,7 @@
"error-reporting-description": "Во время использования этого приложения вы можете столкнуться с ошибками. Сообщение об этих ошибках помогает сделать приложение более безопасным и стабильным.", "error-reporting-description": "Во время использования этого приложения вы можете столкнуться с ошибками. Сообщение об этих ошибках помогает сделать приложение более безопасным и стабильным.",
"error-reporting-rationale": "Когда вы включаете создание отчетов об ошибках, при каждом возникновении ошибки автоматически создается отчет и отправляется на сервер вместе с полезной отладочной информацией, такой как устройства, версии и конкретная ошибка.", "error-reporting-rationale": "Когда вы включаете создание отчетов об ошибках, при каждом возникновении ошибки автоматически создается отчет и отправляется на сервер вместе с полезной отладочной информацией, такой как устройства, версии и конкретная ошибка.",
"why-use-tracking": "Зачем использовать отслеживание?", "why-use-tracking": "Зачем использовать отслеживание?",
"why-use-tracking-description": "Отслеживание помогает ускорить разработку этого приложения, сообщая о странных случаях и надзорах. Это помогает сделать приложение более стабильным и надежным, тем самым улучшая опыт приложений для всех.", "why-use-tracking-description": "Отслеживание помогает ускорить разработку этого приложения, сообщая о странных случаях и упущениях. Это помогает сделать приложение более стабильным и надежным, тем самым повышая удобство работы с приложением для всех.",
"what-data-is-gathered": "Какие данные собираются?", "what-data-is-gathered": "Какие данные собираются?",
"what-data-is-gathered-description": "Мы собираем ошибки, тип устройства, версию ОС, версию приложения и идентификатор устройства. Ни в одном отчете об ошибках не передается состояние приложения. Идентификатор устройства - это уникальный хэш, который можно сбросить в настройках вашего устройства, и мы не можем извлечь какую-либо личную информацию из этого идентификатора.", "what-data-is-gathered-description": "Мы собираем ошибки, тип устройства, версию ОС, версию приложения и идентификатор устройства. Ни в одном отчете об ошибках не передается состояние приложения. Идентификатор устройства - это уникальный хэш, который можно сбросить в настройках вашего устройства, и мы не можем извлечь какую-либо личную информацию из этого идентификатора.",
"where-is-data-stored": "Где хранятся данные?", "where-is-data-stored": "Где хранятся данные?",
@@ -74,5 +74,6 @@
"sleep-timer": "Время сна", "sleep-timer": "Время сна",
"confirm-delete-all-tracks": "Вы уверены, что хотите удалить все загруженные в настоящее время треки?", "confirm-delete-all-tracks": "Вы уверены, что хотите удалить все загруженные в настоящее время треки?",
"delete": "Удалить", "delete": "Удалить",
"cancel": "Отмена" "cancel": "Отмена",
"disc": "Disc"
} }

View File

@@ -74,5 +74,6 @@
"sleep-timer": "Sova timer", "sleep-timer": "Sova timer",
"delete": "Delete", "delete": "Delete",
"cancel": "Avbokning", "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"
} }

View File

@@ -73,6 +73,7 @@
"privacy-policy": "Політика конфіденційності", "privacy-policy": "Політика конфіденційності",
"sleep-timer": "Таймер сну", "sleep-timer": "Таймер сну",
"confirm-delete-all-tracks": "Ви впевнені, що ви хочете видалити всі завантажені треки?", "confirm-delete-all-tracks": "Ви впевнені, що ви хочете видалити всі завантажені треки?",
"delete": "Делет", "delete": "Видалити",
"cancel": "Зареєструватися" "cancel": "Скасувати",
"disc": "Диски"
} }

View File

@@ -72,7 +72,8 @@
"color-scheme-light": "灯光模式", "color-scheme-light": "灯光模式",
"privacy-policy": "隐私政策", "privacy-policy": "隐私政策",
"sleep-timer": "睡眠定时器", "sleep-timer": "睡眠定时器",
"confirm-delete-all-tracks": "你们是否希望删除目前下载的所有轨道?", "confirm-delete-all-tracks": "您确定要删除所有当前下载的曲目吗?",
"delete": "删除", "delete": "删除",
"cancel": "取消" "cancel": "取消",
"disc": "歧视"
} }

View File

@@ -73,4 +73,10 @@ export type LocaleKeys = 'play-next'
| 'privacy-policy' | 'privacy-policy'
| 'sleep-timer' | 'sleep-timer'
| 'delete' | 'delete'
| 'cancel' | 'cancel'
| 'disc'
| 'lyrics'
| 'direct-play'
| 'transcoded'
| 'khz'
| 'kbps'

View File

@@ -12,7 +12,7 @@ import DownloadIcon from '@/components/DownloadIcon';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import { Text } from '@/components/Typography'; import { Text } from '@/components/Typography';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import { useGetImage } from '@/utility/JellyfinApi'; import { useGetImage } from '@/utility/JellyfinApi/lib';
import { ShadowWrapper } from '@/components/Shadow'; import { ShadowWrapper } from '@/components/Shadow';
import { SafeFlatList } from '@/components/SafeNavigatorView'; import { SafeFlatList } from '@/components/SafeNavigatorView';
import { t } from '@/localisation'; import { t } from '@/localisation';

View File

@@ -17,9 +17,11 @@ import { calculateProgressTranslation } from '@/components/Progresstrack';
import { NavigationProp } from '@/screens/types'; import { NavigationProp } from '@/screens/types';
import { ShadowWrapper } from '@/components/Shadow'; import { ShadowWrapper } from '@/components/Shadow';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const NOW_PLAYING_POPOVER_MARGIN = 6; export 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_WIDTH = Dimensions.get('screen').width - 2 * NOW_PLAYING_POPOVER_MARGIN;
export const NOW_PLAYING_POPOVER_HEIGHT = 58;
const PopoverPosition = css` const PopoverPosition = css`
position: absolute; position: absolute;
@@ -34,6 +36,7 @@ const Container = styled.ScrollView`
`; `;
const InnerContainer = styled.TouchableOpacity` const InnerContainer = styled.TouchableOpacity`
height: ${NOW_PLAYING_POPOVER_HEIGHT}px;
padding: 12px; padding: 12px;
overflow: hidden; overflow: hidden;
flex: 1; 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 { index, track } = useCurrentTrack();
const { buffered, position } = useProgress(); const { buffered, position } = useProgress();
const defaultStyles = useDefaultStyles(); const defaultStyles = useDefaultStyles();
const tabBarHeight = useBottomTabBarHeight(); const tabBarHeight = useBottomTabBarHeight();
const insets = useSafeAreaInsets();
const previousBuffered = usePrevious(buffered); const previousBuffered = usePrevious(buffered);
const previousPosition = usePrevious(position); const previousPosition = usePrevious(position);
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
const bufferAnimation = useRef(new Animated.Value(0)); const bufferAnimation = useRef(new Animated.Value(-1_000));
const progressAnimation = useRef(new Animated.Value(0)); const progressAnimation = useRef(new Animated.Value(-1_000));
const openNowPlayingModal = useCallback(() => { const openNowPlayingModal = useCallback(() => {
navigation.navigate('Player'); navigation.navigate('Player');
@@ -163,7 +167,14 @@ function NowPlaying({ offset = 0 }: { offset?: number }) {
} }
return ( 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 */} {/** TODO: Fix shadow overflow on Android */}
{Platform.OS === 'ios' ? ( {Platform.OS === 'ios' ? (
<ShadowOverlay pointerEvents='none'> <ShadowOverlay pointerEvents='none'>

View File

@@ -9,7 +9,7 @@ import { t } from '@/localisation';
import { NavigationProp, StackParams } from '@/screens/types'; import { NavigationProp, StackParams } from '@/screens/types';
import { SubHeader, Text } from '@/components/Typography'; import { SubHeader, Text } from '@/components/Typography';
import { ScrollView } from 'react-native-gesture-handler'; import { ScrollView } from 'react-native-gesture-handler';
import { useGetImage } from '@/utility/JellyfinApi'; import { useGetImage } from '@/utility/JellyfinApi/lib';
import styled from 'styled-components'; import styled from 'styled-components';
import { Dimensions, Pressable } from 'react-native'; import { Dimensions, Pressable } from 'react-native';
import AlbumImage from './components/AlbumImage'; import AlbumImage from './components/AlbumImage';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react'; 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 { SectionList, View } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, ReactText } from 'react'; import React, { useCallback, useEffect, ReactText } from 'react';
import { useGetImage } from '@/utility/JellyfinApi'; import { useGetImage } from '@/utility/JellyfinApi/lib';
import { View } from 'react-native'; import { View } from 'react-native';
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, useMemo } from 'react'; 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 { SectionList, View } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useRef, ReactText } from 'react'; 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 { Text, View, FlatList, ListRenderItem, RefreshControl } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react'; 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 { Text, SafeAreaView, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from '@/store'; import { useAppDispatch, useTypedSelector } from '@/store';

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import FastImage, { FastImageProps } from 'react-native-fast-image'; 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'); const Screen = Dimensions.get('screen');
export const AlbumWidth = Screen.width / 2 - 24; export const AlbumWidth = Screen.width / 2 - 24;
@@ -23,11 +24,17 @@ const Container = styled(FastImage)`
function AlbumImage(props: FastImageProps) { function AlbumImage(props: FastImageProps) {
const [hasError, setError] = useState(false); const [hasError, setError] = useState(false);
const colorScheme = useColorScheme(); const colorScheme = useUserOrSystemScheme();
if (!props.source || hasError) { if (!props.source || hasError) {
return ( 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')
}
/>
); );
} }

View File

@@ -1,12 +1,11 @@
import React, { PropsWithChildren, useCallback, useMemo } from 'react'; import React, { PropsWithChildren, useCallback, useMemo } from 'react';
import { Platform, RefreshControl, StyleSheet, View } from 'react-native'; 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 styled, { css } from 'styled-components/native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from '@/store'; import { useAppDispatch, useTypedSelector } from '@/store';
import TouchableHandler from '@/components/TouchableHandler'; import TouchableHandler from '@/components/TouchableHandler';
import useCurrentTrack from '@/utility/useCurrentTrack'; import useCurrentTrack from '@/utility/useCurrentTrack';
import TrackPlayer from 'react-native-track-player';
import Play from '@/assets/icons/play.svg'; import Play from '@/assets/icons/play.svg';
import Shuffle from '@/assets/icons/shuffle.svg'; import Shuffle from '@/assets/icons/shuffle.svg';
import useDefaultStyles from '@/components/Colors'; import useDefaultStyles from '@/components/Colors';
@@ -25,14 +24,23 @@ import CoverImage from '@/components/CoverImage';
import ticksToDuration from '@/utility/ticksToDuration'; import ticksToDuration from '@/utility/ticksToDuration';
import { t } from '@/localisation'; import { t } from '@/localisation';
import { SafeScrollView, useNavigationOffsets } from '@/components/SafeNavigatorView'; import { SafeScrollView, useNavigationOffsets } from '@/components/SafeNavigatorView';
import { groupBy } from 'lodash';
import Divider from '@/components/Divider';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
index: { index: {
marginRight: 12 marginRight: 12,
textAlign: 'right',
}, },
activeText: { activeText: {
fontWeight: '500', fontWeight: '500',
}, },
discContainer: {
flexDirection: 'row',
gap: 24,
alignItems: 'center',
marginBottom: 12,
}
}); });
const AlbumImageContainer = styled.View` const AlbumImageContainer = styled.View`
@@ -53,7 +61,7 @@ const TrackContainer = styled.View<{ isPlaying: boolean, small?: boolean }>`
`} `}
${props => props.small && css` ${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) ), 0)
), [trackIds, tracks]); ), [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 // Retrieve helpers
const getImage = useGetImage(); const getImage = useGetImage();
const playTracks = usePlayTracks(); const playTracks = usePlayTracks();
@@ -105,16 +125,34 @@ const TrackListView: React.FC<TrackListViewProps> = ({
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
const dispatch = useAppDispatch(); 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 // Setup callbacks
const playEntity = useCallback(() => { playTracks(trackIds); }, [playTracks, trackIds]); const playEntity = useCallback(() => { playTracks(trackIds); }, [playTracks, trackIds]);
const shuffleEntity = useCallback(() => { playTracks(trackIds, { shuffle: true }); }, [playTracks, trackIds]); const shuffleEntity = useCallback(() => { playTracks(trackIds, { shuffle: true }); }, [playTracks, trackIds]);
const selectTrack = useCallback(async (index: number) => { const selectTrack = useCallback(async (index: number) => {
await playTracks(trackIds, { play: false }); await playTracks(trackIds, { playIndex: index });
await TrackPlayer.skip(index);
await TrackPlayer.play();
}, [playTracks, trackIds]); }, [playTracks, trackIds]);
const longPressTrack = useCallback((index: number) => { const longPressTrack = useCallback((index: number) => {
navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() }); navigation.navigate('TrackPopupMenu', { trackId: trackIds[index].toString() });
}, [navigation, trackIds]); }, [navigation, trackIds]);
const downloadAllTracks = useCallback(() => { const downloadAllTracks = useCallback(() => {
trackIds.forEach((trackId) => dispatch(queueTrackForDownload(trackId))); 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" /> <WrappableButton title={shuffleButtonText} icon={Shuffle} onPress={shuffleEntity} testID="shuffle-album" />
</WrappableButtonRow> </WrappableButtonRow>
<View style={{ marginTop: 8 }}> <View style={{ marginTop: 8 }}>
{trackIds.map((trackId, i) => {trackGroups.map(([discNo, groupTrackIds]) => (
<TouchableHandler <View key={`disc_${discNo}`} style={{ marginBottom: 24 }}>
key={trackId} {trackGroups.length > 1 && (
id={i} <View style={styles.discContainer}>
onPress={selectTrack} <SubHeader>{t('disc')} {discNo}</SubHeader>
onLongPress={longPressTrack} <Divider />
testID={`play-track-${trackId}`} </View>
> )}
<TrackContainer {groupTrackIds.map((trackId, i) =>
isPlaying={currentTrack?.backendId === trackId || false} <TouchableHandler
style={[ key={trackId}
defaultStyles.border, id={i}
currentTrack?.backendId === trackId ? defaultStyles.activeBackground : null onPress={selectTrack}
]} onLongPress={longPressTrack}
> testID={`play-track-${trackId}`}
<Text
style={[
styles.index,
defaultStyles.textQuarterOpacity,
currentTrack?.backendId === trackId && styles.activeText,
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
]}
numberOfLines={1}
> >
{listNumberingStyle === 'index' <TrackContainer
? i + 1 isPlaying={currentTrack?.backendId === trackId || false}
: tracks[trackId]?.IndexNumber}
</Text>
<View style={{ flexShrink: 1 }}>
<Text
style={[ style={[
currentTrack?.backendId === trackId && styles.activeText, defaultStyles.border,
currentTrack?.backendId === trackId && defaultStyles.themeColor, currentTrack?.backendId === trackId ? defaultStyles.activeBackground : null
{
flexShrink: 1,
marginRight: 4,
}
]} ]}
numberOfLines={1}
> >
{tracks[trackId]?.Name}
</Text>
{itemDisplayStyle === 'playlist' && (
<Text <Text
style={[ style={[
styles.index,
defaultStyles.textQuarterOpacity,
currentTrack?.backendId === trackId && styles.activeText, currentTrack?.backendId === trackId && styles.activeText,
currentTrack?.backendId === trackId && defaultStyles.themeColor, currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity,
{ indexWidth,
flexShrink: 1,
marginRight: 4,
opacity: currentTrack?.backendId === trackId ? 0.5 : 0.25,
}
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{tracks[trackId]?.Artists.join(', ')} {listNumberingStyle === 'index'
? i + 1
: tracks[trackId]?.IndexNumber}
</Text> </Text>
)} <View style={{ flexShrink: 1 }}>
</View> <Text
<View style={{ marginLeft: 'auto', flexDirection: 'row' }}> style={[
<Text currentTrack?.backendId === trackId && styles.activeText,
style={[ currentTrack?.backendId === trackId && defaultStyles.themeColor,
{ marginRight: 12 }, {
defaultStyles.textQuarterOpacity, flexShrink: 1,
currentTrack?.backendId === trackId && styles.activeText, marginRight: 4,
currentTrack?.backendId === trackId && defaultStyles.themeColorQuarterOpacity, }
]} ]}
numberOfLines={1} numberOfLines={1}
> >
{ticksToDuration(tracks[trackId]?.RunTimeTicks || 0)} {tracks[trackId]?.Name}
</Text> </Text>
<DownloadIcon {itemDisplayStyle === 'playlist' && (
trackId={trackId} <Text
fill={currentTrack?.backendId === trackId ? defaultStyles.themeColorQuarterOpacity.color : undefined} style={[
/> currentTrack?.backendId === trackId && styles.activeText,
</View> currentTrack?.backendId === trackId && defaultStyles.themeColor,
</TrackContainer> {
</TouchableHandler> 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 }}> <Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}>
{t('total-duration')}{': '}{ticksToDuration(totalDuration)} {t('total-duration')}{': '}{ticksToDuration(totalDuration)}
</Text> </Text>

View File

@@ -8,7 +8,7 @@ import { Album, AlbumTrack } from '@/store/music/types';
import { FlatList } from 'react-native-gesture-handler'; import { FlatList } from 'react-native-gesture-handler';
import TouchableHandler from '@/components/TouchableHandler'; import TouchableHandler from '@/components/TouchableHandler';
import { useNavigation } from '@react-navigation/native'; 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 FastImage from 'react-native-fast-image';
import { t } from '@/localisation'; import { t } from '@/localisation';
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors'; import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet } from 'react-native';
import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator, BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
@@ -11,6 +12,7 @@ import Downloads from './Downloads';
import Onboarding from './Onboarding'; import Onboarding from './Onboarding';
import TrackPopupMenu from './modals/TrackPopupMenu'; import TrackPopupMenu from './modals/TrackPopupMenu';
import SetJellyfinServer from './modals/SetJellyfinServer'; import SetJellyfinServer from './modals/SetJellyfinServer';
import ErrorReportingPopup from './modals/ErrorReportingPopup';
import SearchIcon from '@/assets/icons/magnifying-glass.svg'; import SearchIcon from '@/assets/icons/magnifying-glass.svg';
import NotesIcon from '@/assets/icons/notes.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 { useTypedSelector } from '@/store';
import { t } from '@/localisation'; import { t } from '@/localisation';
import ErrorReportingAlert from '@/utility/ErrorReportingAlert'; 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 useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
import Player from './modals/Player';
import { StackParams } from './types'; import { StackParams } from './types';
import Lyrics from './modals/Lyrics';
const Stack = createNativeStackNavigator<StackParams>(); const Stack = createNativeStackNavigator<StackParams>();
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
@@ -36,7 +37,7 @@ type Screens = {
function Screens() { function Screens() {
const styles = useDefaultStyles(); const styles = useDefaultStyles();
const isOnboardingComplete = useTypedSelector(state => state.settings.isOnboardingComplete); const isOnboardingComplete = useTypedSelector(state => state.settings.isOnboardingComplete);
// GUARD: If onboarding has not been completed, we instead render the // GUARD: If onboarding has not been completed, we instead render the
// onboarding component, so that the user can get setup in the app. // onboarding component, so that the user can get setup in the app.
if (!isOnboardingComplete) { if (!isOnboardingComplete) {
@@ -91,12 +92,16 @@ export default function Routes() {
<Stack.Navigator screenOptions={{ <Stack.Navigator screenOptions={{
presentation: 'modal', presentation: 'modal',
headerShown: false, headerShown: false,
contentStyle: {
backgroundColor: 'transparent'
}
}} id="MAIN"> }} id="MAIN">
<Stack.Screen name="Screens" component={Screens} /> <Stack.Screen name="Screens" component={Screens} />
<Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} /> <Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} />
<Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet' }} /> <Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet' }} />
<Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} /> <Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} />
<Stack.Screen name="Player" component={Player} /> <Stack.Screen name="Player" component={Player} />
<Stack.Screen name="Lyrics" component={Lyrics} />
</Stack.Navigator> </Stack.Navigator>
); );
} }
@@ -104,4 +109,4 @@ export default function Routes() {
export type NavigationProp = CompositeNavigationProp< export type NavigationProp = CompositeNavigationProp<
StackNavigationProp<Routes>, StackNavigationProp<Routes>,
BottomTabNavigationProp<Screens> BottomTabNavigationProp<Screens>
>; >;

View 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);

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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 />
);
}

View File

@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import TrackPlayer, { State, usePlaybackState } from 'react-native-track-player'; 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 styled from 'styled-components/native';
import { useHasNextQueue, useHasPreviousQueue } from '@/utility/useQueue'; import { useHasNextQueue, useHasPreviousQueue } from '@/utility/useQueue';
import ForwardIcon from '@/assets/icons/forward-end.svg'; import ForwardIcon from '@/assets/icons/forward-end.svg';
import BackwardIcon from '@/assets/icons/backward-end.svg'; import BackwardIcon from '@/assets/icons/backward-end.svg';
import PlayIcon from '@/assets/icons/play.svg'; import PlayIcon from '@/assets/icons/play.svg';
import PauseIcon from '@/assets/icons/pause.svg'; import PauseIcon from '@/assets/icons/pause.svg';
import { useUserOrSystemScheme } from '@/components/Colors';
const BUTTON_SIZE = 40; const BUTTON_SIZE = 40;
@@ -18,6 +19,7 @@ const previous = () => TrackPlayer.skipToPrevious();
const Container = styled.View` const Container = styled.View`
align-items: center; align-items: center;
margin-top: 40px; margin-top: 40px;
margin-bottom: 52px;
`; `;
const Buttons = styled.View` const Buttons = styled.View`
@@ -33,7 +35,7 @@ const Button = styled.View`
`; `;
export default function MediaControls() { export default function MediaControls() {
const scheme = useColorScheme(); const scheme = useUserOrSystemScheme();
const fill = scheme === 'dark' ? '#ffffff' : '#000000'; const fill = scheme === 'dark' ? '#ffffff' : '#000000';
return ( return (

View 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>
);
}

View File

@@ -187,7 +187,7 @@ function ProgressBar() {
<ProgressTrack <ProgressTrack
style={[ style={[
styles.themeBackground, styles.themeBackground,
bufferStyles bufferStyles,
]} ]}
opacity={0.15} opacity={0.15}
/> />

View File

@@ -13,7 +13,6 @@ import { t } from '@/localisation';
const Container = styled.View` const Container = styled.View`
align-self: flex-start; align-self: flex-start;
align-items: flex-start; align-items: flex-start;
margin-top: 52px;
padding: 8px; padding: 8px;
margin-left: -8px; margin-left: -8px;
flex: 0 1 auto; flex: 0 1 auto;

View File

@@ -6,24 +6,39 @@ import Queue from './components/Queue';
import ConnectionNotice from './components/ConnectionNotice'; import ConnectionNotice from './components/ConnectionNotice';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
import StreamStatus from './components/StreamStatus'; import StreamStatus from './components/StreamStatus';
import { Platform } from 'react-native'; import {Platform} from 'react-native';
import BackButton from './components/Backbutton'; import BackButton from './components/Backbutton';
import Timer from './components/Timer'; 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 ( return (
<GestureHandlerRootView style={{ flex: 1 }}> <GestureHandlerRootView style={{ flex: 1 }}>
{Platform.OS === 'android' && (<BackButton />)} <ColoredBlurView>
<Queue header={( {Platform.OS === 'android' && (<BackButton />)}
<> <Queue header={(
<NowPlaying /> <>
<ConnectionNotice /> <NowPlaying />
<StreamStatus /> <ConnectionNotice />
<ProgressBar /> <StreamStatus />
<MediaControls /> <MediaInformation />
<Timer /> <ProgressBar />
</> <MediaControls />
)} /> <Group>
<Timer />
</Group>
<LyricsPreview />
</>
)} />
</ColoredBlurView>
</GestureHandlerRootView> </GestureHandlerRootView>
); );
} }

View File

@@ -15,7 +15,7 @@ import CoverImage from '@/components/CoverImage';
import { queueTrackForDownload, removeDownloadedTrack } from '@/store/downloads/actions'; import { queueTrackForDownload, removeDownloadedTrack } from '@/store/downloads/actions';
import usePlayTracks from '@/utility/usePlayTracks'; import usePlayTracks from '@/utility/usePlayTracks';
import { selectIsDownloaded } from '@/store/downloads/selectors'; import { selectIsDownloaded } from '@/store/downloads/selectors';
import { useGetImage } from '@/utility/JellyfinApi'; import { useGetImage } from '@/utility/JellyfinApi/lib';
type Route = RouteProp<StackParams, 'TrackPopupMenu'>; type Route = RouteProp<StackParams, 'TrackPopupMenu'>;

View File

@@ -14,6 +14,7 @@ export type StackParams = {
Search: undefined; Search: undefined;
SetJellyfinServer: undefined; SetJellyfinServer: undefined;
TrackPopupMenu: { trackId: string }; TrackPopupMenu: { trackId: string };
Lyrics: undefined;
}; };
export type NavigationProp = StackNavigationProp<StackParams>; export type NavigationProp = StackNavigationProp<StackParams>;

View File

@@ -1,9 +1,9 @@
import { createAction, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'; import { createAction, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { AppState } from '@/store'; import { AppState } from '@/store';
import { generateTrackUrl } from '@/utility/JellyfinApi';
import { downloadFile, unlink, DocumentDirectoryPath, exists } from 'react-native-fs'; import { downloadFile, unlink, DocumentDirectoryPath, exists } from 'react-native-fs';
import { DownloadEntity } from './types'; import { DownloadEntity } from './types';
import MimeTypes from '@/utility/MimeTypes'; import MimeTypes from '@/utility/MimeTypes';
import { generateTrackUrl } from '@/utility/JellyfinApi/track';
export const downloadAdapter = createEntityAdapter<DownloadEntity>(); export const downloadAdapter = createEntityAdapter<DownloadEntity>();
@@ -15,12 +15,9 @@ export const failDownload = createAction<{ id: string }>('download/fail');
export const downloadTrack = createAsyncThunk( export const downloadTrack = createAsyncThunk(
'/downloads/track', '/downloads/track',
async (id: string, { dispatch, getState }) => { async (id: string, { dispatch }) => {
// Get the credentials from the store
const { settings: { jellyfin: credentials } } = (getState() as AppState);
// Generate the URL we can use to download the file // 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 // Get the content-type from the URL by doing a HEAD-only request
const contentType = (await fetch(url, { method: 'HEAD' })).headers.get('Content-Type'); const contentType = (await fetch(url, { method: 'HEAD' })).headers.get('Content-Type');

View File

@@ -80,13 +80,13 @@ const store = configureStore({
reducer: persistedReducer, reducer: persistedReducer,
middleware: (getDefaultMiddleware) => ( middleware: (getDefaultMiddleware) => (
getDefaultMiddleware({ serializableCheck: false, immutableCheck: false }) getDefaultMiddleware({ serializableCheck: false, immutableCheck: false })
.concat(__DEV__ ? [require('redux-flipper').default()] : [])
), ),
}); });
export type AppState = ReturnType<typeof reducers> & { _persist: PersistState }; export type AppState = ReturnType<typeof reducers> & { _persist: PersistState };
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;
export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch }; export type AsyncThunkAPI = { state: AppState, dispatch: AppDispatch };
export type Store = typeof store;
export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector; export const useTypedSelector: TypedUseSelectorHook<AppState> = useSelector;
export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppDispatch: () => AppDispatch = useDispatch;

View File

@@ -1,7 +1,9 @@
import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'; import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { Album, AlbumTrack, Playlist } from './types'; import { Album, AlbumTrack, Playlist } from './types';
import { AsyncThunkAPI } from '..'; 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>({ export const albumAdapter = createEntityAdapter<Album, string>({
selectId: album => album.Id, selectId: album => album.Id,
@@ -13,10 +15,7 @@ export const albumAdapter = createEntityAdapter<Album, string>({
*/ */
export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI>( export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI>(
'/albums/all', '/albums/all',
async (empty, thunkAPI) => { retrieveAllAlbums,
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAllAlbums(credentials) as Promise<Album[]>;
}
); );
/** /**
@@ -24,10 +23,7 @@ export const fetchAllAlbums = createAsyncThunk<Album[], undefined, AsyncThunkAPI
*/ */
export const fetchRecentAlbums = createAsyncThunk<Album[], number | undefined, AsyncThunkAPI>( export const fetchRecentAlbums = createAsyncThunk<Album[], number | undefined, AsyncThunkAPI>(
'/albums/recent', '/albums/recent',
async (numberOfAlbums, thunkAPI) => { retrieveRecentAlbums,
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveRecentAlbums(credentials, numberOfAlbums) as Promise<Album[]>;
}
); );
export const trackAdapter = createEntityAdapter<AlbumTrack, string>({ export const trackAdapter = createEntityAdapter<AlbumTrack, string>({
@@ -40,18 +36,12 @@ export const trackAdapter = createEntityAdapter<AlbumTrack, string>({
*/ */
export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>( export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>(
'/tracks/byAlbum', '/tracks/byAlbum',
async (ItemId, thunkAPI) => { retrieveAlbumTracks,
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAlbumTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
}
); );
export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>( export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>(
'/albums/single', '/albums/single',
async (ItemId, thunkAPI) => { retrieveAlbum,
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAlbum(credentials, ItemId) as Promise<Album>;
}
); );
type SearchAndFetchResults = { type SearchAndFetchResults = {
@@ -67,16 +57,17 @@ AsyncThunkAPI
'/search', '/search',
async ({ term, limit = 24 }, thunkAPI) => { async ({ term, limit = 24 }, thunkAPI) => {
const state = thunkAPI.getState(); 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) => ( const albums = await Promise.all(results.filter((item) => (
!state.music.albums.ids.includes(item.Type === 'MusicAlbum' ? item.Id : item.AlbumId) !state.music.albums.ids.includes(item.Type === 'MusicAlbum' ? item.Id : item.AlbumId)
&& (item.Type === 'Audio' ? item.AlbumId : true)
)).map(async (item) => { )).map(async (item) => {
if (item.Type === 'MusicAlbum') { if (item.Type === 'MusicAlbum') {
return item; return item;
} }
return retrieveAlbum(state.settings.jellyfin, item.AlbumId); return retrieveAlbum(item.AlbumId);
})); }));
return { return {
@@ -96,10 +87,7 @@ export const playlistAdapter = createEntityAdapter<Playlist, string>({
*/ */
export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncThunkAPI>( export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncThunkAPI>(
'/playlists/all', '/playlists/all',
async (empty, thunkAPI) => { retrieveAllPlaylists,
const credentials = thunkAPI.getState().settings.jellyfin;
return retrieveAllPlaylists(credentials) as Promise<Playlist[]>;
}
); );
/** /**
@@ -107,8 +95,5 @@ export const fetchAllPlaylists = createAsyncThunk<Playlist[], undefined, AsyncTh
*/ */
export const fetchTracksByPlaylist = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>( export const fetchTracksByPlaylist = createAsyncThunk<AlbumTrack[], string, AsyncThunkAPI>(
'/tracks/byPlaylist', '/tracks/byPlaylist',
async (ItemId, thunkAPI) => { retrievePlaylistTracks,
const credentials = thunkAPI.getState().settings.jellyfin;
return retrievePlaylistTracks(ItemId, credentials) as Promise<AlbumTrack[]>;
}
); );

View File

@@ -15,8 +15,8 @@ export function useRecentAlbums(amount: number) {
const sorted = [...albumIds].sort((a, b) => { const sorted = [...albumIds].sort((a, b) => {
const albumA = albums[a]; const albumA = albums[a];
const albumB = albums[b]; const albumB = albums[b];
const dateA = albumA ? parseISO(albumA.DateCreated).getTime() : 0; const dateA = albumA && albumA.DateCreated ? parseISO(albumA.DateCreated).getTime() : 0;
const dateB = albumB ? parseISO(albumB.DateCreated).getTime() : 0; const dateB = albumB && albumB.DateCreated ? parseISO(albumB.DateCreated).getTime() : 0;
return dateB - dateA; return dateB - dateA;
}); });

View File

@@ -1,3 +1,5 @@
import {Lyrics} from '@/utility/JellyfinApi/lyrics.ts';
export interface UserData { export interface UserData {
PlaybackPositionTicks: number; PlaybackPositionTicks: number;
PlayCount: number; PlayCount: number;
@@ -6,6 +8,29 @@ export interface UserData {
Key: string; 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 { export interface ArtistItem {
Name: string; Name: string;
Id: string; Id: string;
@@ -52,6 +77,7 @@ export interface AlbumTrack {
RunTimeTicks: number; RunTimeTicks: number;
ProductionYear: number; ProductionYear: number;
IndexNumber: number; IndexNumber: number;
ParentIndexNumber: number;
IsFolder: boolean; IsFolder: boolean;
Type: 'Audio'; Type: 'Audio';
UserData: UserData; UserData: UserData;
@@ -66,6 +92,9 @@ export interface AlbumTrack {
BackdropImageTags: any[]; BackdropImageTags: any[];
LocationType: string; LocationType: string;
MediaType: string; MediaType: string;
HasLyrics: boolean;
Lyrics: Lyrics | null;
MediaStreams: MediaStream[];
} }
export interface State { export interface State {
@@ -98,4 +127,4 @@ export interface Playlist {
export interface SimilarAlbum { export interface SimilarAlbum {
Id: string; Id: string;
} }

View File

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

View File

@@ -0,0 +1,70 @@
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: 'Ascending',
};
/**
* 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<Album[]>(({ user_id }) => `/Users/${user_id}/Items/Latest?${params}`);
}
/**
* 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));
}

View File

@@ -0,0 +1,108 @@
import type { AppState, Store } from '@/store';
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}"`
}
};
}
/**
* 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.jellyfin;
// 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.jellyfin;
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);
}

View 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;
}));
}

View File

@@ -0,0 +1,64 @@
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,
};
// 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);
});
}

View 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.jellyfin;
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));
}

View 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)
));
}

View 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.jellyfin;
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));
}

View File

@@ -9,8 +9,8 @@
import TrackPlayer, { Event, State } from 'react-native-track-player'; import TrackPlayer, { Event, State } from 'react-native-track-player';
import store from '@/store'; import store from '@/store';
import { sendPlaybackEvent } from './JellyfinApi';
import { setTimerDate } from '@/store/sleep-timer'; import { setTimerDate } from '@/store/sleep-timer';
import { sendPlaybackEvent } from './JellyfinApi/playback';
export default async function() { export default async function() {
TrackPlayer.addEventListener(Event.RemotePlay, () => { TrackPlayer.addEventListener(Event.RemotePlay, () => {
@@ -37,18 +37,19 @@ export default async function() {
TrackPlayer.seekTo(event.position); TrackPlayer.seekTo(event.position);
}); });
TrackPlayer.addEventListener(Event.PlaybackTrackChanged, async (e) => { TrackPlayer.addEventListener(Event.PlaybackActiveTrackChanged, async (e) => {
// Retrieve the current settings from the Redux store // Retrieve the current settings from the Redux store
const settings = store.getState().settings; const settings = store.getState().settings;
console.log('TrackChanged', e?.track?.title);
// GUARD: Only report playback when the settings is enabled // GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting && 'track' in e) { if (settings.enablePlaybackReporting && 'track' in e) {
// GUARD: End the previous track if it's about to end // GUARD: End the previous track if it's about to end
if ('nextTrack' in e && typeof e.track === 'number') { if (e.lastTrack) {
sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin, e.track); 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 // GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting) { if (settings.enablePlaybackReporting) {
sendPlaybackEvent('/Sessions/Playing/Progress', settings.jellyfin); sendPlaybackEvent('/Sessions/Playing/Progress');
} }
// check if timerDate is undefined, otherwise start timer // check if timerDate is undefined, otherwise start timer
@@ -69,14 +70,16 @@ export default async function() {
}); });
TrackPlayer.addEventListener(Event.PlaybackState, (event) => { TrackPlayer.addEventListener(Event.PlaybackState, (event) => {
// GUARD: Only respond to stopped events // Retrieve the current settings from the Redux store
if (event.state === State.Stopped) { 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 // GUARD: Only report playback when the settings is enabled
if (settings.enablePlaybackReporting) { if (settings.enablePlaybackReporting) {
sendPlaybackEvent('/Sessions/Playing/Stopped', settings.jellyfin); // 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');
} }
} }
}); });

View File

@@ -1,8 +1,11 @@
import { useCallback, useEffect, useState } from 'react'; import { useTypedSelector } from '@/store';
import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-track-player'; 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 { interface CurrentTrackResponse {
track: Track | undefined; track: Track | undefined;
albumTrack: AlbumTrack | undefined;
index: number | undefined; index: number | undefined;
} }
@@ -13,11 +16,19 @@ export default function useCurrentTrack(): CurrentTrackResponse {
const [track, setTrack] = useState<Track | undefined>(); const [track, setTrack] = useState<Track | undefined>();
const [index, setIndex] = useState<number | 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 // Retrieve the current track from the queue using the index
const retrieveCurrentTrack = useCallback(async () => { const retrieveCurrentTrack = useCallback(async () => {
const queue = await TrackPlayer.getQueue(); const queue = await TrackPlayer.getQueue();
const currentTrackIndex = await TrackPlayer.getCurrentTrack(); const currentTrackIndex = await TrackPlayer.getActiveTrackIndex();
if (currentTrackIndex !== null) { if (currentTrackIndex !== undefined) {
setTrack(queue[currentTrackIndex]); setTrack(queue[currentTrackIndex]);
setIndex(currentTrackIndex); setIndex(currentTrackIndex);
} else { } else {
@@ -28,7 +39,7 @@ export default function useCurrentTrack(): CurrentTrackResponse {
// Then execute the function on component mount and track changes // Then execute the function on component mount and track changes
useEffect(() => { retrieveCurrentTrack(); }, [retrieveCurrentTrack]); useEffect(() => { retrieveCurrentTrack(); }, [retrieveCurrentTrack]);
useTrackPlayerEvents([ Event.PlaybackTrackChanged, Event.PlaybackState ], retrieveCurrentTrack); useTrackPlayerEvents([ Event.PlaybackActiveTrackChanged, Event.PlaybackState ], retrieveCurrentTrack);
return { track, index }; return { track, index, albumTrack };
} }

View File

@@ -1,13 +1,21 @@
import { useTypedSelector } from '@/store'; import { useTypedSelector } from '@/store';
import { useCallback } from 'react'; import { useCallback } from 'react';
import TrackPlayer, { Track } from 'react-native-track-player'; import TrackPlayer, { Track } from 'react-native-track-player';
import { generateTrack } from './JellyfinApi';
import { shuffle as shuffleArray } from 'lodash'; import { shuffle as shuffleArray } from 'lodash';
import { generateTrack } from './JellyfinApi/track';
interface PlayOptions { interface PlayOptions {
play: boolean; play: boolean;
shuffle: boolean; shuffle: boolean;
method: 'add-to-end' | 'add-after-currently-playing' | 'replace'; 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 = { const defaults: PlayOptions = {
@@ -21,7 +29,6 @@ const defaults: PlayOptions = {
* supplied id. * supplied id.
*/ */
export default function usePlayTracks() { export default function usePlayTracks() {
const credentials = useTypedSelector(state => state.settings.jellyfin);
const tracks = useTypedSelector(state => state.music.tracks.entities); const tracks = useTypedSelector(state => state.music.tracks.entities);
const downloads = useTypedSelector(state => state.downloads.entities); const downloads = useTypedSelector(state => state.downloads.entities);
@@ -42,7 +49,7 @@ export default function usePlayTracks() {
const queue = await TrackPlayer.getQueue(); const queue = await TrackPlayer.getQueue();
// Convert all trackIds to the relevant format for react-native-track-player // 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]; const track = tracks[trackId];
// GUARD: Check that the track actually exists in Redux // GUARD: Check that the track actually exists in Redux
@@ -51,7 +58,7 @@ export default function usePlayTracks() {
} }
// Retrieve the generated track from Jellyfin // 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 // Check if a downloaded version exists, and if so rewrite the URL
const download = downloads[trackId]; const download = downloads[trackId];
@@ -60,7 +67,7 @@ export default function usePlayTracks() {
} }
return generatedTrack; return generatedTrack;
}).filter((t): t is Track => typeof t !== 'undefined'); }))).filter((t): t is Track => typeof t !== 'undefined');
// Potentially shuffle all tracks // Potentially shuffle all tracks
const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks; const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;
@@ -104,15 +111,33 @@ export default function usePlayTracks() {
break; break;
} }
case 'replace': { case 'replace': {
// Reset the queue first
await TrackPlayer.reset(); await TrackPlayer.reset();
await TrackPlayer.add(newTracks);
if (play) { // GUARD: Check if we need to skip to a particular index
await TrackPlayer.play(); 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; break;
} }
} }
}, [credentials, downloads, tracks]); }, [downloads, tracks]);
} }