Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3afe403f4c | ||
|
|
c4838b3b9e | ||
|
|
63481d0240 | ||
|
|
eb45169060 | ||
|
|
2e3ced0680 | ||
|
|
76f3ce3972 | ||
|
|
cf8bfdf05a | ||
|
|
09a020afbb | ||
|
|
90a5d94147 | ||
|
|
58c7645170 | ||
|
|
d9326dfc7a |
35
Gemfile.lock
35
Gemfile.lock
@@ -25,26 +25,27 @@ GEM
|
||||
json (>= 1.5.1)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1097.0)
|
||||
aws-sdk-core (3.223.0)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1141.0)
|
||||
aws-sdk-core (3.229.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.100.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (1.110.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.185.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-s3 (1.196.0)
|
||||
aws-sdk-core (~> 3, >= 3.228.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
base64 (0.3.0)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
claide (1.1.0)
|
||||
@@ -118,10 +119,10 @@ GEM
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
faraday-multipart (1.1.1)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
@@ -131,7 +132,7 @@ GEM
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.227.2)
|
||||
fastlane (2.228.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
@@ -227,27 +228,27 @@ GEM
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.11.3)
|
||||
jwt (2.10.1)
|
||||
json (2.13.2)
|
||||
jwt (2.10.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.5)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multi_json (1.17.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
naturally (2.3.0)
|
||||
netrc (0.11.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.2.1)
|
||||
rake (13.3.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
|
||||
@@ -87,8 +87,8 @@ android {
|
||||
applicationId "nl.moeilijkedingen.jellyfinaudioplayer"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 38
|
||||
versionName "2.4.5"
|
||||
versionCode 40
|
||||
versionName "2.4.6"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
7
fastlane/metadata/android/ca/changelogs/40.txt
Normal file
7
fastlane/metadata/android/ca/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Ens alegra oferir-te algunes millores útils en aquesta actualització!
|
||||
|
||||
El rendiment per a grans col·leccions d'àlbums ha estat significativament millorat, fent que la navegació per la teva biblioteca musical sigui més fluida i responsiva.
|
||||
|
||||
També hem solucionat un problema que feia que la part superior de l'aplicació es tallés en Android 15 i versions més recents. L'aplicació ara hauria de mostrar-se correctament en els dispositius Android més recents.
|
||||
|
||||
Gràcies per usar Fintunes!
|
||||
7
fastlane/metadata/android/cs-CZ/changelogs/40.txt
Normal file
7
fastlane/metadata/android/cs-CZ/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Jsme rádi, že vám můžeme přinést několik užitečných vylepšení v této aktualizaci!
|
||||
|
||||
Výkon pro velké kolekce alb byl výrazně vylepšen, což činí procházení vaší hudební knihovny plynulejším a více responzivním.
|
||||
|
||||
Také jsme opravili problém, který způsoboval, že horní část aplikace byla ořezána na Androidu 15 a novějších verzích. Aplikace by se nyní měla správně zobrazovat na nejnovějších Android zařízeních.
|
||||
|
||||
Děkujeme za používání Fintunes!
|
||||
7
fastlane/metadata/android/de-DE/changelogs/40.txt
Normal file
7
fastlane/metadata/android/de-DE/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Nützliche Verbesserungen in diesem Update!
|
||||
|
||||
Die Leistung für große Albumsammlungen wurde erheblich verbessert, wodurch das Durchsuchen Ihrer Musikbibliothek reibungsloser wird.
|
||||
|
||||
Wir haben auch ein Problem behoben, das dazu führte, dass der obere Teil der App auf Android 15+ abgeschnitten wurde.
|
||||
|
||||
Vielen Dank, dass Sie Fintunes verwenden!
|
||||
7
fastlane/metadata/android/en-US/changelogs/40.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
We're pleased to bring you some useful improvements in this update!
|
||||
|
||||
Performance for large album collections has been significantly enhanced, making browsing through your music library smoother and more responsive.
|
||||
|
||||
We've also fixed an issue that was causing the top part of the app to be cut off on Android 15 and newer versions. The app should now display properly on the latest Android devices.
|
||||
|
||||
Thanks for using Fintunes!
|
||||
7
fastlane/metadata/android/es-ES/changelogs/40.txt
Normal file
7
fastlane/metadata/android/es-ES/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
¡Mejoras útiles en esta actualización!
|
||||
|
||||
El rendimiento para grandes colecciones de álbumes ha sido significativamente mejorado, haciendo que la navegación por tu biblioteca musical sea más fluida.
|
||||
|
||||
También hemos solucionado un problema que causaba que la parte superior de la aplicación se cortara en Android 15+.
|
||||
|
||||
¡Gracias por usar Fintunes!
|
||||
7
fastlane/metadata/android/fr-FR/changelogs/40.txt
Normal file
7
fastlane/metadata/android/fr-FR/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Améliorations utiles dans cette mise à jour !
|
||||
|
||||
Les performances pour les grandes collections d'albums ont été considérablement améliorées, rendant la navigation plus fluide.
|
||||
|
||||
Nous avons également corrigé un problème qui causait la coupure de la partie supérieure de l'application sur Android 15+.
|
||||
|
||||
Merci d'utiliser Fintunes !
|
||||
7
fastlane/metadata/android/it-IT/changelogs/40.txt
Normal file
7
fastlane/metadata/android/it-IT/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Siamo lieti di offrirti alcuni miglioramenti utili in questo aggiornamento!
|
||||
|
||||
Le prestazioni per grandi collezioni di album sono state significativamente migliorate, rendendo la navigazione nella tua libreria musicale più fluida e reattiva.
|
||||
|
||||
Abbiamo anche risolto un problema che causava il taglio della parte superiore dell'app su Android 15 e versioni più recenti. L'app ora dovrebbe visualizzarsi correttamente sui dispositivi Android più recenti.
|
||||
|
||||
Grazie per aver usato Fintunes!
|
||||
7
fastlane/metadata/android/ja-JP/changelogs/40.txt
Normal file
7
fastlane/metadata/android/ja-JP/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
有用な改善をお届けします!
|
||||
|
||||
大規模なアルバムコレクションのパフォーマンスが大幅に向上し、音楽ライブラリの閲覧がよりスムーズになりました。
|
||||
|
||||
また、Android 15+でアプリの上部が切れる問題を修正しました。
|
||||
|
||||
Fintunesをご利用いただき、ありがとうございます!
|
||||
7
fastlane/metadata/android/nl-NL/changelogs/40.txt
Normal file
7
fastlane/metadata/android/nl-NL/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
We zijn blij je enkele nuttige verbeteringen te kunnen brengen in deze update!
|
||||
|
||||
De prestaties voor grote albumcollecties zijn aanzienlijk verbeterd, waardoor het browsen door je muziekbibliotheek soepeler en responsiever wordt.
|
||||
|
||||
We hebben ook een probleem opgelost dat ervoor zorgde dat het bovenste deel van de app werd afgesneden op Android 15 en nieuwere versies. De app zou nu correct moeten worden weergegeven op de nieuwste Android-apparaten.
|
||||
|
||||
Bedankt voor het gebruik van Fintunes!
|
||||
7
fastlane/metadata/android/no-NO/changelogs/40.txt
Normal file
7
fastlane/metadata/android/no-NO/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Vi er glade for å bringe deg noen nyttige forbedringer i denne oppdateringen!
|
||||
|
||||
Ytelsen for store albumsamlinger har blitt betydelig forbedret, noe som gjør surfing gjennom musikk-biblioteket ditt jevnere og mer responsivt.
|
||||
|
||||
Vi har også fikset et problem som forårsaket at den øverste delen av appen ble kuttet av på Android 15 og nyere versjoner. Appen bør nå vises riktig på de nyeste Android-enhetene.
|
||||
|
||||
Takk for at du bruker Fintunes!
|
||||
7
fastlane/metadata/android/pl-PL/changelogs/40.txt
Normal file
7
fastlane/metadata/android/pl-PL/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Przydatne ulepszenia w tej aktualizacji!
|
||||
|
||||
Wydajność dla dużych kolekcji albumów została znacznie poprawiona, dzięki czemu przeglądanie biblioteki muzycznej jest płynniejsze.
|
||||
|
||||
Naprawiliśmy również problem, który powodował obcinanie górnej części aplikacji na Androidzie 15+.
|
||||
|
||||
Dziękujemy za korzystanie z Fintunes!
|
||||
7
fastlane/metadata/android/pt-BR/changelogs/40.txt
Normal file
7
fastlane/metadata/android/pt-BR/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Estamos satisfeitos em trazer algumas melhorias úteis nesta atualização!
|
||||
|
||||
O desempenho para grandes coleções de álbuns foi significativamente aprimorado, tornando a navegação pela sua biblioteca musical mais suave e responsiva.
|
||||
|
||||
Também corrigimos um problema que estava causando o corte da parte superior do aplicativo no Android 15 e versões mais recentes. O aplicativo agora deve ser exibido corretamente nos dispositivos Android mais recentes.
|
||||
|
||||
Obrigado por usar o Fintunes!
|
||||
7
fastlane/metadata/android/ru-RU/changelogs/40.txt
Normal file
7
fastlane/metadata/android/ru-RU/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Полезные улучшения в обновлении!
|
||||
|
||||
Производительность для больших коллекций альбомов значительно улучшена, что делает просмотр музыкальной библиотеки более плавным.
|
||||
|
||||
Исправили проблему обрезания верхней части приложения на Android 15+.
|
||||
|
||||
Спасибо за использование Fintunes!
|
||||
7
fastlane/metadata/android/sv-SE/changelogs/40.txt
Normal file
7
fastlane/metadata/android/sv-SE/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Vi är glada att kunna erbjuda dig några användbara förbättringar i denna uppdatering!
|
||||
|
||||
Prestandan för stora albumsamlingar har förbättrats betydligt, vilket gör surfandet genom ditt musikbibliotek smidigare och mer responsivt.
|
||||
|
||||
Vi har också åtgärdat ett problem som orsakade att den övre delen av appen blev avskuren på Android 15 och nyare versioner. Appen bör nu visas korrekt på de senaste Android-enheterna.
|
||||
|
||||
Tack för att du använder Fintunes!
|
||||
7
fastlane/metadata/android/uk/changelogs/40.txt
Normal file
7
fastlane/metadata/android/uk/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
Корисні покращення в цьому оновленні!
|
||||
|
||||
Продуктивність для великих колекцій альбомів була значно покращена, роблячи перегляд музичної бібліотеки плавнішим.
|
||||
|
||||
Ми також виправили проблему обрізання верхньої частини додатку на Android 15+.
|
||||
|
||||
Дякуємо за використання Fintunes!
|
||||
7
fastlane/metadata/android/zh-CN/changelogs/40.txt
Normal file
7
fastlane/metadata/android/zh-CN/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
我们很高兴为您带来此次更新中的一些实用改进!
|
||||
|
||||
大型专辑集合的性能得到了显著提升,使您浏览音乐库更加流畅和响应迅速。
|
||||
|
||||
我们还修复了一个在Android 15及更新版本上导致应用程序顶部被截断的问题。应用程序现在应该在最新的Android设备上正确显示。
|
||||
|
||||
感谢您使用Fintunes!
|
||||
7
fastlane/metadata/android/zh-TW/changelogs/40.txt
Normal file
7
fastlane/metadata/android/zh-TW/changelogs/40.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
我們很高興為您帶來此次更新中的一些實用改進!
|
||||
|
||||
大型專輯集合的性能得到了顯著提升,使您瀏覽音樂庫更加流暢和響應迅速。
|
||||
|
||||
我們還修復了一個在Android 15及更新版本上導致應用程序頂部被截斷的問題。應用程序現在應該在最新的Android設備上正確顯示。
|
||||
|
||||
感謝您使用Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Aquesta versió soluciona un problema on Fintunes només podia reproduir el primer disc en un àlbum de múltiples discs. A més, gràcies als nostres increïbles col·laboradors lingüístics, Fintunes ara està disponible en tàmil. A més, s'han actualitzat 12 idiomes. Gaudeix d'aquesta nova versió de Fintunes i considera unir-te al nostre servidor Discord!
|
||||
Ens alegra oferir-te algunes millores útils en aquesta actualització!
|
||||
|
||||
El rendiment per a grans col·leccions d'àlbums ha estat significativament millorat, fent que la navegació per la teva biblioteca musical sigui més fluida i responsiva.
|
||||
|
||||
També hem solucionat un problema que feia que la part superior de l'aplicació es tallés en Android 15 i versions més recents. L'aplicació ara hauria de mostrar-se correctament en els dispositius Android més recents.
|
||||
|
||||
Gràcies per usar Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Tato verze opravuje problém, kdy Fintunes mohl přehrávat pouze první disk v albu s více disky. Navíc díky našim úžasným jazykovým přispěvatelům je Fintunes nyní k dispozici v tamilštině. Dále bylo aktualizováno 12 jazyků. Užijte si tuto novou verzi Fintunes a připojte se k našemu Discord serveru!
|
||||
Jsme rádi, že vám můžeme přinést několik užitečných vylepšení v této aktualizaci!
|
||||
|
||||
Výkon pro velké kolekce alb byl výrazně vylepšen, což činí procházení vaší hudební knihovny plynulejším a více responzivním.
|
||||
|
||||
Také jsme opravili problém, který způsoboval, že horní část aplikace byla ořezána na Androidu 15 a novějších verzích. Aplikace by se nyní měla správně zobrazovat na nejnovějších Android zařízeních.
|
||||
|
||||
Děkujeme za používání Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Diese Version behebt ein Problem, bei dem Fintunes nur die erste CD eines Mehrfach-CD-Albums abspielen konnte. Außerdem ist Fintunes dank unserer großartigen Sprachmitwirkenden jetzt auch auf Tamilisch verfügbar. Zusätzlich wurden 12 Sprachen aktualisiert. Genießen Sie diese neue Version von Fintunes und treten Sie gerne unserem Discord-Server bei!
|
||||
Wir freuen uns, Ihnen einige nützliche Verbesserungen in diesem Update zu bieten!
|
||||
|
||||
Die Leistung für große Albumsammlungen wurde erheblich verbessert, wodurch das Durchsuchen Ihrer Musikbibliothek reibungsloser und reaktionsschneller wird.
|
||||
|
||||
Wir haben auch ein Problem behoben, das dazu führte, dass der obere Teil der App auf Android 15 und neueren Versionen abgeschnitten wurde. Die App sollte nun ordnungsgemäß auf den neuesten Android-Geräten angezeigt werden.
|
||||
|
||||
Vielen Dank, dass Sie Fintunes verwenden!
|
||||
@@ -1 +1,7 @@
|
||||
This version fixes an issue where Fintunes was only able to play back the first disc in a multi-disc album. Also, thanks to our amazing language contributors, Fintunes is now available in Tamil. Additionally, there's 12 languages that have been updated. Enjoy this new version of Fintunes and consider joining our Discord server!
|
||||
We're pleased to bring you some useful improvements in this update!
|
||||
|
||||
Performance for large album collections has been significantly enhanced, making browsing through your music library smoother and more responsive.
|
||||
|
||||
We've also fixed an issue that was causing the top part of the app to be cut off on Android 15 and newer versions. The app should now display properly on the latest Android devices.
|
||||
|
||||
Thanks for using Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Esta versión soluciona un problema donde Fintunes solo podía reproducir el primer disco en un álbum de múltiples discos. Además, gracias a nuestros increíbles colaboradores de idiomas, Fintunes ahora está disponible en tamil. También se han actualizado 12 idiomas. ¡Disfruta de esta nueva versión de Fintunes y considera unirte a nuestro servidor de Discord!
|
||||
¡Nos complace ofrecerte algunas mejoras útiles en esta actualización!
|
||||
|
||||
El rendimiento para grandes colecciones de álbumes ha sido significativamente mejorado, haciendo que la navegación por tu biblioteca musical sea más fluida y responsiva.
|
||||
|
||||
También hemos solucionado un problema que causaba que la parte superior de la aplicación se cortara en Android 15 y versiones más recientes. La aplicación ahora debería mostrarse correctamente en los dispositivos Android más recientes.
|
||||
|
||||
¡Gracias por usar Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Cette version corrige un problème où Fintunes ne pouvait lire que le premier disque d'un album multi-disques. De plus, grâce à nos incroyables contributeurs linguistiques, Fintunes est maintenant disponible en tamoul. En outre, 12 langues ont été mises à jour. Profitez de cette nouvelle version de Fintunes et rejoignez notre serveur Discord !
|
||||
Nous sommes heureux de vous apporter quelques améliorations utiles dans cette mise à jour !
|
||||
|
||||
Les performances pour les grandes collections d'albums ont été considérablement améliorées, rendant la navigation dans votre bibliothèque musicale plus fluide et plus réactive.
|
||||
|
||||
Nous avons également corrigé un problème qui causait la coupure de la partie supérieure de l'application sur Android 15 et les versions plus récentes. L'application devrait maintenant s'afficher correctement sur les derniers appareils Android.
|
||||
|
||||
Merci d'utiliser Fintunes !
|
||||
@@ -1 +1,7 @@
|
||||
Questa versione risolve un problema in cui Fintunes poteva riprodurre solo il primo disco in un album multi-disco. Inoltre, grazie ai nostri fantastici collaboratori linguistici, Fintunes è ora disponibile in tamil. Inoltre, sono state aggiornate 12 lingue. Goditi questa nuova versione di Fintunes e unisciti al nostro server Discord!
|
||||
Siamo lieti di offrirti alcuni miglioramenti utili in questo aggiornamento!
|
||||
|
||||
Le prestazioni per grandi collezioni di album sono state significativamente migliorate, rendendo la navigazione nella tua libreria musicale più fluida e reattiva.
|
||||
|
||||
Abbiamo anche risolto un problema che causava il taglio della parte superiore dell'app su Android 15 e versioni più recenti. L'app ora dovrebbe visualizzarsi correttamente sui dispositivi Android più recenti.
|
||||
|
||||
Grazie per aver usato Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
このバージョンでは、マルチディスクアルバムの最初のディスクしか再生できなかった問題を修正しました。また、素晴らしい言語コントリビューターのおかげで、Fintunesはタミル語でも利用可能になりました。さらに、12の言語が更新されました。Fintunesの新バージョンをお楽しみください。Discordサーバーへの参加もお待ちしています!
|
||||
このアップデートで、いくつかの有用な改善をお届けできることを嬉しく思います!
|
||||
|
||||
大規模なアルバムコレクションのパフォーマンスが大幅に向上し、音楽ライブラリの閲覧がよりスムーズで反応性の良いものになりました。
|
||||
|
||||
また、Android 15以降のバージョンでアプリの上部が切れてしまう問題も修正しました。最新のAndroidデバイスでアプリが正しく表示されるようになりました。
|
||||
|
||||
Fintunesをご利用いただき、ありがとうございます!
|
||||
@@ -1 +1,7 @@
|
||||
Deze versie lost een probleem op waarbij Fintunes alleen de eerste schijf van een album met meerdere schijven kon afspelen. Bovendien is Fintunes dankzij onze geweldige taalbijdragers nu beschikbaar in het Tamil. Daarnaast zijn er 12 talen bijgewerkt. Geniet van deze nieuwe versie van Fintunes en overweeg om lid te worden van onze Discord-server!
|
||||
We zijn blij je enkele nuttige verbeteringen te kunnen brengen in deze update!
|
||||
|
||||
De prestaties voor grote albumcollecties zijn aanzienlijk verbeterd, waardoor het browsen door je muziekbibliotheek soepeler en responsiever wordt.
|
||||
|
||||
We hebben ook een probleem opgelost dat ervoor zorgde dat het bovenste deel van de app werd afgesneden op Android 15 en nieuwere versies. De app zou nu correct moeten worden weergegeven op de nieuwste Android-apparaten.
|
||||
|
||||
Bedankt voor het gebruik van Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Denne versjonen løser et problem hvor Fintunes bare kunne spille av den første disken i et flerdisk-album. I tillegg, takket være våre fantastiske språkbidragsytere, er Fintunes nå tilgjengelig på tamil. Dessuten er 12 språk oppdatert. Nyt denne nye versjonen av Fintunes og vurder å bli med på vår Discord-server!
|
||||
Vi er glade for å bringe deg noen nyttige forbedringer i denne oppdateringen!
|
||||
|
||||
Ytelsen for store albumsamlinger har blitt betydelig forbedret, noe som gjør surfing gjennom musikk-biblioteket ditt jevnere og mer responsivt.
|
||||
|
||||
Vi har også fikset et problem som forårsaket at den øverste delen av appen ble kuttet av på Android 15 og nyere versjoner. Appen bør nå vises riktig på de nyeste Android-enhetene.
|
||||
|
||||
Takk for at du bruker Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Ta wersja naprawia problem, w którym Fintunes mógł odtwarzać tylko pierwszy dysk w albumie wielopłytowym. Ponadto, dzięki naszym wspaniałym współtwórcom językowym, Fintunes jest teraz dostępny w języku tamilskim. Dodatkowo zaktualizowano 12 języków. Ciesz się tą nową wersją Fintunes i dołącz do naszego serwera Discord!
|
||||
Cieszymy się, że możemy dostarczyć Ci kilka przydatnych ulepszeń w tej aktualizacji!
|
||||
|
||||
Wydajność dla dużych kolekcji albumów została znacznie poprawiona, dzięki czemu przeglądanie biblioteki muzycznej jest płynniejsze i bardziej responsywne.
|
||||
|
||||
Naprawiliśmy również problem, który powodował obcinanie górnej części aplikacji na Androidzie 15 i nowszych wersjach. Aplikacja powinna teraz wyświetlać się prawidłowo na najnowszych urządzeniach z Androidem.
|
||||
|
||||
Dziękujemy za korzystanie z Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Esta versão corrige um problema em que o Fintunes só conseguia reproduzir o primeiro disco em um álbum com múltiplos discos. Além disso, graças aos nossos incríveis colaboradores de idiomas, o Fintunes agora está disponível em tâmil. Adicionalmente, 12 idiomas foram atualizados. Aproveite esta nova versão do Fintunes e considere entrar em nosso servidor Discord!
|
||||
Estamos satisfeitos em trazer algumas melhorias úteis nesta atualização!
|
||||
|
||||
O desempenho para grandes coleções de álbuns foi significativamente aprimorado, tornando a navegação pela sua biblioteca musical mais suave e responsiva.
|
||||
|
||||
Também corrigimos um problema que estava causando o corte da parte superior do aplicativo no Android 15 e versões mais recentes. O aplicativo agora deve ser exibido corretamente nos dispositivos Android mais recentes.
|
||||
|
||||
Obrigado por usar o Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Эта версия исправляет проблему, при которой Fintunes мог воспроизводить только первый диск в альбоме с несколькими дисками. Кроме того, благодаря нашим замечательным языковым контрибьюторам, Fintunes теперь доступен на тамильском языке. Также обновлены 12 языков. Наслаждайтесь новой версией Fintunes и присоединяйтесь к нашему серверу Discord!
|
||||
Мы рады предоставить вам несколько полезных улучшений в этом обновлении!
|
||||
|
||||
Производительность для больших коллекций альбомов была значительно улучшена, что делает просмотр вашей музыкальной библиотеки более плавным и отзывчивым.
|
||||
|
||||
Мы также исправили проблему, которая приводила к обрезанию верхней части приложения на Android 15 и более новых версиях. Приложение теперь должно корректно отображаться на новейших Android-устройствах.
|
||||
|
||||
Спасибо за использование Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Denna version åtgärdar ett problem där Fintunes bara kunde spela upp den första skivan i ett flerskivsalbum. Dessutom, tack vare våra fantastiska språkbidragsgivare, är Fintunes nu tillgängligt på tamil. Dessutom har 12 språk uppdaterats. Njut av denna nya version av Fintunes och överväg att gå med i vår Discord-server!
|
||||
Vi är glada att kunna erbjuda dig några användbara förbättringar i denna uppdatering!
|
||||
|
||||
Prestandan för stora albumsamlingar har förbättrats betydligt, vilket gör surfandet genom ditt musikbibliotek smidigare och mer responsivt.
|
||||
|
||||
Vi har också åtgärdat ett problem som orsakade att den övre delen av appen blev avskuren på Android 15 och nyare versioner. Appen bör nu visas korrekt på de senaste Android-enheterna.
|
||||
|
||||
Tack för att du använder Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
Ця версія виправляє проблему, коли Fintunes міг відтворювати лише перший диск у багатодисковому альбомі. Крім того, завдяки нашим чудовим мовним учасникам, Fintunes тепер доступний тамільською мовою. Також оновлено 12 мов. Насолоджуйтесь цією новою версією Fintunes та приєднуйтесь до нашого сервера Discord!
|
||||
Ми раді принести вам кілька корисних покращень у цьому оновленні!
|
||||
|
||||
Продуктивність для великих колекцій альбомів була значно покращена, роблячи перегляд вашої музичної бібліотеки плавнішим та більш відгукливим.
|
||||
|
||||
Ми також виправили проблему, яка спричиняла обрізання верхньої частини додатку на Android 15 і новіших версіях. Додаток тепер повинен правильно відображатися на найновіших Android-пристроях.
|
||||
|
||||
Дякуємо за використання Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
此版本修复了Fintunes只能播放多碟专辑中第一张碟片的问题。此外,感谢我们出色的语言贡献者,Fintunes现在支援泰米尔语。另外还有12种语言已更新。享受这个新版本的Fintunes,并考虑加入我们的Discord伺服器!
|
||||
我们很高兴为您带来此次更新中的一些实用改进!
|
||||
|
||||
大型专辑集合的性能得到了显著提升,使您浏览音乐库更加流畅和响应迅速。
|
||||
|
||||
我们还修复了一个在Android 15及更新版本上导致应用程序顶部被截断的问题。应用程序现在应该在最新的Android设备上正确显示。
|
||||
|
||||
感谢您使用Fintunes!
|
||||
@@ -1 +1,7 @@
|
||||
此版本修復了Fintunes只能播放多碟專輯中第一張碟片的問題。此外,感謝我們出色的語言貢獻者,Fintunes現在支援泰米爾語。另外還有12種語言已更新。享受這個新版本的Fintunes,並考慮加入我們的Discord伺服器!
|
||||
我們很高興為您帶來此次更新中的一些實用改進!
|
||||
|
||||
大型專輯集合的性能得到了顯著提升,使您瀏覽音樂庫更加流暢和響應迅速。
|
||||
|
||||
我們還修復了一個在Android 15及更新版本上導致應用程序頂部被截斷的問題。應用程序現在應該在最新的Android設備上正確顯示。
|
||||
|
||||
感謝您使用Fintunes!
|
||||
@@ -319,6 +319,7 @@
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/RNSVG/RNSVGFilters.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/React-Core_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-cxxreact/React-cxxreact_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/Sentry/Sentry.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/boost/boost_privacy.bundle",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle",
|
||||
@@ -330,6 +331,7 @@
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNSVGFilters.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-Core_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/React-cxxreact_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SDWebImage.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Sentry.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/boost_privacy.bundle",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle",
|
||||
@@ -389,9 +391,11 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = HD2D35G9Y4;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
@@ -412,6 +416,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.moeilijkedingen.jellyfinaudioplayer;
|
||||
PRODUCT_NAME = Fintunes;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1754345870";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -427,7 +432,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = HD2D35G9Y4;
|
||||
INFOPLIST_FILE = Fintunes/Info.plist;
|
||||
@@ -446,7 +451,7 @@
|
||||
PRODUCT_NAME = Fintunes;
|
||||
PROVISIONING_PROFILE = "915c5213-22f6-4f9d-8065-2a06300f9bfb";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore";
|
||||
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "nl.moeilijkedingen.jellyfinaudioplayer AppStore 1754345870";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
@@ -607,7 +612,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -642,7 +647,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 115;
|
||||
CURRENT_PROJECT_VERSION = 124;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = HD2D35G9Y4;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.4.5</string>
|
||||
<string>2.4.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>115</string>
|
||||
<string>124</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
|
||||
@@ -8,6 +8,11 @@ PODS:
|
||||
- hermes-engine (0.79.2):
|
||||
- hermes-engine/Pre-built (= 0.79.2)
|
||||
- hermes-engine/Pre-built (0.79.2)
|
||||
- libavif/core (0.11.1)
|
||||
- libavif/libdav1d (0.11.1):
|
||||
- libavif/core
|
||||
- libdav1d (>= 0.6.0)
|
||||
- libdav1d (1.2.0)
|
||||
- libwebp (1.3.2):
|
||||
- libwebp/demux (= 1.3.2)
|
||||
- libwebp/mux (= 1.3.2)
|
||||
@@ -1894,10 +1899,59 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNFastImage (8.6.3):
|
||||
- RNFastImage (8.9.2):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- libavif/core (~> 0.11.1)
|
||||
- libavif/libdav1d (~> 0.11.1)
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- SDWebImage (~> 5.11.1)
|
||||
- SDWebImageWebPCoder (~> 0.8.4)
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-hermes
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- SDWebImage (>= 5.19.1)
|
||||
- SDWebImageAVIFCoder (~> 0.11.0)
|
||||
- SDWebImageWebPCoder (~> 0.14)
|
||||
- Yoga
|
||||
- RNFlashList (1.8.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
- RCT-Folly (= 2024.11.18.00)
|
||||
- RCTRequired
|
||||
- RCTTypeSafety
|
||||
- React-Core
|
||||
- React-debug
|
||||
- React-Fabric
|
||||
- React-featureflags
|
||||
- React-graphics
|
||||
- React-hermes
|
||||
- React-ImageManager
|
||||
- React-jsi
|
||||
- React-NativeModulesApple
|
||||
- React-RCTFabric
|
||||
- React-renderercss
|
||||
- React-rendererdebug
|
||||
- React-utils
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNFS (2.20.0):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.25.0):
|
||||
@@ -2197,12 +2251,15 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- SDWebImage (5.11.1):
|
||||
- SDWebImage/Core (= 5.11.1)
|
||||
- SDWebImage/Core (5.11.1)
|
||||
- SDWebImageWebPCoder (0.8.5):
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- SDWebImageAVIFCoder (0.11.0):
|
||||
- libavif/core (>= 0.11.0)
|
||||
- SDWebImage (~> 5.10)
|
||||
- SDWebImageWebPCoder (0.14.6):
|
||||
- libwebp (~> 1.0)
|
||||
- SDWebImage/Core (~> 5.10)
|
||||
- SDWebImage/Core (~> 5.17)
|
||||
- Sentry/HybridSDK (8.49.2)
|
||||
- SocketRocket (0.7.1)
|
||||
- SwiftAudioEx (1.1.0)
|
||||
@@ -2291,7 +2348,8 @@ DEPENDENCIES:
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNFastImage (from `../node_modules/react-native-fast-image`)
|
||||
- "RNFastImage (from `../node_modules/@d11/react-native-fast-image`)"
|
||||
- "RNFlashList (from `../node_modules/@shopify/flash-list`)"
|
||||
- RNFS (from `../node_modules/react-native-fs`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||
@@ -2303,8 +2361,11 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- libavif
|
||||
- libdav1d
|
||||
- libwebp
|
||||
- SDWebImage
|
||||
- SDWebImageAVIFCoder
|
||||
- SDWebImageWebPCoder
|
||||
- Sentry
|
||||
- SocketRocket
|
||||
@@ -2471,7 +2532,9 @@ EXTERNAL SOURCES:
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNFastImage:
|
||||
:path: "../node_modules/react-native-fast-image"
|
||||
:path: "../node_modules/@d11/react-native-fast-image"
|
||||
RNFlashList:
|
||||
:path: "../node_modules/@shopify/flash-list"
|
||||
RNFS:
|
||||
:path: "../node_modules/react-native-fs"
|
||||
RNGestureHandler:
|
||||
@@ -2497,6 +2560,8 @@ SPEC CHECKSUMS:
|
||||
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
|
||||
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
|
||||
hermes-engine: 314be5250afa5692b57b4dd1705959e1973a8ebe
|
||||
libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
|
||||
libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
|
||||
RCTDeprecation: 83ffb90c23ee5cea353bd32008a7bca100908f8c
|
||||
@@ -2570,7 +2635,8 @@ SPEC CHECKSUMS:
|
||||
ReactCommon: 76d2dc87136d0a667678668b86f0fca0c16fdeb0
|
||||
RNCAsyncStorage: 39c42c1e478e1f5166d1db52b5055e090e85ad66
|
||||
RNDateTimePicker: 43ee3de2bc639bc0d9b77564961060a54dfc7111
|
||||
RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
|
||||
RNFastImage: 72769fac450a8a7d111378433111d8e52f2b251e
|
||||
RNFlashList: 5001dd06f0003a497de3e2035653c54cf8b48e96
|
||||
RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8
|
||||
RNGestureHandler: ebef699ea17e7c0006c1074e1e423ead60ce0121
|
||||
RNLocalize: 66046b78816e61e5b8211084b72afab4191d1db3
|
||||
@@ -2578,8 +2644,9 @@ SPEC CHECKSUMS:
|
||||
RNScreens: 5621e3ad5a329fbd16de683344ac5af4192b40d3
|
||||
RNSentry: f444779917ac0314ecffea5e8bd1041d1be06c31
|
||||
RNSVG: 794f269526df9ddc1f79b3d1a202b619df0368e3
|
||||
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
|
||||
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
||||
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
|
||||
Sentry: 47021097466aa130802420d485a34da445963d99
|
||||
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
|
||||
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fintunes",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.6",
|
||||
"main": "src/index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -12,6 +12,7 @@
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"dependencies": {
|
||||
"@d11/react-native-fast-image": "^8.9.2",
|
||||
"@react-native-async-storage/async-storage": "^2.1.2",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native-community/datetimepicker": "^8.3.0",
|
||||
@@ -22,6 +23,7 @@
|
||||
"@react-navigation/native-stack": "^6.11.0",
|
||||
"@react-navigation/stack": "^6.4.1",
|
||||
"@reduxjs/toolkit": "^2.7.0",
|
||||
"@shopify/flash-list": "^1.8.0",
|
||||
"@shopify/react-native-skia": "2.0.0-next.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"events": "^3.3.0",
|
||||
@@ -35,7 +37,6 @@
|
||||
"react-native-accessibility-settings": "^0.1.2",
|
||||
"react-native-collapsible": "^1.6.2",
|
||||
"react-native-dotenv": "^3.4.11",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "^2.25.0",
|
||||
"react-native-localize": "^3.4.1",
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
diff --git a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
|
||||
index f710081..87c4532 100644
|
||||
--- a/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
|
||||
+++ b/node_modules/react-native-fast-image/ios/FastImage/FFFastImageView.m
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
- (void) setOnFastImageLoadEnd: (RCTDirectEventBlock)onFastImageLoadEnd {
|
||||
_onFastImageLoadEnd = onFastImageLoadEnd;
|
||||
- if (self.hasCompleted) {
|
||||
+ if (self.hasCompleted && _onFastImageLoadEnd != NULL) {
|
||||
_onFastImageLoadEnd(@{});
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
- (void) setOnFastImageError: (RCTDirectEventBlock)onFastImageError {
|
||||
_onFastImageError = onFastImageError;
|
||||
- if (self.hasErrored) {
|
||||
+ if (self.hasErrored && _onFastImageError != NULL) {
|
||||
_onFastImageError(@{});
|
||||
}
|
||||
}
|
||||
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
@@ -19,6 +19,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@d11/react-native-fast-image':
|
||||
specifier: ^8.9.2
|
||||
version: 8.9.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)
|
||||
'@react-native-async-storage/async-storage':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))
|
||||
@@ -49,6 +52,9 @@ importers:
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^2.7.0
|
||||
version: 2.7.0(react-redux@9.2.0(@types/react@18.3.20)(react@19.0.0)(redux@5.0.1))(react@19.0.0)
|
||||
'@shopify/flash-list':
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0(@babel/runtime@7.27.1)(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)
|
||||
'@shopify/react-native-skia':
|
||||
specifier: 2.0.0-next.3
|
||||
version: 2.0.0-next.3(react-native-reanimated@3.17.5(@babel/core@7.27.1)(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)
|
||||
@@ -88,9 +94,6 @@ importers:
|
||||
react-native-dotenv:
|
||||
specifier: ^3.4.11
|
||||
version: 3.4.11(@babel/runtime@7.27.1)
|
||||
react-native-fast-image:
|
||||
specifier: ^8.6.3
|
||||
version: 8.6.3(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)
|
||||
react-native-fs:
|
||||
specifier: ^2.20.0
|
||||
version: 2.20.0(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))
|
||||
@@ -791,6 +794,12 @@ packages:
|
||||
resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@d11/react-native-fast-image@8.9.2':
|
||||
resolution: {integrity: sha512-7d0O5mA17SCbIAYq9D0WxDlS3HXf5v9vg9ltphDfP/3R57sBEf6/wby/sHkOmrQGwvgUlz2oSo5Ld8hgocLJeQ==}
|
||||
peerDependencies:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
'@egjs/hammerjs@2.0.17':
|
||||
resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
@@ -1355,6 +1364,13 @@ packages:
|
||||
resolution: {integrity: sha512-JL8UDjrsKxKclTdLXfuHfE7B3KbrAPEYP7tMyN/xiO2vsF6D84fjwYyalO0ZMtuFZE6vpSze8ZOLEh6hLnPYsw==}
|
||||
engines: {node: '>=14.18'}
|
||||
|
||||
'@shopify/flash-list@1.8.0':
|
||||
resolution: {integrity: sha512-APZ48kceCCJobUimmI2594io+HujELK60HFKgzIyIdHGX5ySR5YfvsPy3PKtPwHHDtIMFNaq3U/BY3qZocOhCA==}
|
||||
peerDependencies:
|
||||
'@babel/runtime': '*'
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
'@shopify/react-native-skia@2.0.0-next.3':
|
||||
resolution: {integrity: sha512-mm9oc8fPhh7hOr6kYk/xmcCOb7NZVk9+BRd6mukW5E563Wc/8loDeNtWMMVRsBGAUblYKj91WTmcda9gACeAHA==}
|
||||
hasBin: true
|
||||
@@ -3830,12 +3846,6 @@ packages:
|
||||
react: '*'
|
||||
react-native: '*'
|
||||
|
||||
react-native-fast-image@8.6.3:
|
||||
resolution: {integrity: sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==}
|
||||
peerDependencies:
|
||||
react: ^17 || ^18
|
||||
react-native: '>=0.60.0'
|
||||
|
||||
react-native-fs@2.20.0:
|
||||
resolution: {integrity: sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==}
|
||||
peerDependencies:
|
||||
@@ -3961,6 +3971,12 @@ packages:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
recyclerlistview@4.2.3:
|
||||
resolution: {integrity: sha512-STR/wj/FyT8EMsBzzhZ1l2goYirMkIgfV3gYEPxI3Kf3lOnu6f7Dryhyw7/IkQrgX5xtTcDrZMqytvteH9rL3g==}
|
||||
peerDependencies:
|
||||
react: '>= 15.2.1'
|
||||
react-native: '>= 0.30.0'
|
||||
|
||||
redux-persist@6.0.0:
|
||||
resolution: {integrity: sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==}
|
||||
peerDependencies:
|
||||
@@ -4429,6 +4445,9 @@ packages:
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
ts-object-utils@0.0.5:
|
||||
resolution: {integrity: sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==}
|
||||
|
||||
tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
|
||||
@@ -5425,6 +5444,11 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@d11/react-native-fast-image@8.9.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0)
|
||||
|
||||
'@egjs/hammerjs@2.0.17':
|
||||
dependencies:
|
||||
'@types/hammerjs': 2.0.46
|
||||
@@ -6387,6 +6411,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@sentry/core': 8.54.0
|
||||
|
||||
'@shopify/flash-list@1.8.0(@babel/runtime@7.27.1)(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.27.1
|
||||
react: 19.0.0
|
||||
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0)
|
||||
recyclerlistview: 4.2.3(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@shopify/react-native-skia@2.0.0-next.3(react-native-reanimated@3.17.5(@babel/core@7.27.1)(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0))(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
canvaskit-wasm: 0.40.0
|
||||
@@ -9350,11 +9382,6 @@ snapshots:
|
||||
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0)
|
||||
optional: true
|
||||
|
||||
react-native-fast-image@8.6.3(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0)
|
||||
|
||||
react-native-fs@2.20.0(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0)):
|
||||
dependencies:
|
||||
base-64: 0.1.0
|
||||
@@ -9524,6 +9551,14 @@ snapshots:
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
recyclerlistview@4.2.3(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
lodash.debounce: 4.0.8
|
||||
prop-types: 15.8.1
|
||||
react: 19.0.0
|
||||
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@18.0.0(typescript@5.8.3))(@types/react@18.3.20)(react@19.0.0)
|
||||
ts-object-utils: 0.0.5
|
||||
|
||||
redux-persist@6.0.0(react@19.0.0)(redux@5.0.1):
|
||||
dependencies:
|
||||
redux: 5.0.1
|
||||
@@ -10061,6 +10096,8 @@ snapshots:
|
||||
ts-interface-checker@0.1.13:
|
||||
optional: true
|
||||
|
||||
ts-object-utils@0.0.5: {}
|
||||
|
||||
tslib@2.6.2: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const ALBUM_CACHE_AMOUNT_OF_DAYS = 7;
|
||||
export const PLAYLIST_CACHE_AMOUNT_OF_DAYS = 7;
|
||||
export const ALPHABET_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ';
|
||||
export const ALPHABET_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#';
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TapGestureHandlerGestureEvent
|
||||
} from 'react-native-gesture-handler';
|
||||
import useDefaultStyles from './Colors';
|
||||
import { useNavigationOffsets } from './SafeNavigatorView';
|
||||
|
||||
// interface LetterContainerProps {
|
||||
// onPress: (letter: string) => void;
|
||||
@@ -17,23 +18,21 @@ import useDefaultStyles from './Colors';
|
||||
|
||||
const Container = styled.View`
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
padding: 5px;
|
||||
margin: auto 0;
|
||||
justify-content: space-around;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Letter = styled.Text`
|
||||
const Letter = styled.Text<{ isSelected?: boolean }>`
|
||||
text-align: center;
|
||||
padding: 1px 0;
|
||||
padding: 1.5px 10px;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
onSelect: (index: number) => void;
|
||||
onSelect: (selected: { index: number, letter: string }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +43,7 @@ const AlphabetScroller: React.FC<Props> = ({ onSelect }) => {
|
||||
const styles = useDefaultStyles();
|
||||
const [ height, setHeight ] = useState(0);
|
||||
const [ index, setIndex ] = useState<number>();
|
||||
const { top, bottom } = useNavigationOffsets();
|
||||
|
||||
// Handler for setting the correct height for a single alphabet item
|
||||
const handleLayout = useCallback((event: LayoutChangeEvent) => {
|
||||
@@ -52,16 +52,20 @@ const AlphabetScroller: React.FC<Props> = ({ onSelect }) => {
|
||||
|
||||
// Handler for passing on a new index when it is tapped or swiped
|
||||
const handleGestureEvent = useCallback((event: PanGestureHandlerGestureEvent | TapGestureHandlerGestureEvent) => {
|
||||
const newIndex = Math.floor(event.nativeEvent.y / height);
|
||||
const { y } = event.nativeEvent;
|
||||
const newIndex = Math.min(
|
||||
Math.max(0, Math.floor(y / height)),
|
||||
ALPHABET_LETTERS.length - 1
|
||||
);
|
||||
|
||||
if (newIndex !== index) {
|
||||
setIndex(newIndex);
|
||||
onSelect(newIndex);
|
||||
onSelect({ index: newIndex, letter: ALPHABET_LETTERS[newIndex] });
|
||||
}
|
||||
}, [height, index, onSelect]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Container style={{ top, bottom }}>
|
||||
<TapGestureHandler onHandlerStateChange={handleGestureEvent}>
|
||||
<PanGestureHandler onGestureEvent={handleGestureEvent}>
|
||||
<View>
|
||||
@@ -70,7 +74,10 @@ const AlphabetScroller: React.FC<Props> = ({ onSelect }) => {
|
||||
key={l}
|
||||
onLayout={i === 0 ? handleLayout : undefined}
|
||||
>
|
||||
<Letter style={styles.themeColor}>
|
||||
<Letter
|
||||
style={styles.themeColor}
|
||||
isSelected={i === index}
|
||||
>
|
||||
{l}
|
||||
</Letter>
|
||||
</View>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { ColorSchemeProvider, themes, useUserOrSystemScheme } from './Colors';
|
||||
import DownloadManager from './DownloadManager';
|
||||
import AppLoading from './AppLoading';
|
||||
import { captureException } from '@sentry/react-native';
|
||||
|
||||
const LightTheme = {
|
||||
...DefaultTheme,
|
||||
@@ -52,7 +53,9 @@ export default function App(): JSX.Element | null {
|
||||
|
||||
useEffect(() => {
|
||||
async function setupTrackPlayer() {
|
||||
await TrackPlayer.setupPlayer({ autoHandleInterruptions: true });
|
||||
await TrackPlayer.setupPlayer({
|
||||
autoHandleInterruptions: true,
|
||||
});
|
||||
await TrackPlayer.updateOptions({
|
||||
capabilities: [
|
||||
Capability.Play,
|
||||
@@ -68,7 +71,12 @@ export default function App(): JSX.Element | null {
|
||||
}
|
||||
|
||||
if (!hasSetupPlayer) {
|
||||
setupTrackPlayer();
|
||||
setupTrackPlayer()
|
||||
.catch((e: unknown) => {
|
||||
console.error(e);
|
||||
captureException(e);
|
||||
setHasSetupPlayer(true);
|
||||
});
|
||||
}
|
||||
}, [hasSetupPlayer]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Dimensions, ViewProps } from 'react-native';
|
||||
import { Canvas, Blur, Image as SkiaImage, useImage, Offset, Mask, RoundedRect, Shadow } from '@shopify/react-native-skia';
|
||||
import useDefaultStyles, { useUserOrSystemScheme } from './Colors';
|
||||
@@ -28,7 +28,7 @@ interface Props {
|
||||
margin?: number;
|
||||
radius?: number;
|
||||
style?: ViewProps['style'];
|
||||
src: string;
|
||||
src?: string;
|
||||
}
|
||||
|
||||
const emptyAlbumLight = require('@/assets/images/empty-album-light.png');
|
||||
@@ -49,9 +49,8 @@ function CoverImage({
|
||||
}: Props) {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const colorScheme = useUserOrSystemScheme();
|
||||
const [hasFailed, setFailed] = useState(false);
|
||||
|
||||
const image = useImage(src || null, () => setFailed(true));
|
||||
const image = useImage(src || null);
|
||||
const fallback = useImage(colorScheme === 'light' ? emptyAlbumLight : emptyAlbumDark);
|
||||
const { canvasSize, imageSize } = useMemo(() => {
|
||||
const imageSize = Screen.width - margin;
|
||||
@@ -69,7 +68,7 @@ function CoverImage({
|
||||
<Shadow dx={0} dy={8} blur={16} color="#0000000d" />
|
||||
<Shadow dx={0} dy={16} blur={32} color="#0000000d" />
|
||||
</RoundedRect>
|
||||
{src && (
|
||||
{src ? (
|
||||
<>
|
||||
<SkiaImage
|
||||
image={image}
|
||||
@@ -97,8 +96,7 @@ function CoverImage({
|
||||
</SkiaImage>
|
||||
</Mask>
|
||||
</>
|
||||
)}
|
||||
{(!src || hasFailed) && (
|
||||
) : (
|
||||
<Mask
|
||||
mask={
|
||||
<RoundedRect
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useHeaderHeight } from '@react-navigation/elements';
|
||||
import { FlatList, FlatListProps, ScrollView, ScrollViewProps, SectionList, SectionListProps } from 'react-native';
|
||||
import useCurrentTrack from '../utility/useCurrentTrack';
|
||||
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
|
||||
import { FlashList, FlashListProps } from '@shopify/flash-list';
|
||||
|
||||
declare module 'react' {
|
||||
function forwardRef<T, P = {}>(
|
||||
@@ -26,7 +27,7 @@ export function SafeScrollView({
|
||||
contentContainerStyle,
|
||||
{ paddingTop: top, paddingBottom: bottom },
|
||||
]}
|
||||
scrollIndicatorInsets={{ top: top / 2, bottom: bottom / 2 + 5 }}
|
||||
scrollIndicatorInsets={{ top: top / 2, bottom: bottom / 2 + 5 }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -48,7 +49,7 @@ function BareSafeSectionList<I, S>({
|
||||
{ paddingTop: top, paddingBottom: bottom },
|
||||
contentContainerStyle,
|
||||
]}
|
||||
scrollIndicatorInsets={{ top: top / 2, bottom: bottom / 2 + 5 }}
|
||||
scrollIndicatorInsets={{ top: top / 2, bottom: bottom / 2 + 5 }}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
@@ -81,6 +82,30 @@ function BareSafeFlatList<I>({
|
||||
|
||||
export const SafeFlatList = forwardRef(BareSafeFlatList);
|
||||
|
||||
/**
|
||||
* A wrapper for ScrollView that takes any paddings, margins and insets into
|
||||
* account that result from the bottom tabs, potential NowPlaying overlay and header.
|
||||
*/
|
||||
function BareSafeFlashList<I>({
|
||||
contentContainerStyle,
|
||||
...props
|
||||
}: FlashListProps<I>, ref: ForwardedRef<FlashList<I>>) {
|
||||
const { top, bottom } = useNavigationOffsets();
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
contentContainerStyle={
|
||||
{ ...contentContainerStyle, paddingBottom: bottom }
|
||||
}
|
||||
scrollIndicatorInsets={{ top: top * 0.4, bottom: bottom * 0.55 }}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const SafeFlashList = forwardRef(BareSafeFlashList);
|
||||
|
||||
/**
|
||||
* A hook that returns the correct offset that should be applied to any Views
|
||||
* that are wrapped in a NavigationView, in order to account for overlays,
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"download-track": "Download Track",
|
||||
"download-album": "Download Album",
|
||||
"download-playlist": "Download Playlist",
|
||||
"no-downloads": "You have not yet downloaded any tracks",
|
||||
"no-downloads": "You have not downloaded any tracks yet",
|
||||
"delete-track": "Delete Track",
|
||||
"delete-all-tracks": "Delete All Tracks",
|
||||
"confirm-delete-all-tracks": "Are you sure you want to delete all currently downloaded tracks?",
|
||||
|
||||
@@ -136,7 +136,7 @@ function Downloads() {
|
||||
<DownloadedTrack>
|
||||
<View style={{ marginRight: 12 }}>
|
||||
<ShadowWrapper size="small">
|
||||
<AlbumImage source={{ uri: getImage(item) }} style={defaultStyles.imageBackground} />
|
||||
<AlbumImage source={{ uri: getImage(tracks[item]) }} style={defaultStyles.imageBackground} />
|
||||
</ShadowWrapper>
|
||||
</View>
|
||||
<View style={{ flexShrink: 1, marginRight: 8 }}>
|
||||
@@ -182,7 +182,7 @@ function Downloads() {
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
{ListHeaderComponent}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { StatusBar, StyleSheet } from 'react-native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { t } from '@/localisation';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import useDefaultStyles, { ColoredBlurView, useUserOrSystemScheme } from '@/components/Colors';
|
||||
import { StackParams } from '@/screens/types';
|
||||
import NowPlaying from './overlays/NowPlaying';
|
||||
|
||||
@@ -14,31 +14,36 @@ import Playlists from './stacks/Playlists';
|
||||
import Playlist from './stacks/Playlist';
|
||||
import Artists from './stacks/Artists';
|
||||
import Artist from './stacks/Artist';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
const Stack = createStackNavigator<StackParams>();
|
||||
|
||||
function MusicStack() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const scheme = useUserOrSystemScheme();
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack.Navigator initialRouteName="RecentAlbums" screenOptions={{
|
||||
headerTintColor: defaultStyles.themeColor.color,
|
||||
headerTitleStyle: defaultStyles.stackHeader,
|
||||
cardStyle: defaultStyles.view,
|
||||
headerTransparent: true,
|
||||
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
|
||||
}}>
|
||||
<Stack.Screen name="RecentAlbums" component={RecentAlbums} options={{ headerTitle: t('recent-albums'), headerShown: false }} />
|
||||
<Stack.Screen name="Albums" component={Albums} options={{ headerTitle: t('albums') }} />
|
||||
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
|
||||
<Stack.Screen name="Artists" component={Artists} options={{ headerTitle: t('artists') }} />
|
||||
<Stack.Screen name="Artist" component={Artist} options={({ route }) => ({ headerTitle: route.params.Name })} />
|
||||
<Stack.Screen name="Playlists" component={Playlists} options={{ headerTitle: t('playlists') }} />
|
||||
<Stack.Screen name="Playlist" component={Playlist} options={{ headerTitle: t('playlist') }} />
|
||||
</Stack.Navigator>
|
||||
<NowPlaying />
|
||||
</GestureHandlerRootView>
|
||||
<SafeAreaProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<StatusBar backgroundColor="transparent" barStyle={scheme === 'dark' ? 'light-content' : 'dark-content'} />
|
||||
<Stack.Navigator initialRouteName="RecentAlbums" screenOptions={{
|
||||
headerTintColor: defaultStyles.themeColor.color,
|
||||
headerTitleStyle: defaultStyles.stackHeader,
|
||||
cardStyle: defaultStyles.view,
|
||||
headerTransparent: true,
|
||||
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
|
||||
}}>
|
||||
<Stack.Screen name="RecentAlbums" component={RecentAlbums} options={{ headerTitle: t('recent-albums'), headerShown: false }} />
|
||||
<Stack.Screen name="Albums" component={Albums} options={{ headerTitle: t('albums') }} />
|
||||
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
|
||||
<Stack.Screen name="Artists" component={Artists} options={{ headerTitle: t('artists') }} />
|
||||
<Stack.Screen name="Artist" component={Artist} options={({ route }) => ({ headerTitle: route.params.Name })} />
|
||||
<Stack.Screen name="Playlists" component={Playlists} options={{ headerTitle: t('playlists') }} />
|
||||
<Stack.Screen name="Playlist" component={Playlist} options={{ headerTitle: t('playlist') }} />
|
||||
</Stack.Navigator>
|
||||
<NowPlaying />
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect, useRef, ReactText, useMemo } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { SectionList, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
@@ -8,7 +8,7 @@ import { fetchAllAlbums } from '@/store/music/actions';
|
||||
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from '@/CONSTANTS';
|
||||
import TouchableHandler from '@/components/TouchableHandler';
|
||||
import AlbumImage, { AlbumHeight, AlbumItem } from './components/AlbumImage';
|
||||
import { selectAlbumsByAlphabet, SectionedId } from '@/store/music/selectors';
|
||||
import { selectAlbumsByAlphabet } from '@/store/music/selectors';
|
||||
import AlphabetScroller from '@/components/AlphabetScroller';
|
||||
import styled from 'styled-components/native';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
@@ -16,20 +16,12 @@ import { Album } from '@/store/music/types';
|
||||
import { Text } from '@/components/Typography';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { SafeSectionList } from '@/components/SafeNavigatorView';
|
||||
|
||||
const HeadingHeight = 50;
|
||||
|
||||
function generateSection({ section }: { section: SectionedId }) {
|
||||
return (
|
||||
<SectionHeading label={section.label} key={section.label} />
|
||||
);
|
||||
}
|
||||
import { SafeFlashList, useNavigationOffsets } from '@/components/SafeNavigatorView';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
|
||||
const SectionContainer = styled.View`
|
||||
height: ${HeadingHeight}px;
|
||||
justify-content: center;
|
||||
padding: 0 24px;
|
||||
padding: 12px 24px;
|
||||
`;
|
||||
|
||||
const SectionText = styled(Text)`
|
||||
@@ -37,21 +29,26 @@ const SectionText = styled(Text)`
|
||||
font-weight: 400;
|
||||
`;
|
||||
|
||||
const SectionHeading = React.memo(function SectionHeading(props: { label: string }) {
|
||||
const SectionHeading = React.memo(function SectionHeading(props: {
|
||||
label: string;
|
||||
}) {
|
||||
const { top } = useNavigationOffsets();
|
||||
const { label } = props;
|
||||
|
||||
return (
|
||||
<ColoredBlurView>
|
||||
<SectionContainer>
|
||||
<SectionText>{label}</SectionText>
|
||||
</SectionContainer>
|
||||
</ColoredBlurView>
|
||||
<View style={{ paddingTop: top }}>
|
||||
<ColoredBlurView>
|
||||
<SectionContainer>
|
||||
<SectionText>{label}</SectionText>
|
||||
</SectionContainer>
|
||||
</ColoredBlurView>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
interface GeneratedAlbumItemProps {
|
||||
id: ReactText;
|
||||
imageUrl: string;
|
||||
imageUrl?: string | null;
|
||||
name: string;
|
||||
artist: string;
|
||||
onPress: (id: string) => void;
|
||||
@@ -69,7 +66,7 @@ const GeneratedAlbumItem = React.memo(function GeneratedAlbumItem(props: Generat
|
||||
<TouchableHandler id={id as string} onPress={onPress}>
|
||||
<AlbumItem>
|
||||
<ShadowWrapper size="medium">
|
||||
<AlbumImage source={{ uri: imageUrl }} style={[defaultStyles.imageBackground]} />
|
||||
<AlbumImage source={imageUrl ? { uri: imageUrl } : undefined} style={[defaultStyles.imageBackground]} />
|
||||
</ShadowWrapper>
|
||||
<Text numberOfLines={1} style={defaultStyles.text}>{name}</Text>
|
||||
<HalfOpacity style={defaultStyles.text} numberOfLines={1}>{artist}</HalfOpacity>
|
||||
@@ -89,65 +86,55 @@ const Albums: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const getImage = useGetImage();
|
||||
const listRef = useRef<SectionList<string[]>>(null);
|
||||
const listRef = useRef<FlashList<any>>(null);
|
||||
|
||||
// Create an array that computes all the height data for the entire list in
|
||||
// advance. We can then use this pre-computed data to respond to
|
||||
// `getItemLayout` calls, without having to compute things in place (and
|
||||
// fail horribly).
|
||||
// This approach was inspired by https://gist.github.com/RaphBlanchet/472ed013e05398c083caae6216b598b5
|
||||
const itemLayouts = useMemo(() => {
|
||||
// Create an array in which we will store all possible outputs for
|
||||
// `getItemLayout`. We will loop through each potential album and add
|
||||
// items that will be in the list
|
||||
const layouts: Array<{ length: number; offset: number; index: number }> = [];
|
||||
|
||||
// Keep track of both the index of items and the offset (in pixels) from
|
||||
// the top
|
||||
let index = 0;
|
||||
let offset = 0;
|
||||
|
||||
// Loop through each individual section (i.e. alphabet letter) and add
|
||||
// all items in that particular section.
|
||||
// Convert sections to flat array format for FlashList
|
||||
const flatData = useMemo(() => {
|
||||
const data: (string | string[])[] = [];
|
||||
sections.forEach((section) => {
|
||||
// Each section starts with a header, so we'll need to add the item,
|
||||
// as well as the offset.
|
||||
layouts[index] = ({ length: HeadingHeight, offset, index });
|
||||
index++;
|
||||
offset += HeadingHeight;
|
||||
|
||||
// Then, loop through all the rows (sets of two albums) and add
|
||||
// items for those as well.
|
||||
section.data.forEach(() => {
|
||||
layouts[index] = ({ length: AlbumHeight, offset, index });
|
||||
index++;
|
||||
offset += AlbumHeight;
|
||||
if (!section.data.length || !section.data[0].length) return;
|
||||
// Add section header
|
||||
data.push(section.label);
|
||||
// Add section items
|
||||
section.data.forEach((item) => {
|
||||
data.push(item);
|
||||
});
|
||||
|
||||
// The way SectionList works is that you get an item for a
|
||||
// SectionHeader and a SectionFooter, no matter if you've specified
|
||||
// whether you want them or not. Thus, we will need to add an empty
|
||||
// footer as an item, so that we don't mismatch our indexes
|
||||
layouts[index] = { length: 0, offset, index };
|
||||
index++;
|
||||
});
|
||||
|
||||
// Then, store and memoize the output
|
||||
return layouts;
|
||||
return data;
|
||||
}, [sections]);
|
||||
|
||||
// Compute sticky header indices
|
||||
const stickyHeaderIndices = useMemo(() => {
|
||||
return flatData
|
||||
.map((item, index) => typeof item === 'string' ? index : null)
|
||||
.filter((item): item is number => item !== null);
|
||||
}, [flatData]);
|
||||
|
||||
// Set callbacks
|
||||
const retrieveData = useCallback(() => dispatch(fetchAllAlbums()), [dispatch]);
|
||||
const selectAlbum = useCallback((id: string) => navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]);
|
||||
const selectLetter = useCallback((sectionIndex: number) => {
|
||||
listRef.current?.scrollToLocation({ sectionIndex, itemIndex: 0, animated: false, });
|
||||
}, [listRef]);
|
||||
const generateItem = useCallback(({ item }: { item: string[] }) => {
|
||||
const selectLetter = useCallback(({ letter }: { letter: string, index: number }) => {
|
||||
const index = flatData.findIndex((item) => (
|
||||
typeof item === 'string' && item === letter
|
||||
));
|
||||
if (index !== -1) {
|
||||
listRef.current?.scrollToIndex({ index, animated: false });
|
||||
}
|
||||
}, [flatData]);
|
||||
|
||||
const renderItem = useCallback(({ item }: { item: string | string[]; index: number }) => {
|
||||
if (typeof item === 'string') {
|
||||
return (
|
||||
<SectionHeading
|
||||
label={item}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item.join('-')}>
|
||||
{item.map((id) => (
|
||||
<View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }}>
|
||||
{item.map((id, i) => (
|
||||
<GeneratedAlbumItem
|
||||
key={id}
|
||||
key={i}
|
||||
id={id}
|
||||
imageUrl={getImage(albums[id])}
|
||||
name={albums[id]?.Name || ''}
|
||||
@@ -161,7 +148,6 @@ const Albums: React.FC = () => {
|
||||
|
||||
// Retrieve data on mount
|
||||
useEffect(() => {
|
||||
// GUARD: Only refresh this API call every set amounts of days
|
||||
if (!lastRefreshed || differenceInDays(lastRefreshed, new Date()) > ALBUM_CACHE_AMOUNT_OF_DAYS) {
|
||||
retrieveData();
|
||||
}
|
||||
@@ -170,19 +156,18 @@ const Albums: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<AlphabetScroller onSelect={selectLetter} />
|
||||
<SafeSectionList
|
||||
sections={sections}
|
||||
<SafeFlashList
|
||||
data={flatData}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
getItemLayout={(_, i) => itemLayouts[i] ?? { length: 0, offset: 0, index: i }}
|
||||
ref={listRef}
|
||||
keyExtractor={(item) => item.join('-')}
|
||||
renderSectionHeader={generateSection}
|
||||
renderItem={generateItem}
|
||||
renderItem={renderItem}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
estimatedItemSize={AlbumHeight}
|
||||
getItemType={(item) => typeof item === 'string' ? 'sectionHeader' : 'row'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Albums;
|
||||
@@ -19,7 +19,7 @@ import { chunk } from 'lodash';
|
||||
|
||||
interface GeneratedAlbumItemProps {
|
||||
id: ReactText;
|
||||
imageUrl: string;
|
||||
imageUrl: string | undefined;
|
||||
name: string;
|
||||
artist: string;
|
||||
onPress: (id: string) => void;
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { SectionList, View } from 'react-native';
|
||||
import { View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import { fetchAllAlbums } from '@/store/music/actions';
|
||||
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from '@/CONSTANTS';
|
||||
import AlbumImage from './components/AlbumImage';
|
||||
import { SectionArtistItem, SectionedArtist, selectArtists } from '@/store/music/selectors';
|
||||
import { SectionArtistItem, selectArtists } from '@/store/music/selectors';
|
||||
import AlphabetScroller from '@/components/AlphabetScroller';
|
||||
import styled from 'styled-components/native';
|
||||
import useDefaultStyles, { ColoredBlurView } from '@/components/Colors';
|
||||
import { Text } from '@/components/Typography';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { SafeSectionList } from '@/components/SafeNavigatorView';
|
||||
import { SafeFlashList, useNavigationOffsets } from '@/components/SafeNavigatorView';
|
||||
import { Gap } from '@/components/Utility';
|
||||
|
||||
const HeadingHeight = 50;
|
||||
|
||||
function generateSection({ section }: { section: SectionedArtist }) {
|
||||
return (
|
||||
<SectionHeading label={section.label} key={section.label} />
|
||||
);
|
||||
}
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
|
||||
const SectionContainer = styled.View`
|
||||
height: ${HeadingHeight}px;
|
||||
justify-content: center;
|
||||
padding: 0 24px;
|
||||
padding: 12px 24px;
|
||||
`;
|
||||
|
||||
const SectionText = styled(Text)`
|
||||
@@ -51,20 +43,23 @@ const ArtistContainer = styled.Pressable`
|
||||
`;
|
||||
|
||||
const SectionHeading = React.memo(function SectionHeading(props: { label: string }) {
|
||||
const { top } = useNavigationOffsets();
|
||||
const { label } = props;
|
||||
|
||||
return (
|
||||
<ColoredBlurView>
|
||||
<SectionContainer>
|
||||
<SectionText>{label}</SectionText>
|
||||
</SectionContainer>
|
||||
</ColoredBlurView>
|
||||
<View style={{ paddingTop: top }}>
|
||||
<ColoredBlurView>
|
||||
<SectionContainer>
|
||||
<SectionText>{label}</SectionText>
|
||||
</SectionContainer>
|
||||
</ColoredBlurView>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
interface GeneratedArtistItemProps {
|
||||
item: SectionArtistItem;
|
||||
imageURL: string;
|
||||
imageURL: string | undefined;
|
||||
onPress: (payload: SectionArtistItem) => void;
|
||||
}
|
||||
|
||||
@@ -95,8 +90,6 @@ const GeneratedArtistItem = React.memo(function GeneratedArtistItem(props: Gener
|
||||
pressed && defaultStyles.themeColor,
|
||||
{ flexShrink: 1 }
|
||||
]}
|
||||
|
||||
|
||||
>
|
||||
{item.Name}
|
||||
</Text>
|
||||
@@ -108,7 +101,6 @@ const GeneratedArtistItem = React.memo(function GeneratedArtistItem(props: Gener
|
||||
|
||||
const Artists: React.FC = () => {
|
||||
// Retrieve data from store
|
||||
// const { entities: albums } = useTypedSelector((state) => state.music.albums);
|
||||
const isLoading = useTypedSelector((state) => state.music.albums.isLoading);
|
||||
const lastRefreshed = useTypedSelector((state) => state.music.albums.lastRefreshed);
|
||||
const sections = useTypedSelector(selectArtists);
|
||||
@@ -117,63 +109,50 @@ const Artists: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const getImage = useGetImage();
|
||||
const listRef = useRef<SectionList<SectionArtistItem>>(null);
|
||||
const listRef = useRef<FlashList<any>>(null);
|
||||
|
||||
// Create an array that computes all the height data for the entire list in
|
||||
// advance. We can then use this pre-computed data to respond to
|
||||
// `getItemLayout` calls, without having to compute things in place (and
|
||||
// fail horribly).
|
||||
// This approach was inspired by https://gist.github.com/RaphBlanchet/472ed013e05398c083caae6216b598b5
|
||||
const itemLayouts = useMemo(() => {
|
||||
// Create an array in which we will store all possible outputs for
|
||||
// `getItemLayout`. We will loop through each potential album and add
|
||||
// items that will be in the list
|
||||
const layouts: Array<{ length: number; offset: number; index: number }> = [];
|
||||
|
||||
// Keep track of both the index of items and the offset (in pixels) from
|
||||
// the top
|
||||
let index = 0;
|
||||
let offset = 0;
|
||||
|
||||
// Loop through each individual section (i.e. alphabet letter) and add
|
||||
// all items in that particular section.
|
||||
// Convert sections to flat array format for FlashList
|
||||
const flatData = useMemo(() => {
|
||||
const data: (string | SectionArtistItem)[] = [];
|
||||
sections.forEach((section) => {
|
||||
// Each section starts with a header, so we'll need to add the item,
|
||||
// as well as the offset.
|
||||
layouts[index] = ({ length: HeadingHeight, offset, index });
|
||||
index++;
|
||||
offset += HeadingHeight;
|
||||
|
||||
// Then, loop through all the rows and add items for those as well.
|
||||
section.data.forEach(() => {
|
||||
offset += ArtistHeight;
|
||||
layouts[index] = ({ length: ArtistHeight, offset, index });
|
||||
index++;
|
||||
if (!section.data.length) return;
|
||||
// Add section header
|
||||
data.push(section.label);
|
||||
// Add section items
|
||||
section.data.forEach((item) => {
|
||||
data.push(item);
|
||||
});
|
||||
|
||||
// The way SectionList works is that you get an item for a
|
||||
// SectionHeader and a SectionFooter, no matter if you've specified
|
||||
// whether you want them or not. Thus, we will need to add an empty
|
||||
// footer as an item, so that we don't mismatch our indexes
|
||||
layouts[index] = { length: 0, offset, index };
|
||||
index++;
|
||||
});
|
||||
|
||||
// Then, store and memoize the output
|
||||
return layouts;
|
||||
return data;
|
||||
}, [sections]);
|
||||
|
||||
// Compute sticky header indices
|
||||
const stickyHeaderIndices = useMemo(() => {
|
||||
return flatData
|
||||
.map((item, index) => typeof item === 'string' ? index : null)
|
||||
.filter((item): item is number => item !== null);
|
||||
}, [flatData]);
|
||||
|
||||
// Set callbacks
|
||||
const retrieveData = useCallback(() => dispatch(fetchAllAlbums()), [dispatch]);
|
||||
const selectArtist = useCallback((payload: SectionArtistItem) => (
|
||||
navigation.navigate('Artist', payload)
|
||||
), [navigation]);
|
||||
const selectLetter = useCallback((sectionIndex: number) => {
|
||||
listRef.current?.scrollToLocation({ sectionIndex, itemIndex: 0, animated: false, });
|
||||
}, [listRef]);
|
||||
const generateItem = useCallback(({ item }: { item: SectionArtistItem }) => {
|
||||
const selectLetter = useCallback(({ letter }: { letter: string, index: number }) => {
|
||||
const index = flatData.findIndex((item) => (
|
||||
typeof item === 'string' && item === letter
|
||||
));
|
||||
if (index !== -1) {
|
||||
listRef.current?.scrollToIndex({ index, animated: false });
|
||||
}
|
||||
}, [flatData]);
|
||||
|
||||
const renderItem = useCallback(({ item }: { item: string | SectionArtistItem }) => {
|
||||
if (typeof item === 'string') {
|
||||
return <SectionHeading label={item} />;
|
||||
}
|
||||
return (
|
||||
<View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item.Id}>
|
||||
<View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 32 }}>
|
||||
<GeneratedArtistItem
|
||||
key={item.Id}
|
||||
item={item}
|
||||
@@ -186,7 +165,6 @@ const Artists: React.FC = () => {
|
||||
|
||||
// Retrieve data on mount
|
||||
useEffect(() => {
|
||||
// GUARD: Only refresh this API call every set amounts of days
|
||||
if (!lastRefreshed || differenceInDays(lastRefreshed, new Date()) > ALBUM_CACHE_AMOUNT_OF_DAYS) {
|
||||
retrieveData();
|
||||
}
|
||||
@@ -195,24 +173,18 @@ const Artists: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<AlphabetScroller onSelect={selectLetter} />
|
||||
<SafeSectionList
|
||||
sections={sections}
|
||||
<SafeFlashList
|
||||
data={flatData}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
getItemLayout={(_, i) => {
|
||||
if (!(i in itemLayouts)) {
|
||||
// console.log('COuLD NOT FIND LAYOUT ITEM', i, _);
|
||||
}
|
||||
return itemLayouts[i] ?? { length: 0, offset: 0, index: i };
|
||||
}}
|
||||
ref={listRef}
|
||||
keyExtractor={(item) => item.Id}
|
||||
renderSectionHeader={generateSection}
|
||||
renderItem={generateItem}
|
||||
renderItem={renderItem}
|
||||
stickyHeaderIndices={stickyHeaderIndices}
|
||||
estimatedItemSize={ArtistHeight}
|
||||
getItemType={(item) => typeof item === 'string' ? 'sectionHeader' : 'row'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Artists;
|
||||
@@ -14,7 +14,7 @@ import { SafeFlatList, useNavigationOffsets } from '@/components/SafeNavigatorVi
|
||||
|
||||
interface GeneratedAlbumItemProps {
|
||||
id: ReactText;
|
||||
imageUrl: string;
|
||||
imageUrl?: string | null;
|
||||
name: string;
|
||||
onPress: (id: string) => void;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ const GeneratedPlaylistItem = React.memo(function GeneratedPlaylistItem(props: G
|
||||
return (
|
||||
<TouchableHandler id={id as string} onPress={onPress}>
|
||||
<AlbumItem>
|
||||
<AlbumImage source={{ uri: imageUrl }} style={defaultStyles.imageBackground} />
|
||||
<AlbumImage source={{ uri: imageUrl || undefined }} style={defaultStyles.imageBackground} />
|
||||
<Text numberOfLines={1} style={defaultStyles.text}>{name}</Text>
|
||||
</AlbumItem>
|
||||
</TouchableHandler>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useGetImage } from '@/utility/JellyfinApi/lib';
|
||||
import { Text, SafeAreaView, StyleSheet } from 'react-native';
|
||||
import { Text, StyleSheet, View } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import { fetchRecentAlbums } from '@/store/music/actions';
|
||||
@@ -18,6 +18,7 @@ import styled from 'styled-components/native';
|
||||
import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { SafeFlatList } from '@/components/SafeNavigatorView';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
columnWrapper: {
|
||||
@@ -36,7 +37,7 @@ const NavigationHeader: React.FC = () => {
|
||||
const handleAllAlbumsClick = useCallback(() => { navigation.navigate('Albums'); }, [navigation]);
|
||||
const handlePlaylistsClick = useCallback(() => { navigation.navigate('Playlists'); }, [navigation]);
|
||||
const handleArtistsClick = useCallback(() => { navigation.navigate('Artists'); }, [navigation]);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListButton onPress={handleAllAlbumsClick} testID="all-albums">
|
||||
@@ -65,7 +66,7 @@ const RecentAlbums: React.FC = () => {
|
||||
const { entities: albums } = useTypedSelector((state) => state.music.albums);
|
||||
const recentAlbums = useRecentAlbums(24);
|
||||
const isLoading = useTypedSelector((state) => state.music.albums.isLoading);
|
||||
|
||||
|
||||
// Initialise helpers
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
@@ -74,14 +75,20 @@ const RecentAlbums: React.FC = () => {
|
||||
// Set callbacks
|
||||
const retrieveData = useCallback(() => dispatch(fetchRecentAlbums()), [dispatch]);
|
||||
const selectAlbum = useCallback((id: string) => navigation.navigate('Album', { id, album: albums[id] as Album }), [navigation, albums]);
|
||||
|
||||
|
||||
// Retrieve data on mount
|
||||
useEffect(() => { retrieveData(); }, [retrieveData]);
|
||||
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<View
|
||||
style={{
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: 1 * insets.bottom,
|
||||
}}>
|
||||
<SafeFlatList
|
||||
data={recentAlbums as string[]}
|
||||
data={recentAlbums as string[]}
|
||||
refreshing={isLoading}
|
||||
onRefresh={retrieveData}
|
||||
numColumns={2}
|
||||
@@ -100,7 +107,7 @@ const RecentAlbums: React.FC = () => {
|
||||
</TouchableHandler>
|
||||
)}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
import FastImage, { FastImageProps } from 'react-native-fast-image';
|
||||
import FastImage, { FastImageProps, Source } from '@d11/react-native-fast-image';
|
||||
import { Dimensions } from 'react-native';
|
||||
import { useUserOrSystemScheme } from '@/components/Colors';
|
||||
|
||||
@@ -22,24 +22,29 @@ const Container = styled(FastImage)`
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
function AlbumImage(props: FastImageProps) {
|
||||
const [hasError, setError] = useState(false);
|
||||
const colorScheme = useUserOrSystemScheme();
|
||||
export interface AlbumImageProps extends FastImageProps {
|
||||
source: number | Omit<Source, 'uri'> & {
|
||||
uri?: string;
|
||||
};
|
||||
}
|
||||
|
||||
if (!props.source || hasError) {
|
||||
return (
|
||||
<Container
|
||||
{...props}
|
||||
source={colorScheme === 'light'
|
||||
? require('@/assets/images/empty-album-light.png')
|
||||
: require('@/assets/images/empty-album-dark.png')
|
||||
}
|
||||
/>
|
||||
);
|
||||
const defaultImageDark = require('@/assets/images/empty-album-dark.png');
|
||||
const defaultImageLight = require('@/assets/images/empty-album-light.png');
|
||||
|
||||
function AlbumImage(props: FastImageProps) {
|
||||
const colorScheme = useUserOrSystemScheme();
|
||||
const defaultImage = colorScheme === 'light' ? defaultImageLight : defaultImageDark;
|
||||
|
||||
// If no source is provided, use the default image as the main source
|
||||
if (!props.source || (typeof props.source === 'object' && 'uri' in props.source && !props.source.uri)) {
|
||||
return <Container {...props} source={defaultImage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container {...props} onError={() => setError(true)} />
|
||||
<Container
|
||||
{...props}
|
||||
defaultSource={defaultImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
||||
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
|
||||
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
|
||||
const entity = useTypedSelector((state) => itemDisplayStyle === 'album' ? state.music.albums.entities[entityId] : state.music.playlists.entities[entityId]);
|
||||
const totalDuration = useMemo(() => (
|
||||
trackIds.reduce<number>((sum, trackId) => (
|
||||
sum + (tracks[trackId]?.RunTimeTicks || 0)
|
||||
@@ -173,7 +174,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
||||
>
|
||||
<View style={{ padding: 24, paddingTop: 32, paddingBottom: 32 }}>
|
||||
<AlbumImageContainer>
|
||||
<CoverImage src={getImage(entityId)} />
|
||||
<CoverImage src={getImage(entity)} />
|
||||
</AlbumImageContainer>
|
||||
<Header>{title}</Header>
|
||||
<SubHeader>{artist}</SubHeader>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Album from '@/screens/Music/stacks/Album';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import NowPlaying from '@/screens/Music/overlays/NowPlaying';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||
|
||||
const Stack = createStackNavigator<StackParams>();
|
||||
|
||||
@@ -16,27 +17,29 @@ function SearchStack() {
|
||||
const [isInitialRoute, setIsInitialRoute] = useState(true);
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack.Navigator initialRouteName="Search"
|
||||
screenOptions={{
|
||||
headerTintColor: defaultStyles.themeColor.color,
|
||||
headerTitleStyle: defaultStyles.stackHeader,
|
||||
cardStyle: defaultStyles.view,
|
||||
headerTransparent: true,
|
||||
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
|
||||
}}
|
||||
screenListeners={{
|
||||
state: (e) => {
|
||||
const { state: { routes } } = e.data as { state: { routes?: { key: string, name: string }[] } };
|
||||
setIsInitialRoute(routes?.length === 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="Search" component={Search} options={{ headerTitle: t('search'), headerShown: false }} />
|
||||
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
|
||||
</Stack.Navigator>
|
||||
<NowPlaying offset={isInitialRoute ? 64 : 0} />
|
||||
</GestureHandlerRootView>
|
||||
<SafeAreaProvider>
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<Stack.Navigator initialRouteName="Search"
|
||||
screenOptions={{
|
||||
headerTintColor: defaultStyles.themeColor.color,
|
||||
headerTitleStyle: defaultStyles.stackHeader,
|
||||
cardStyle: defaultStyles.view,
|
||||
headerTransparent: true,
|
||||
headerBackground: () => <ColoredBlurView style={StyleSheet.absoluteFill} />,
|
||||
}}
|
||||
screenListeners={{
|
||||
state: (e) => {
|
||||
const { state: { routes } } = e.data as { state: { routes?: { key: string, name: string }[] } };
|
||||
setIsInitialRoute(routes?.length === 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="Search" component={Search} options={{ headerTitle: t('search'), headerShown: false }} />
|
||||
<Stack.Screen name="Album" component={Album} options={{ headerTitle: t('album') }} />
|
||||
</Stack.Navigator>
|
||||
<NowPlaying offset={isInitialRoute ? 64 : 0} />
|
||||
</GestureHandlerRootView>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import Input from '@/components/Input';
|
||||
import { ActivityIndicator, Animated, KeyboardAvoidingView, Platform, SafeAreaView, View } from 'react-native';
|
||||
import { ActivityIndicator, Animated, KeyboardAvoidingView, Platform, View } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import Fuse, { IFuseOptions } from 'fuse.js';
|
||||
@@ -21,6 +21,7 @@ import { ShadowWrapper } from '@/components/Shadow';
|
||||
import { NavigationProp } from '@/screens/types';
|
||||
import { useNavigationOffsets } from '@/components/SafeNavigatorView';
|
||||
import BaseAlbumImage from '@/screens/Music/stacks/components/AlbumImage';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
// import MicrophoneIcon from '@/assets/icons/microphone.svg';
|
||||
// import AlbumIcon from '@/assets/icons/collection.svg';
|
||||
// import TrackIcon from '@/assets/icons/note.svg';
|
||||
@@ -31,7 +32,8 @@ import BaseAlbumImage from '@/screens/Music/stacks/components/AlbumImage';
|
||||
|
||||
const KEYBOARD_OFFSET = Platform.select({
|
||||
ios: 0,
|
||||
android: 72,
|
||||
// Android 15+ has edge-to-edge support, changing the keyboard offset to 0
|
||||
android: Number.parseInt(Platform.Version as string) >= 35 ? 0 : 72,
|
||||
});
|
||||
const SEARCH_INPUT_HEIGHT = 62;
|
||||
|
||||
@@ -266,8 +268,10 @@ export default function Search() {
|
||||
...jellyfinResults,
|
||||
]), [fuseResults, jellyfinResults]);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, marginBottom: offsets.bottom }}>
|
||||
<View style={{ flex: 1, paddingTop: insets.top, marginBottom: offsets.bottom }}>
|
||||
<KeyboardAvoidingView behavior="height" style={{ flex: 1 }} keyboardVerticalOffset={KEYBOARD_OFFSET}>
|
||||
<FlatList
|
||||
keyboardShouldPersistTaps="handled"
|
||||
@@ -323,6 +327,6 @@ export default function Search() {
|
||||
) : null}
|
||||
{SearchInput}
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { Paragraph } from '@/components/Typography';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Switch } from 'react-native-gesture-handler';
|
||||
import { t } from '@/localisation';
|
||||
import { SafeScrollView } from '@/components/SafeNavigatorView';
|
||||
import { useAppDispatch, useTypedSelector } from '@/store';
|
||||
import { setEnablePlaybackReporting } from '@/store/settings/actions';
|
||||
import Container from '../components/Container';
|
||||
@@ -17,14 +16,12 @@ export default function PlaybackReporting() {
|
||||
}, [isEnabled, dispatch]);
|
||||
|
||||
return (
|
||||
<SafeScrollView>
|
||||
<Container>
|
||||
<Paragraph>{t('playback-reporting-description')}</Paragraph>
|
||||
<SwitchContainer>
|
||||
<SwitchLabel>{t('playback-reporting')}</SwitchLabel>
|
||||
<Switch value={isEnabled} onValueChange={toggleSwitch} />
|
||||
</SwitchContainer>
|
||||
</Container>
|
||||
</SafeScrollView>
|
||||
<Container>
|
||||
<Paragraph>{t('playback-reporting-description')}</Paragraph>
|
||||
<SwitchContainer>
|
||||
<SwitchLabel>{t('playback-reporting')}</SwitchLabel>
|
||||
<Switch value={isEnabled} onValueChange={toggleSwitch} />
|
||||
</SwitchContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -98,7 +98,7 @@ export default function Routes() {
|
||||
}} id="MAIN">
|
||||
<Stack.Screen name="Screens" component={Screens} />
|
||||
<Stack.Screen name="SetJellyfinServer" component={SetJellyfinServer} />
|
||||
<Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet' }} />
|
||||
<Stack.Screen name="TrackPopupMenu" component={TrackPopupMenu} options={{ presentation: 'formSheet', sheetCornerRadius: 10, sheetAllowedDetents: [0.85, 1.0]}} />
|
||||
<Stack.Screen name="ErrorReporting" component={ErrorReportingPopup} />
|
||||
<Stack.Screen name="Player" component={Player} />
|
||||
<Stack.Screen name="Lyrics" component={Lyrics} />
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import XmarkIcon from '@/assets/icons/xmark.svg';
|
||||
import { TouchableOpacity } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
|
||||
const Container = styled.View`
|
||||
padding: 6px 12px;
|
||||
const Container = styled.TouchableOpacity`
|
||||
padding: 12px 0px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
function BackButton() {
|
||||
@@ -16,10 +16,8 @@ function BackButton() {
|
||||
}, [navigation]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TouchableOpacity onPress={handlePress}>
|
||||
<XmarkIcon />
|
||||
</TouchableOpacity>
|
||||
<Container onPress={handlePress}>
|
||||
<XmarkIcon />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,10 +33,12 @@ const NumberBar = styled.View`
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const Number = styled(ReText)`
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const DragHandle = styled(Reanimated.View)`
|
||||
@@ -51,7 +53,7 @@ const DragHandle = styled(Reanimated.View)`
|
||||
|
||||
function ProgressBar() {
|
||||
const styles = useDefaultStyles();
|
||||
const { position, buffered } = useProgress();
|
||||
const { position, buffered } = useProgress(990);
|
||||
const { track } = useCurrentTrack();
|
||||
|
||||
const width = useSharedValue(0);
|
||||
@@ -205,8 +207,8 @@ function ProgressBar() {
|
||||
]}
|
||||
/>
|
||||
<NumberBar style={{ flex: 1 }}>
|
||||
<Number text={timePassed} style={timePassedStyles} />
|
||||
<Number text={timeRemaining} style={timeRemainingStyles} />
|
||||
<Number text={timePassed} style={[timePassedStyles]} />
|
||||
<Number text={timeRemaining} style={[timeRemainingStyles, { textAlign: 'right' }]} />
|
||||
</NumberBar>
|
||||
</Container>
|
||||
</GestureDetector>
|
||||
|
||||
@@ -6,7 +6,7 @@ import Queue from './components/Queue';
|
||||
import ConnectionNotice from './components/ConnectionNotice';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import StreamStatus from './components/StreamStatus';
|
||||
import {Platform} from 'react-native';
|
||||
import { Platform } from 'react-native';
|
||||
import BackButton from './components/Backbutton';
|
||||
import Timer from './components/Timer';
|
||||
import styled from 'styled-components/native';
|
||||
@@ -23,9 +23,11 @@ export default function Player() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<ColoredBlurView>
|
||||
{Platform.OS === 'android' && (<BackButton />)}
|
||||
<Queue header={(
|
||||
<>
|
||||
{Platform.OS === 'android' && (
|
||||
<BackButton />
|
||||
)}
|
||||
<NowPlaying />
|
||||
<ConnectionNotice />
|
||||
<StreamStatus />
|
||||
|
||||
@@ -75,7 +75,7 @@ function TrackPopupMenu() {
|
||||
}, [trackId, dispatch, closeModal]);
|
||||
|
||||
return (
|
||||
<ColoredBlurView>
|
||||
<ColoredBlurView style={{flex: 1}}>
|
||||
<Container>
|
||||
<Artwork src={getImage(track)} />
|
||||
<Header>{track?.Name}</Header>
|
||||
|
||||
@@ -17,16 +17,17 @@ export const failDownload = createAction<{ id: string }>('download/fail');
|
||||
|
||||
export const downloadTrack = createAsyncThunk(
|
||||
'/downloads/track',
|
||||
async (id: string, { dispatch }) => {
|
||||
async (id: string, { dispatch, getState }) => {
|
||||
// Generate the URL we can use to download the file
|
||||
const entity = (getState() as AppState).music.tracks.entities[id];
|
||||
const audioUrl = generateTrackUrl(id);
|
||||
const imageUrl = getImage(id);
|
||||
const imageUrl = getImage(entity);
|
||||
|
||||
// Get the content-type from the URL by doing a HEAD-only request
|
||||
const [audioExt, imageExt] = await Promise.all([
|
||||
getExtensionForUrl(audioUrl),
|
||||
// Image files may be absent
|
||||
getExtensionForUrl(imageUrl).catch(() => null)
|
||||
imageUrl ? getExtensionForUrl(imageUrl).catch(() => null) : null
|
||||
]);
|
||||
|
||||
// Then generate the proper location
|
||||
@@ -51,7 +52,7 @@ export const downloadTrack = createAsyncThunk(
|
||||
|
||||
const { promise: imagePromise } = imageExt && imageLocation
|
||||
? downloadFile({
|
||||
fromUrl: imageUrl,
|
||||
fromUrl: imageUrl!,
|
||||
toFile: imageLocation,
|
||||
background: true,
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function fetchApi<T>(
|
||||
path: PathOrCredentialInserter,
|
||||
providedConfig?: RequestInit,
|
||||
parseResponse = true
|
||||
) {
|
||||
) {
|
||||
// Retrieve the latest credentials from the Redux store
|
||||
const credentials = asyncFetchStore().getState().settings.credentials;
|
||||
|
||||
@@ -67,11 +67,11 @@ export async function fetchApi<T>(
|
||||
...generateConfig(credentials).headers,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Actually perform the request
|
||||
const response = await fetch(url, config);
|
||||
|
||||
if (__DEV__) {
|
||||
if (__DEV__) {
|
||||
console.log(`%c[HTTP] → [${response.status}] ${url}`, 'font-weight:bold;');
|
||||
console.log('\t', config);
|
||||
}
|
||||
@@ -96,24 +96,55 @@ export async function fetchApi<T>(
|
||||
if (parseResponse) {
|
||||
// Parse body as JSON
|
||||
const data = await response.json() as Promise<T>;
|
||||
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatImageUri(ItemId: string | number, baseUri: string): string {
|
||||
return encodeURI(`${baseUri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an image URL for a given ItemId
|
||||
*/
|
||||
export function getImage(ItemId: string | number, credentials?: AppState['settings']['credentials']): string {
|
||||
export function getImage(item: string | number | Album | AlbumTrack | Playlist | ArtistItem | null, credentials?: AppState['settings']['credentials']): string | undefined {
|
||||
// Either accept provided credentials, or retrieve them directly from the store
|
||||
const { uri: serverUri } = credentials
|
||||
?? asyncFetchStore().getState().settings.credentials ?? {};
|
||||
|
||||
// Generate the uri and return
|
||||
const uri = encodeURI(`${serverUri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
||||
return uri;
|
||||
const state = asyncFetchStore().getState();
|
||||
const { uri: serverUri } = credentials ?? state.settings.credentials ?? {};
|
||||
|
||||
if (!item || !serverUri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get the item ID
|
||||
const itemId = typeof item === 'string' || typeof item === 'number'
|
||||
? item
|
||||
: 'PrimaryImageItemId' in item
|
||||
? item.PrimaryImageItemId || item.Id
|
||||
: item.Id;
|
||||
|
||||
// Check if we have a downloaded image for this item
|
||||
const downloadEntity = state.downloads.entities[itemId];
|
||||
if (downloadEntity?.image) {
|
||||
return downloadEntity.image;
|
||||
}
|
||||
|
||||
// If no downloaded image, fall back to server URL
|
||||
if (typeof item === 'string' || typeof item === 'number') {
|
||||
if (__DEV__) {
|
||||
console.warn('useGetImage: supplied item is string or number. Please submit an item object instead.', { item });
|
||||
}
|
||||
return formatImageUri(item, serverUri);
|
||||
} else if ('PrimaryImageItemId' in item) {
|
||||
return formatImageUri(item.PrimaryImageItemId || item.Id, serverUri);
|
||||
} else if ('ImageTags' in item && item.ImageTags.Primary) {
|
||||
return formatImageUri(item.Id, serverUri);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,18 +153,7 @@ export function getImage(ItemId: string | number, credentials?: AppState['settin
|
||||
export function useGetImage() {
|
||||
const credentials = useTypedSelector((state) => state.settings.credentials);
|
||||
|
||||
return (item: string | number | Album | AlbumTrack | Playlist | ArtistItem | null) => {
|
||||
if (!item) {
|
||||
return '';
|
||||
// GUARD: If the item's just the id, we'll pass it on directly.
|
||||
} else if (typeof item === 'string' || typeof item === 'number') {
|
||||
return getImage(item, credentials);
|
||||
// GUARD: If the item has an `PrimaryImageItemId` (for Emby servers),
|
||||
// we'll attemp to return that
|
||||
} else if ('PrimaryImageItemId' in item) {
|
||||
return getImage(item.PrimaryImageItemId || item.Id, credentials);
|
||||
} else {
|
||||
return getImage(item.Id);
|
||||
}
|
||||
return (item: Parameters<typeof getImage>[0]) => {
|
||||
return getImage(item, credentials);
|
||||
};
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export async function generateTrack(track: AlbumTrack): Promise<Track> {
|
||||
artist: track.Artists.join(', '),
|
||||
album: track.Album,
|
||||
duration: track.RunTimeTicks,
|
||||
artwork: getImage(track.Id),
|
||||
artwork: getImage(track),
|
||||
bitRate: baseTrackOptions.audioBitRate,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user