Compare commits

...

11 Commits

Author SHA1 Message Date
Lei Nelissen
3afe403f4c chore: release v2.4.6 2025-08-05 00:51:57 +02:00
Kris
c4838b3b9e Fixed Android Safe View Areas (#294)
* Fixed Android safe view areas

* fix: xmark positioning

* fix: redundant safeareaprovider

* fix: roll back redundant changes

* fix: linter

---------

Co-authored-by: Lei Nelissen <lei@codified.nl>
2025-08-05 00:03:40 +02:00
Lei Nelissen
63481d0240 fix: start app, even if setting up player fails 2025-06-07 18:19:22 +02:00
Lei Nelissen
eb45169060 chore: release ios beta 2025-05-24 15:28:20 +02:00
Lei Nelissen
2e3ced0680 fix: linter 2025-05-24 00:34:44 +02:00
Lei Nelissen
76f3ce3972 fix: improve alphabetscroller working 2025-05-24 00:32:50 +02:00
Lei Nelissen
cf8bfdf05a feat: attempt to retrieve images from downloaded items 2025-05-24 00:22:31 +02:00
Lei Nelissen
09a020afbb chore: remove react-native-fast-image patch 2025-05-24 00:15:08 +02:00
Lei Nelissen
90a5d94147 fix: missing images 2025-05-24 00:09:12 +02:00
Lei Nelissen
58c7645170 fix: progress bar numbers cutoff 2025-05-24 00:08:56 +02:00
Lei Nelissen
d9326dfc7a feat: swap sectionlists for @shopify/flashlist 2025-05-22 23:36:53 +02:00
68 changed files with 754 additions and 404 deletions

View File

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

View File

@@ -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 {

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

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

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

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

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

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

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

View File

@@ -0,0 +1,7 @@
有用な改善をお届けします!
大規模なアルバムコレクションのパフォーマンスが大幅に向上し、音楽ライブラリの閲覧がよりスムーズになりました。
また、Android 15+でアプリの上部が切れる問題を修正しました。
Fintunesをご利用いただき、ありがとうございます

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

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

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

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

View File

@@ -0,0 +1,7 @@
Полезные улучшения в обновлении!
Производительность для больших коллекций альбомов значительно улучшена, что делает просмотр музыкальной библиотеки более плавным.
Исправили проблему обрезания верхней части приложения на Android 15+.
Спасибо за использование Fintunes!

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

View File

@@ -0,0 +1,7 @@
Корисні покращення в цьому оновленні!
Продуктивність для великих колекцій альбомів була значно покращена, роблячи перегляд музичної бібліотеки плавнішим.
Ми також виправили проблему обрізання верхньої частини додатку на Android 15+.
Дякуємо за використання Fintunes!

View File

@@ -0,0 +1,7 @@
我们很高兴为您带来此次更新中的一些实用改进!
大型专辑集合的性能得到了显著提升,使您浏览音乐库更加流畅和响应迅速。
我们还修复了一个在Android 15及更新版本上导致应用程序顶部被截断的问题。应用程序现在应该在最新的Android设备上正确显示。
感谢您使用Fintunes

View File

@@ -0,0 +1,7 @@
我們很高興為您帶來此次更新中的一些實用改進!
大型專輯集合的性能得到了顯著提升,使您瀏覽音樂庫更加流暢和響應迅速。
我們還修復了一個在Android 15及更新版本上導致應用程序頂部被截斷的問題。應用程序現在應該在最新的Android設備上正確顯示。
感謝您使用Fintunes

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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 !

View File

@@ -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!

View File

@@ -1 +1,7 @@
このバージョンでは、マルチディスクアルバムの最初のディスクしか再生できなかった問題を修正しました。また、素晴らしい言語コントリビューターのおかげで、Fintunesはタミル語でも利用可能になりました。さらに、12の言語が更新されました。Fintunesの新バージョンをお楽しみください。Discordサーバーへの参加もお待ちしています!
このアップデートで、いくつかの有用な改善をお届けできることを嬉しく思います!
大規模なアルバムコレクションのパフォーマンスが大幅に向上し、音楽ライブラリの閲覧がよりスムーズで反応性の良いものになりました。
また、Android 15以降のバージョンでアプリの上部が切れてしまう問題も修正しました。最新のAndroidデバイスでアプリが正しく表示されるようになりました。
Fintunesをご利用いただき、ありがとうございます

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -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!

View File

@@ -1 +1,7 @@
Эта версия исправляет проблему, при которой Fintunes мог воспроизводить только первый диск в альбоме с несколькими дисками. Кроме того, благодаря нашим замечательным языковым контрибьюторам, Fintunes теперь доступен на тамильском языке. Также обновлены 12 языков. Наслаждайтесь новой версией Fintunes и присоединяйтесь к нашему серверу Discord!
Мы рады предоставить вам несколько полезных улучшений в этом обновлении!
Производительность для больших коллекций альбомов была значительно улучшена, что делает просмотр вашей музыкальной библиотеки более плавным и отзывчивым.
Мы также исправили проблему, которая приводила к обрезанию верхней части приложения на Android 15 и более новых версиях. Приложение теперь должно корректно отображаться на новейших Android-устройствах.
Спасибо за использование Fintunes!

View File

@@ -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!

View File

@@ -1 +1,7 @@
Ця версія виправляє проблему, коли Fintunes міг відтворювати лише перший диск у багатодисковому альбомі. Крім того, завдяки нашим чудовим мовним учасникам, Fintunes тепер доступний тамільською мовою. Також оновлено 12 мов. Насолоджуйтесь цією новою версією Fintunes та приєднуйтесь до нашого сервера Discord!
Ми раді принести вам кілька корисних покращень у цьому оновленні!
Продуктивність для великих колекцій альбомів була значно покращена, роблячи перегляд вашої музичної бібліотеки плавнішим та більш відгукливим.
Ми також виправили проблему, яка спричиняла обрізання верхньої частини додатку на Android 15 і новіших версіях. Додаток тепер повинен правильно відображатися на найновіших Android-пристроях.
Дякуємо за використання Fintunes!

View File

@@ -1 +1,7 @@
此版本修复了Fintunes只能播放多碟专辑中第一张碟片的问题。此外感谢我们出色的语言贡献者Fintunes现在支援泰米尔语。另外还有12种语言已更新。享受这个新版本的Fintunes并考虑加入我们的Discord伺服器
我们很高兴为您带来此次更新中的一些实用改进!
大型专辑集合的性能得到了显著提升,使您浏览音乐库更加流畅和响应迅速。
我们还修复了一个在Android 15及更新版本上导致应用程序顶部被截断的问题。应用程序现在应该在最新的Android设备上正确显示。
感谢您使用Fintunes

View File

@@ -1 +1,7 @@
此版本修復了Fintunes只能播放多碟專輯中第一張碟片的問題。此外感謝我們出色的語言貢獻者Fintunes現在支援泰米爾語。另外還有12種語言已更新。享受這個新版本的Fintunes並考慮加入我們的Discord伺服器
我們很高興為您帶來此次更新中的一些實用改進!
大型專輯集合的性能得到了顯著提升,使您瀏覽音樂庫更加流暢和響應迅速。
我們還修復了一個在Android 15及更新版本上導致應用程序頂部被截斷的問題。應用程序現在應該在最新的Android設備上正確顯示。
感謝您使用Fintunes

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

@@ -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: {}

View File

@@ -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#';

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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?",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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} />

View File

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

View File

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

View File

@@ -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 />

View File

@@ -75,7 +75,7 @@ function TrackPopupMenu() {
}, [trackId, dispatch, closeModal]);
return (
<ColoredBlurView>
<ColoredBlurView style={{flex: 1}}>
<Container>
<Artwork src={getImage(track)} />
<Header>{track?.Name}</Header>

View File

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

View File

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

View File

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