mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
Localization support (#99)
* basic i18n poc * translate home, filters, tabs support dot notation in backend for namespaces * i18n context menu, artist filters, list controls also nothings here fix backend not caching fallback * i18n queue, artist view, search/results * i18n settings and server view * Added translation using Weblate (Norwegian Bokmål) * Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (6 of 6 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/nb_NO/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ * Update translation files Updated by "Cleanup translation files" hook in Weblate. Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ * fix url escaping * added some mostly naive text overflow fixes rewrote filter context menu as a slide in because the old one apparently can't handle dynamic width * Added translation using Weblate (French) * Translated using Weblate (French) Currently translated at 17.4% (11 of 63 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (French) Currently translated at 19.0% (12 of 63 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (French) Currently translated at 40.0% (26 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * add weblate and some pretty badges to readme * fix link * Translated using Weblate (French) Currently translated at 50.7% (33 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (English) Currently translated at 100.0% (65 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/en/ * Translated using Weblate (French) Currently translated at 90.7% (59 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * i18n now playing context type fix overscroll on new filter menu fix getting default namespace from the i18n backend * Translated using Weblate (French) Currently translated at 96.9% (63 of 65 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (French) Currently translated at 100.0% (66 of 66 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/fr/ * Translated using Weblate (Japanese) (#98) Currently translated at 7.5% (5 of 66 strings) Translation: Subtracks/subtracks Translate-URL: https://hosted.weblate.org/projects/subtracks/subtracks/ja/ Co-authored-by: Austin Riedhammer <austinried@functionkey.xyz> * little note to remind me why that's there * update licenses Co-authored-by: Allan Nordhøy <epost@anotheragency.no> Co-authored-by: Hosted Weblate <hosted@weblate.org> Co-authored-by: Clyhtsuriva <aimeric@adjutor.xyz>
This commit is contained in:
parent
4905f75564
commit
860a4cec16
@ -3,6 +3,8 @@
|
|||||||
#
|
#
|
||||||
Subtracks is an Android open source music streaming app for [Subsonic-API-compatible](http://www.subsonic.org/pages/api.jsp) servers ([Subsonic](http://www.subsonic.org/pages/index.jsp), [Navidrome](https://www.navidrome.org/), [Airsonic](https://airsonic.github.io/), and more). It's designed to give you clean and convenient access to your music in the style of modern media players.
|
Subtracks is an Android open source music streaming app for [Subsonic-API-compatible](http://www.subsonic.org/pages/api.jsp) servers ([Subsonic](http://www.subsonic.org/pages/index.jsp), [Navidrome](https://www.navidrome.org/), [Airsonic](https://airsonic.github.io/), and more). It's designed to give you clean and convenient access to your music in the style of modern media players.
|
||||||
|
|
||||||
|
[](https://hosted.weblate.org/engage/subtracks/)   
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
<p float="left">
|
<p float="left">
|
||||||
<img src="metadata/en-US/images/phoneScreenshots/01_home.png" alt="home" width="200"/>
|
<img src="metadata/en-US/images/phoneScreenshots/01_home.png" alt="home" width="200"/>
|
||||||
@ -45,3 +47,10 @@ Subtracks is an Android open source music streaming app for [Subsonic-API-compat
|
|||||||
|
|
||||||
# Building
|
# Building
|
||||||
See [Building from source](BUILDING.md).
|
See [Building from source](BUILDING.md).
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
Want to see Subtracks in your language? Visit the project on [Weblate](https://hosted.weblate.org/engage/subtracks/) to help!
|
||||||
|
|
||||||
|
<a href="https://hosted.weblate.org/engage/subtracks/">
|
||||||
|
<img src="https://hosted.weblate.org/widgets/subtracks/-/subtracks/multi-auto.svg" alt="Translation status" />
|
||||||
|
</a>
|
||||||
|
|||||||
151
android/app/src/main/assets/custom/i18n/en.json
vendored
Normal file
151
android/app/src/main/assets/custom/i18n/en.json
vendored
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"song": {
|
||||||
|
"name": "Song",
|
||||||
|
"name_plural": "Songs",
|
||||||
|
"lists": {
|
||||||
|
"artistTopSongs": "Top Songs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"album": {
|
||||||
|
"name": "Album",
|
||||||
|
"name_plural": "Albums",
|
||||||
|
"lists": {
|
||||||
|
"sort": "Sort Albums",
|
||||||
|
"random": "Random",
|
||||||
|
"newest": "Recently Added",
|
||||||
|
"frequent": "Frequently Played",
|
||||||
|
"recent": "Recently Played",
|
||||||
|
"starred": "Starred",
|
||||||
|
"alphabeticalByName": "By Name",
|
||||||
|
"alphabeticalByArtist": "By Artist",
|
||||||
|
"byYear": "By Year",
|
||||||
|
"byGenre": "By Genre"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"play": "Play Album",
|
||||||
|
"view": "View Album"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"artist": {
|
||||||
|
"name": "Artist",
|
||||||
|
"name_plural": "Artists",
|
||||||
|
"lists": {
|
||||||
|
"sort": "Sort Artists",
|
||||||
|
"random": "Random",
|
||||||
|
"starred": "Starred",
|
||||||
|
"alphabeticalByName": "By Name"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"view": "View Artist"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playlist": {
|
||||||
|
"name": "Playlist",
|
||||||
|
"name_plural": "Playlists",
|
||||||
|
"actions": {
|
||||||
|
"play": "Play Playlist"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"queue": {
|
||||||
|
"name": "Queue",
|
||||||
|
"name_plural": "Queues"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"actions": {
|
||||||
|
"star": "Star",
|
||||||
|
"unstar": "Unstar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"tabs": {
|
||||||
|
"home": "Home",
|
||||||
|
"library": "Library",
|
||||||
|
"search": "Search",
|
||||||
|
"settings": "Settings"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"nothingHere": "Nothing here…"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"inputPlaceholder": "Search",
|
||||||
|
"headerTitle": "Search: {{query}}",
|
||||||
|
"moreResults": "More…",
|
||||||
|
"nowPlayingContext": "Search Results"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"servers": {
|
||||||
|
"name": "Servers",
|
||||||
|
"fields": {
|
||||||
|
"address": "Address",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"forcePlaintextPassword": {
|
||||||
|
"title": "Force plaintext password",
|
||||||
|
"descriptionOn": "Send password in plaintext (legacy, make sure your connection is secure!)",
|
||||||
|
"descriptionOff": "Send password as token + salt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"add": "Add Server",
|
||||||
|
"testConnection": "Test Connection",
|
||||||
|
"delete": "Delete",
|
||||||
|
"save": "Save"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"connectionOk": "Connection to {{address}} OK!",
|
||||||
|
"connectionFailed": "Connection to {{address}} failed, check settings or server"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network": {
|
||||||
|
"name": "Network",
|
||||||
|
"values": {
|
||||||
|
"kbps": "{{value}}kbps",
|
||||||
|
"unlimitedKbps": "Unlimited",
|
||||||
|
"seconds": "{{value}} seconds"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"maxBitrateWifi": {
|
||||||
|
"title": "Maximum bitrate (Wi-Fi)"
|
||||||
|
},
|
||||||
|
"maxBitrateMobile": {
|
||||||
|
"title": "Maximum bitrate (mobile)"
|
||||||
|
},
|
||||||
|
"minBuffer": {
|
||||||
|
"title": "Minimum buffer time"
|
||||||
|
},
|
||||||
|
"maxBuffer": {
|
||||||
|
"title": "Maximum buffer time"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"music": {
|
||||||
|
"name": "Music",
|
||||||
|
"options": {
|
||||||
|
"scrobble": {
|
||||||
|
"title": "Scrobble plays",
|
||||||
|
"descriptionOn": "Scrobble play history",
|
||||||
|
"descriptionOff": "Don't scrobble play history"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"name": "Reset",
|
||||||
|
"actions": {
|
||||||
|
"clearImageCache": "Clear Image Cache"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"name": "About",
|
||||||
|
"version": "version {{version}}",
|
||||||
|
"actions": {
|
||||||
|
"projectHomepage": "Project Homepage",
|
||||||
|
"licenses": "Licenses"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
android/app/src/main/assets/custom/i18n/fr.json
vendored
Normal file
151
android/app/src/main/assets/custom/i18n/fr.json
vendored
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"album": {
|
||||||
|
"name": "Album",
|
||||||
|
"name_plural": "Albums",
|
||||||
|
"lists": {
|
||||||
|
"random": "Aléatoire",
|
||||||
|
"newest": "Récemment Ajouté",
|
||||||
|
"frequent": "Fréquemment Joué",
|
||||||
|
"recent": "Récemment Joué",
|
||||||
|
"alphabeticalByName": "Par Nom",
|
||||||
|
"byYear": "Par Année",
|
||||||
|
"alphabeticalByArtist": "Par Artiste",
|
||||||
|
"byGenre": "Par Genre",
|
||||||
|
"starred": "Favoris",
|
||||||
|
"sort": "Trier les albums"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"play": "Jouer l'album",
|
||||||
|
"view": "Voir l'album"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"song": {
|
||||||
|
"name": "Chanson",
|
||||||
|
"name_plural": "Chansons",
|
||||||
|
"lists": {
|
||||||
|
"artistTopSongs": "Meilleures Chansons"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"artist": {
|
||||||
|
"name": "Artiste",
|
||||||
|
"name_plural": "Artistes",
|
||||||
|
"lists": {
|
||||||
|
"random": "Aléatoire",
|
||||||
|
"starred": "Favoris",
|
||||||
|
"alphabeticalByName": "Par Nom",
|
||||||
|
"sort": "Trier les artistes"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"view": "Voir l'artiste"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"playlist": {
|
||||||
|
"actions": {
|
||||||
|
"play": "Lire la playlist"
|
||||||
|
},
|
||||||
|
"name": "Playlist",
|
||||||
|
"name_plural": "Playlists"
|
||||||
|
},
|
||||||
|
"queue": {
|
||||||
|
"name": "File d'attente",
|
||||||
|
"name_plural": "Files d'attente"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"network": {
|
||||||
|
"values": {
|
||||||
|
"seconds": "{{value}} secondes",
|
||||||
|
"unlimitedKbps": "Illimité",
|
||||||
|
"kbps": "{{value}}kbit/s"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"maxBitrateWifi": {
|
||||||
|
"title": "Débit binaire maximum (Wi-Fi)"
|
||||||
|
},
|
||||||
|
"maxBitrateMobile": {
|
||||||
|
"title": "Débit binaire maximum (mobile)"
|
||||||
|
},
|
||||||
|
"maxBuffer": {
|
||||||
|
"title": "Temps maximum en mémoire tampon"
|
||||||
|
},
|
||||||
|
"minBuffer": {
|
||||||
|
"title": "Temps minimum en mémoire tampon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"name": "Réseau"
|
||||||
|
},
|
||||||
|
"servers": {
|
||||||
|
"fields": {
|
||||||
|
"username": "Nom d'utilisateur",
|
||||||
|
"address": "Adresse",
|
||||||
|
"password": "Mot de passe"
|
||||||
|
},
|
||||||
|
"actions": {
|
||||||
|
"testConnection": "Tester la connexion",
|
||||||
|
"add": "Ajouter un serveur",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"save": "Sauvegarder"
|
||||||
|
},
|
||||||
|
"name": "Serveurs",
|
||||||
|
"options": {
|
||||||
|
"forcePlaintextPassword": {
|
||||||
|
"title": "Forcer le mot de passe en texte clair",
|
||||||
|
"descriptionOn": "Envoyer le mot de passe en test clair (héritage, assurez-vous que la connexion est sécurisée !)",
|
||||||
|
"descriptionOff": "Envoyer le mot de passe sous forme de jeton + salage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"connectionOk": "Connexion à {{address}} OK !",
|
||||||
|
"connectionFailed": "Échec de la connexion à {{address}}, vérifiez les paramètres ou le serveur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"music": {
|
||||||
|
"name": "Musique",
|
||||||
|
"options": {
|
||||||
|
"scrobble": {
|
||||||
|
"descriptionOff": "Ne pas scrobbler l'historique de lecture",
|
||||||
|
"descriptionOn": "Scrobbler l'historique de lecture",
|
||||||
|
"title": "Scrobbler la lecture"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"about": {
|
||||||
|
"version": "version {{version}}",
|
||||||
|
"name": "À propos",
|
||||||
|
"actions": {
|
||||||
|
"licenses": "Licenses",
|
||||||
|
"projectHomepage": "Page d'accueil du projet"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset": {
|
||||||
|
"actions": {
|
||||||
|
"clearImageCache": "Vider le cache d'images"
|
||||||
|
},
|
||||||
|
"name": "Réinitialiser"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"tabs": {
|
||||||
|
"library": "Bibliothèque",
|
||||||
|
"home": "Accueil",
|
||||||
|
"search": "Recherche",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"headerTitle": "Recherche : {{query}}",
|
||||||
|
"inputPlaceholder": "Recherche",
|
||||||
|
"moreResults": "Plus…",
|
||||||
|
"nowPlayingContext": "Résultats de recherche"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"actions": {
|
||||||
|
"star": "Mettre en favoris",
|
||||||
|
"unstar": "Enlever des favoris"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"nothingHere": "Rien ici…"
|
||||||
|
}
|
||||||
|
}
|
||||||
15
android/app/src/main/assets/custom/i18n/ja.json
vendored
Normal file
15
android/app/src/main/assets/custom/i18n/ja.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"resources": {
|
||||||
|
"album": {
|
||||||
|
"lists": {
|
||||||
|
"random": "ランダムアルバム",
|
||||||
|
"frequent": "よく聴くアルバム",
|
||||||
|
"recent": "最近再生した",
|
||||||
|
"starred": "星付きアルバム"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"song": {
|
||||||
|
"name": "歌"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
android/app/src/main/assets/custom/i18n/nb_NO.json
vendored
Normal file
1
android/app/src/main/assets/custom/i18n/nb_NO.json
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
129
android/app/src/main/assets/licenses.html
vendored
129
android/app/src/main/assets/licenses.html
vendored
@ -27165,6 +27165,30 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: html-escaper. A copy of the source code may be downloaded from https://github.com/WebReflection/html-escaper.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: http-errors. A copy of the source code may be downloaded from https://github.com/jshttp/http-errors.git. This software contains the following license and notice below:
|
The following software may be included in this product: http-errors. A copy of the source code may be downloaded from https://github.com/jshttp/http-errors.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
@ -27192,6 +27216,32 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: i18next. A copy of the source code may be downloaded from https://github.com/i18next/i18next.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 i18next
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: ieee754. A copy of the source code may be downloaded from git://github.com/feross/ieee754.git. This software contains the following license and notice below:
|
The following software may be included in this product: ieee754. A copy of the source code may be downloaded from git://github.com/feross/ieee754.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
Copyright 2008 Fair Oaks Labs, Inc.
|
Copyright 2008 Fair Oaks Labs, Inc.
|
||||||
@ -29386,6 +29436,32 @@ SOFTWARE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: react-i18next. A copy of the source code may be downloaded from https://github.com/i18next/react-i18next.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 i18next
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: react-native-blob-util. A copy of the source code may be downloaded from https://github.com/RonRadtke/react-native-blob-util. This software contains the following license and notice below:
|
The following software may be included in this product: react-native-blob-util. A copy of the source code may be downloaded from https://github.com/RonRadtke/react-native-blob-util. This software contains the following license and notice below:
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
@ -29542,6 +29618,32 @@ SOFTWARE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: react-native-localize. A copy of the source code may be downloaded from https://github.com/zoontek/react-native-localize.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017-present, Mathieu Acthernoene
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: react-native-popup-menu. A copy of the source code may be downloaded from git+ssh://git@github.com:instea/react-native-popup-menu.git. This software contains the following license and notice below:
|
The following software may be included in this product: react-native-popup-menu. A copy of the source code may be downloaded from git+ssh://git@github.com:instea/react-native-popup-menu.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
ISC License
|
ISC License
|
||||||
@ -31559,6 +31661,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: void-elements. A copy of the source code may be downloaded from https://github.com/pugjs/void-elements.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014 hemanth
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: walker. A copy of the source code may be downloaded from https://github.com/daaku/nodejs-walker. This software contains the following license and notice below:
|
The following software may be included in this product: walker. A copy of the source code may be downloaded from https://github.com/daaku/nodejs-walker. This software contains the following license and notice below:
|
||||||
|
|
||||||
Copyright 2013 Naitik Shah
|
Copyright 2013 Naitik Shah
|
||||||
|
|||||||
129
android/app/src/main/assets/licenses/npm.txt
vendored
129
android/app/src/main/assets/licenses/npm.txt
vendored
@ -3863,6 +3863,30 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: html-escaper. A copy of the source code may be downloaded from https://github.com/WebReflection/html-escaper.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: http-errors. A copy of the source code may be downloaded from https://github.com/jshttp/http-errors.git. This software contains the following license and notice below:
|
The following software may be included in this product: http-errors. A copy of the source code may be downloaded from https://github.com/jshttp/http-errors.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
@ -3890,6 +3914,32 @@ THE SOFTWARE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: i18next. A copy of the source code may be downloaded from https://github.com/i18next/i18next.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 i18next
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: ieee754. A copy of the source code may be downloaded from git://github.com/feross/ieee754.git. This software contains the following license and notice below:
|
The following software may be included in this product: ieee754. A copy of the source code may be downloaded from git://github.com/feross/ieee754.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
Copyright 2008 Fair Oaks Labs, Inc.
|
Copyright 2008 Fair Oaks Labs, Inc.
|
||||||
@ -6084,6 +6134,32 @@ SOFTWARE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: react-i18next. A copy of the source code may be downloaded from https://github.com/i18next/react-i18next.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2022 i18next
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: react-native-blob-util. A copy of the source code may be downloaded from https://github.com/RonRadtke/react-native-blob-util. This software contains the following license and notice below:
|
The following software may be included in this product: react-native-blob-util. A copy of the source code may be downloaded from https://github.com/RonRadtke/react-native-blob-util. This software contains the following license and notice below:
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
@ -6240,6 +6316,32 @@ SOFTWARE.
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: react-native-localize. A copy of the source code may be downloaded from https://github.com/zoontek/react-native-localize.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017-present, Mathieu Acthernoene
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: react-native-popup-menu. A copy of the source code may be downloaded from git+ssh://git@github.com:instea/react-native-popup-menu.git. This software contains the following license and notice below:
|
The following software may be included in this product: react-native-popup-menu. A copy of the source code may be downloaded from git+ssh://git@github.com:instea/react-native-popup-menu.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
ISC License
|
ISC License
|
||||||
@ -8257,6 +8359,33 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
The following software may be included in this product: void-elements. A copy of the source code may be downloaded from https://github.com/pugjs/void-elements.git. This software contains the following license and notice below:
|
||||||
|
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2014 hemanth
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
'Software'), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
The following software may be included in this product: walker. A copy of the source code may be downloaded from https://github.com/daaku/nodejs-walker. This software contains the following license and notice below:
|
The following software may be included in this product: walker. A copy of the source code may be downloaded from https://github.com/daaku/nodejs-walker. This software contains the following license and notice below:
|
||||||
|
|
||||||
Copyright 2013 Naitik Shah
|
Copyright 2013 Naitik Shah
|
||||||
|
|||||||
@ -14,6 +14,9 @@ public class MainActivity extends ReactActivity {
|
|||||||
return "subtracks";
|
return "subtracks";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// required by react-native-screens
|
||||||
|
// "This change is required to avoid crashes related to View state being not persisted consistently across Activity restarts."
|
||||||
|
// https://reactnavigation.org/docs/getting-started
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(null);
|
super.onCreate(null);
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import RootNavigator from '@app/navigation/RootNavigator'
|
|||||||
import SplashPage from '@app/screens/SplashPage'
|
import SplashPage from '@app/screens/SplashPage'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StatusBar, View, StyleSheet } from 'react-native'
|
import { StatusBar, StyleSheet, View } from 'react-native'
|
||||||
import ProgressHook from './components/ProgressHook'
|
|
||||||
import { useStore } from './state/store'
|
|
||||||
import { MenuProvider } from 'react-native-popup-menu'
|
import { MenuProvider } from 'react-native-popup-menu'
|
||||||
import { QueryClientProvider } from 'react-query'
|
import { QueryClientProvider } from 'react-query'
|
||||||
|
import ProgressHook from './components/ProgressHook'
|
||||||
import queryClient from './queryClient'
|
import queryClient from './queryClient'
|
||||||
|
import { useStore } from './state/store'
|
||||||
|
|
||||||
const Debug = () => {
|
const Debug = () => {
|
||||||
const currentTrackTitle = useStore(store => store.currentTrack?.title)
|
const currentTrackTitle = useStore(store => store.currentTrack?.title)
|
||||||
|
|||||||
@ -16,7 +16,13 @@ const Button: React.FC<{
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={[styles.container, buttonStyle !== undefined ? styles[buttonStyle] : {}, style]}>
|
style={[styles.container, buttonStyle !== undefined ? styles[buttonStyle] : {}, style]}>
|
||||||
{title ? <Text style={styles.text}>{title}</Text> : children}
|
{title ? (
|
||||||
|
<Text style={styles.text} numberOfLines={2} adjustsFontSizeToFit={true}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -26,6 +32,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: colors.accent,
|
backgroundColor: colors.accent,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
minHeight: 42,
|
minHeight: 42,
|
||||||
|
maxHeight: 42,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: 1000,
|
borderRadius: 1000,
|
||||||
},
|
},
|
||||||
@ -43,6 +50,7 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: font.bold,
|
fontFamily: font.bold,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,14 @@ import font from '@app/styles/font'
|
|||||||
import { NavigationProp, useNavigation } from '@react-navigation/native'
|
import { NavigationProp, useNavigation } from '@react-navigation/native'
|
||||||
import { ReactComponentLike } from 'prop-types'
|
import { ReactComponentLike } from 'prop-types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ScrollView, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
|
import { ScrollView, StyleProp, StyleSheet, Text, View, ViewStyle } from 'react-native'
|
||||||
import { Menu, MenuOption, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu'
|
import { Menu, MenuOption, MenuOptions, MenuTrigger, renderers } from 'react-native-popup-menu'
|
||||||
import IconFA from 'react-native-vector-icons/FontAwesome'
|
import IconFA from 'react-native-vector-icons/FontAwesome'
|
||||||
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
||||||
import CoverArt from './CoverArt'
|
import CoverArt from './CoverArt'
|
||||||
import { Star } from './Star'
|
import { Star } from './Star'
|
||||||
|
import { withSuspenseMemo } from './withSuspense'
|
||||||
|
|
||||||
const { SlideInMenu } = renderers
|
const { SlideInMenu } = renderers
|
||||||
|
|
||||||
@ -106,7 +108,9 @@ const ContextMenuIconTextOption = React.memo<ContextMenuIconTextOptionProps>(
|
|||||||
return (
|
return (
|
||||||
<ContextMenuOption onSelect={onSelect}>
|
<ContextMenuOption onSelect={onSelect}>
|
||||||
<View style={styles.icon}>{Icon}</View>
|
<View style={styles.icon}>{Icon}</View>
|
||||||
<Text style={styles.optionText}>{text}</Text>
|
<Text style={styles.optionText} numberOfLines={1} adjustsFontSizeToFit={true} minimumFontScale={0.6}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
</ContextMenuOption>
|
</ContextMenuOption>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -154,27 +158,30 @@ const MenuHeader = React.memo<{
|
|||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
|
|
||||||
const OptionStar = React.memo<{
|
const OptionStar = withSuspenseMemo<{
|
||||||
id: string
|
id: string
|
||||||
type: StarrableItemType
|
type: StarrableItemType
|
||||||
additionalText?: string
|
additionalText?: string
|
||||||
}>(({ id, type, additionalText: text }) => {
|
}>(({ id, type, additionalText: text }) => {
|
||||||
const { query, toggle } = useStar(id, type)
|
const { query, toggle } = useStar(id, type)
|
||||||
|
const { t } = useTranslation('context.actions')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenuIconTextOption
|
<ContextMenuIconTextOption
|
||||||
IconComponentRaw={<Star starred={!!query.data} size={26} />}
|
IconComponentRaw={<Star starred={!!query.data} size={26} />}
|
||||||
text={(query.data ? 'Unstar' : 'Star') + (text ? ` ${text}` : '')}
|
text={(query.data ? t('unstar') : t('star')) + (text ? ` ${text}` : '')}
|
||||||
onSelect={() => toggle.mutate()}
|
onSelect={() => toggle.mutate()}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const OptionViewArtist = React.memo<{
|
const OptionViewArtist = withSuspenseMemo<{
|
||||||
navigation: NavigationProp<any>
|
navigation: NavigationProp<any>
|
||||||
artist?: string
|
artist?: string
|
||||||
artistId?: string
|
artistId?: string
|
||||||
}>(({ navigation, artist, artistId }) => {
|
}>(({ navigation, artist, artistId }) => {
|
||||||
|
const { t } = useTranslation('resources.artist.actions')
|
||||||
|
|
||||||
if (!artist || !artistId) {
|
if (!artist || !artistId) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
@ -184,17 +191,19 @@ const OptionViewArtist = React.memo<{
|
|||||||
IconComponent={IconFA}
|
IconComponent={IconFA}
|
||||||
name="microphone"
|
name="microphone"
|
||||||
size={26}
|
size={26}
|
||||||
text="View Artist"
|
text={t('view')}
|
||||||
onSelect={() => navigation.navigate('artist', { id: artistId, title: artist })}
|
onSelect={() => navigation.navigate('artist', { id: artistId, title: artist })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const OptionViewAlbum = React.memo<{
|
const OptionViewAlbum = withSuspenseMemo<{
|
||||||
navigation: NavigationProp<any>
|
navigation: NavigationProp<any>
|
||||||
album?: string
|
album?: string
|
||||||
albumId?: string
|
albumId?: string
|
||||||
}>(({ navigation, album, albumId }) => {
|
}>(({ navigation, album, albumId }) => {
|
||||||
|
const { t } = useTranslation('resources.album.actions')
|
||||||
|
|
||||||
if (!album || !albumId) {
|
if (!album || !albumId) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
@ -204,7 +213,7 @@ const OptionViewAlbum = React.memo<{
|
|||||||
IconComponent={IconFA5}
|
IconComponent={IconFA5}
|
||||||
name="compact-disc"
|
name="compact-disc"
|
||||||
size={26}
|
size={26}
|
||||||
text="View Album"
|
text={t('view')}
|
||||||
onSelect={() => navigation.navigate('album', { id: albumId, title: album })}
|
onSelect={() => navigation.navigate('album', { id: albumId, title: album })}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -318,6 +327,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
optionsWrapper: {
|
optionsWrapper: {
|
||||||
// marginBottom: 10,
|
// marginBottom: 10,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
// backgroundColor: 'purple',
|
||||||
},
|
},
|
||||||
menuHeader: {
|
menuHeader: {
|
||||||
paddingTop: 14,
|
paddingTop: 14,
|
||||||
@ -348,9 +359,11 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
option: {
|
option: {
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 20,
|
// paddingHorizontal: 100,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
// backgroundColor: 'blue',
|
||||||
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Text, StyleSheet } from 'react-native'
|
import { Text, StyleSheet, View } from 'react-native'
|
||||||
import { MenuOption, Menu, MenuTrigger, MenuOptions } from 'react-native-popup-menu'
|
import { MenuOption, Menu, MenuTrigger, MenuOptions, renderers } from 'react-native-popup-menu'
|
||||||
import PressableOpacity from './PressableOpacity'
|
import PressableOpacity from './PressableOpacity'
|
||||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
|
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||||
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
|
|
||||||
|
const { SlideInMenu } = renderers
|
||||||
|
|
||||||
export type OptionData = {
|
export type OptionData = {
|
||||||
value: string
|
value: string
|
||||||
@ -17,12 +20,14 @@ const Option = React.memo<{
|
|||||||
selected?: boolean
|
selected?: boolean
|
||||||
}>(({ text, value, selected }) => (
|
}>(({ text, value, selected }) => (
|
||||||
<MenuOption style={styles.option} value={value}>
|
<MenuOption style={styles.option} value={value}>
|
||||||
<Text style={styles.optionText}>{text}</Text>
|
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<Icon name="checkbox-marked-circle" size={26} color={colors.accent} />
|
<Icon name="checkbox-marked-circle" size={32} color={colors.accent} style={styles.icon} />
|
||||||
) : (
|
) : (
|
||||||
<Icon name="checkbox-blank-circle-outline" size={26} color={colors.text.secondary} />
|
<Icon name="checkbox-blank-circle-outline" size={32} color={colors.text.secondary} style={styles.icon} />
|
||||||
)}
|
)}
|
||||||
|
<Text style={styles.optionText} numberOfLines={1} adjustsFontSizeToFit={true} minimumFontScale={0.6}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
</MenuOption>
|
</MenuOption>
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -30,9 +35,10 @@ const FilterButton = React.memo<{
|
|||||||
value?: string
|
value?: string
|
||||||
data: OptionData[]
|
data: OptionData[]
|
||||||
onSelect?: (selection: string) => void
|
onSelect?: (selection: string) => void
|
||||||
}>(({ value, data, onSelect }) => {
|
title: string
|
||||||
|
}>(({ value, data, onSelect, title }) => {
|
||||||
return (
|
return (
|
||||||
<Menu onSelect={onSelect}>
|
<Menu onSelect={onSelect} renderer={SlideInMenu}>
|
||||||
<MenuTrigger
|
<MenuTrigger
|
||||||
customStyles={{
|
customStyles={{
|
||||||
triggerOuterWrapper: styles.filterOuterWrapper,
|
triggerOuterWrapper: styles.filterOuterWrapper,
|
||||||
@ -40,16 +46,23 @@ const FilterButton = React.memo<{
|
|||||||
triggerTouchable: { style: styles.filter },
|
triggerTouchable: { style: styles.filter },
|
||||||
TriggerTouchableComponent: PressableOpacity,
|
TriggerTouchableComponent: PressableOpacity,
|
||||||
}}>
|
}}>
|
||||||
<Icon name="filter-variant" color="white" size={30} style={styles.filterIcon} />
|
<Icon name="filter-variant" color="white" size={30} />
|
||||||
</MenuTrigger>
|
</MenuTrigger>
|
||||||
<MenuOptions
|
<MenuOptions
|
||||||
customStyles={{
|
customStyles={{
|
||||||
optionsWrapper: styles.optionsWrapper,
|
optionsWrapper: styles.optionsWrapper,
|
||||||
optionsContainer: styles.optionsContainer,
|
optionsContainer: styles.optionsContainer,
|
||||||
}}>
|
}}>
|
||||||
{data.map(o => (
|
<ScrollView style={styles.optionsScroll} overScrollMode="never">
|
||||||
<Option key={o.value} text={o.text} value={o.value} selected={o.value === value} />
|
<View style={styles.header}>
|
||||||
))}
|
<Text style={styles.headerText} numberOfLines={2} ellipsizeMode="clip">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{data.map(o => (
|
||||||
|
<Option key={o.value} text={o.text} value={o.value} selected={o.value === value} />
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
</MenuOptions>
|
</MenuOptions>
|
||||||
</Menu>
|
</Menu>
|
||||||
)
|
)
|
||||||
@ -71,28 +84,45 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: colors.accent,
|
backgroundColor: colors.accent,
|
||||||
},
|
},
|
||||||
filterIcon: {
|
optionsScroll: {
|
||||||
// top: 4,
|
maxHeight: 260,
|
||||||
},
|
},
|
||||||
optionsWrapper: {
|
optionsWrapper: {
|
||||||
maxWidth: 145,
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
optionsContainer: {
|
optionsContainer: {
|
||||||
backgroundColor: colors.gradient.high,
|
backgroundColor: 'rgba(45, 45, 45, 0.95)',
|
||||||
maxWidth: 145,
|
},
|
||||||
|
header: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
// paddingVertical: 10,
|
||||||
|
marginTop: 16,
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
headerText: {
|
||||||
|
fontFamily: font.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
color: colors.text.primary,
|
||||||
},
|
},
|
||||||
option: {
|
option: {
|
||||||
flexDirection: 'row',
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
justifyContent: 'center',
|
paddingHorizontal: 20,
|
||||||
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
},
|
},
|
||||||
optionText: {
|
optionText: {
|
||||||
color: colors.text.primary,
|
|
||||||
fontFamily: font.semiBold,
|
fontFamily: font.semiBold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
flex: 1,
|
color: colors.text.primary,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
marginRight: 14,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
// backgroundColor: 'red',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -2,19 +2,22 @@ import Button from '@app/components/Button'
|
|||||||
import { Song } from '@app/models/library'
|
import { Song } from '@app/models/library'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'
|
||||||
import Icon from 'react-native-vector-icons/Ionicons'
|
import Icon from 'react-native-vector-icons/Ionicons'
|
||||||
import IconMat from 'react-native-vector-icons/MaterialIcons'
|
import IconMat from 'react-native-vector-icons/MaterialIcons'
|
||||||
|
import { withSuspenseMemo } from './withSuspense'
|
||||||
|
|
||||||
const ListPlayerControls = React.memo<{
|
const ListPlayerControls = withSuspenseMemo<{
|
||||||
songs: Song[]
|
songs: Song[]
|
||||||
typeName: string
|
listType: 'album' | 'playlist'
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
play: () => void
|
play: () => void
|
||||||
shuffle: () => void
|
shuffle: () => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}>(({ typeName, style, play, shuffle, disabled }) => {
|
}>(({ listType, style, play, shuffle, disabled }) => {
|
||||||
const [downloaded, setDownloaded] = useState(false)
|
const [downloaded, setDownloaded] = useState(false)
|
||||||
|
const { t } = useTranslation('resources')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.controls, style]}>
|
<View style={[styles.controls, style]}>
|
||||||
@ -31,7 +34,7 @@ const ListPlayerControls = React.memo<{
|
|||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.controlsCenter}>
|
<View style={styles.controlsCenter}>
|
||||||
<Button title={`Play ${typeName}`} disabled={disabled} onPress={play} />
|
<Button title={t(`${listType}.actions.play`)} disabled={disabled} onPress={play} />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.controlsSide}>
|
<View style={styles.controlsSide}>
|
||||||
<Button disabled={disabled} onPress={shuffle}>
|
<Button disabled={disabled} onPress={shuffle}>
|
||||||
@ -55,6 +58,7 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
maxWidth: '65%',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,25 @@
|
|||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Text, View, StyleSheet, ViewStyle } from 'react-native'
|
import { Text, View, StyleSheet, ViewStyle } from 'react-native'
|
||||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
|
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||||
|
import { withSuspenseMemo } from './withSuspense'
|
||||||
|
|
||||||
const NothingHere = React.memo<{
|
const NothingHere = withSuspenseMemo<{
|
||||||
height?: number
|
height?: number
|
||||||
width?: number
|
width?: number
|
||||||
style?: ViewStyle
|
style?: ViewStyle
|
||||||
}>(({ height, width, style }) => {
|
}>(({ height, width, style }) => {
|
||||||
|
const { t } = useTranslation('messages')
|
||||||
height = height || 200
|
height = height || 200
|
||||||
width = width || 200
|
width = width || 200
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, { height, width }, style]}>
|
<View style={[styles.container, { height, width }, style]}>
|
||||||
<Icon name="music-rest-quarter" color={styles.text.color} size={width / 2} />
|
<Icon name="music-rest-quarter" color={styles.text.color} size={width / 2} />
|
||||||
<Text style={[styles.text, { fontSize: width / 8 }]}>Nothing here...</Text>
|
<Text style={[styles.text, { fontSize: width / 8 }]} numberOfLines={3}>
|
||||||
|
{t('nothingHere')}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
32
app/components/withSuspense.tsx
Normal file
32
app/components/withSuspense.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { ComponentType, FunctionComponent, Suspense, SuspenseProps } from 'react'
|
||||||
|
|
||||||
|
export function withSuspense<P extends string | number | object>(
|
||||||
|
WrappedComponent: ComponentType<P>,
|
||||||
|
fallback: SuspenseProps['fallback'] = null,
|
||||||
|
): FunctionComponent<P> {
|
||||||
|
function ComponentWithSuspense(props: P) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={fallback}>
|
||||||
|
<WrappedComponent {...props} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComponentWithSuspense
|
||||||
|
}
|
||||||
|
|
||||||
|
export function withSuspenseMemo<P extends string | number | object>(
|
||||||
|
WrappedComponent: ComponentType<P>,
|
||||||
|
fallback: SuspenseProps['fallback'] = null,
|
||||||
|
propsAreEqual?: Parameters<typeof React.memo>['1'],
|
||||||
|
) {
|
||||||
|
function ComponentWithSuspense(props: P) {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={fallback}>
|
||||||
|
<WrappedComponent {...props} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return React.memo(ComponentWithSuspense, propsAreEqual)
|
||||||
|
}
|
||||||
64
app/i18n.ts
Normal file
64
app/i18n.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { BackendModule, LanguageDetectorAsyncModule } from 'i18next'
|
||||||
|
import path from 'path'
|
||||||
|
import RNFS from 'react-native-fs'
|
||||||
|
import * as RNLocalize from 'react-native-localize'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const I18N_ASSETS_DIR = path.join('custom', 'i18n')
|
||||||
|
|
||||||
|
const cache: {
|
||||||
|
[language: string]: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
async function loadTranslation(language: string) {
|
||||||
|
const text = await RNFS.readFileAssets(path.join(I18N_ASSETS_DIR, `${language}.json`), 'utf8')
|
||||||
|
return JSON.parse(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readTranslation(language: string, namespace: string) {
|
||||||
|
if (!cache[language]) {
|
||||||
|
cache[language] = await loadTranslation(language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return namespace === 'translation' ? cache[language] : _.get(cache[language], namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backend = {
|
||||||
|
type: 'backend',
|
||||||
|
init: () => {},
|
||||||
|
read: async (language, namespace, callback) => {
|
||||||
|
try {
|
||||||
|
callback(null, await readTranslation(language, namespace))
|
||||||
|
} catch (err) {
|
||||||
|
callback(err as any, null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} as BackendModule
|
||||||
|
|
||||||
|
export const languageDetector = {
|
||||||
|
type: 'languageDetector',
|
||||||
|
async: true,
|
||||||
|
detect: async callback => {
|
||||||
|
try {
|
||||||
|
const languageTags = (await RNFS.readDirAssets(I18N_ASSETS_DIR))
|
||||||
|
.map(f => f.name)
|
||||||
|
.filter(n => n.endsWith('.json'))
|
||||||
|
.map(n => n.slice(0, -5))
|
||||||
|
|
||||||
|
console.log('translations available:', languageTags)
|
||||||
|
console.log(
|
||||||
|
'locales list:',
|
||||||
|
RNLocalize.getLocales().map(l => l.languageTag),
|
||||||
|
)
|
||||||
|
console.log('best language:', RNLocalize.findBestAvailableLanguage(languageTags)?.languageTag)
|
||||||
|
|
||||||
|
callback(RNLocalize.findBestAvailableLanguage(languageTags)?.languageTag)
|
||||||
|
} catch {
|
||||||
|
callback(undefined)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init: () => {},
|
||||||
|
cacheUserLanguage: () => {},
|
||||||
|
} as LanguageDetectorAsyncModule
|
||||||
@ -47,7 +47,9 @@ const BottomTabButton = React.memo<{
|
|||||||
return (
|
return (
|
||||||
<PressableOpacity onPress={onPress} style={styles.button} disabled={disabled}>
|
<PressableOpacity onPress={onPress} style={styles.button} disabled={disabled}>
|
||||||
<Image source={imgSource} style={imgStyle} fadeDuration={0} />
|
<Image source={imgSource} style={imgStyle} fadeDuration={0} />
|
||||||
<Text style={textStyle}>{label}</Text>
|
<Text style={textStyle} numberOfLines={1} ellipsizeMode="clip">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -92,6 +94,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
flexGrow: 1,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { withSuspense } from '@app/components/withSuspense'
|
||||||
import { useFirstRun } from '@app/hooks/settings'
|
import { useFirstRun } from '@app/hooks/settings'
|
||||||
import { Album, Playlist } from '@app/models/library'
|
import { Album, Playlist } from '@app/models/library'
|
||||||
import BottomTabBar from '@app/navigation/BottomTabBar'
|
import BottomTabBar from '@app/navigation/BottomTabBar'
|
||||||
@ -16,6 +17,7 @@ import font from '@app/styles/font'
|
|||||||
import { BottomTabNavigationProp, createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
import { BottomTabNavigationProp, createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||||
import { RouteProp, StackActions } from '@react-navigation/native'
|
import { RouteProp, StackActions } from '@react-navigation/native'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator, NativeStackNavigationProp } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
@ -116,7 +118,7 @@ const SearchTab = createTabStackNavigator(Search)
|
|||||||
type SettingsStackParamList = {
|
type SettingsStackParamList = {
|
||||||
main: undefined
|
main: undefined
|
||||||
server?: { id?: string }
|
server?: { id?: string }
|
||||||
web: { uri: string }
|
web: { uri: string; title?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerScreenNavigationProp = NativeStackNavigationProp<SettingsStackParamList, 'server'>
|
type ServerScreenNavigationProp = NativeStackNavigationProp<SettingsStackParamList, 'server'>
|
||||||
@ -133,7 +135,9 @@ type WebScreenProps = {
|
|||||||
route: WebScreenRouteProp
|
route: WebScreenRouteProp
|
||||||
navigation: WebScreenNavigationProp
|
navigation: WebScreenNavigationProp
|
||||||
}
|
}
|
||||||
const WebScreen: React.FC<WebScreenProps> = ({ route }) => <WebViewScreen uri={route.params.uri} />
|
const WebScreen: React.FC<WebScreenProps> = ({ route }) => (
|
||||||
|
<WebViewScreen uri={route.params.uri} title={route.params.title} />
|
||||||
|
)
|
||||||
|
|
||||||
const SettingsStack = createNativeStackNavigator()
|
const SettingsStack = createNativeStackNavigator()
|
||||||
|
|
||||||
@ -156,7 +160,6 @@ const SettingsTab = () => {
|
|||||||
name="web"
|
name="web"
|
||||||
component={WebScreen}
|
component={WebScreen}
|
||||||
options={{
|
options={{
|
||||||
title: 'Web View',
|
|
||||||
headerStyle: styles.stackheaderStyle,
|
headerStyle: styles.stackheaderStyle,
|
||||||
headerHideShadow: true,
|
headerHideShadow: true,
|
||||||
headerTintColor: 'white',
|
headerTintColor: 'white',
|
||||||
@ -169,7 +172,8 @@ const SettingsTab = () => {
|
|||||||
|
|
||||||
const Tab = createBottomTabNavigator()
|
const Tab = createBottomTabNavigator()
|
||||||
|
|
||||||
const BottomTabNavigator = () => {
|
const BottomTabNavigator = withSuspense(() => {
|
||||||
|
const { t } = useTranslation('navigation.tabs')
|
||||||
const firstRun = useFirstRun()
|
const firstRun = useFirstRun()
|
||||||
const resetServer = useStore(store => store.resetServer)
|
const resetServer = useStore(store => store.resetServer)
|
||||||
|
|
||||||
@ -179,14 +183,14 @@ const BottomTabNavigator = () => {
|
|||||||
<></>
|
<></>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Tab.Screen name="home" component={HomeTab} options={{ tabBarLabel: 'Home' }} />
|
<Tab.Screen name="home" component={HomeTab} options={{ tabBarLabel: t('home') }} />
|
||||||
<Tab.Screen name="library" component={LibraryTab} options={{ tabBarLabel: 'Library' }} />
|
<Tab.Screen name="library" component={LibraryTab} options={{ tabBarLabel: t('library') }} />
|
||||||
<Tab.Screen name="search" component={SearchTab} options={{ tabBarLabel: 'Search' }} />
|
<Tab.Screen name="search" component={SearchTab} options={{ tabBarLabel: t('search') }} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Tab.Screen name="settings" component={SettingsTab} options={{ tabBarLabel: 'Settings' }} />
|
<Tab.Screen name="settings" component={SettingsTab} options={{ tabBarLabel: t('settings') }} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
export default BottomTabNavigator
|
export default BottomTabNavigator
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { withSuspense } from '@app/components/withSuspense'
|
||||||
import AlbumsTab from '@app/screens/LibraryAlbums'
|
import AlbumsTab from '@app/screens/LibraryAlbums'
|
||||||
import ArtistsTab from '@app/screens/LibraryArtists'
|
import ArtistsTab from '@app/screens/LibraryArtists'
|
||||||
import PlaylistsTab from '@app/screens/LibraryPlaylists'
|
import PlaylistsTab from '@app/screens/LibraryPlaylists'
|
||||||
@ -6,12 +7,14 @@ import dimensions from '@app/styles/dimensions'
|
|||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
|
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
|
||||||
const Tab = createMaterialTopTabNavigator()
|
const Tab = createMaterialTopTabNavigator()
|
||||||
|
|
||||||
const LibraryTopTabNavigator = () => {
|
const LibraryTopTabNavigator = withSuspense(() => {
|
||||||
|
const { t } = useTranslation('resources')
|
||||||
const marginTop = useSafeAreaInsets().top
|
const marginTop = useSafeAreaInsets().top
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -22,12 +25,16 @@ const LibraryTopTabNavigator = () => {
|
|||||||
indicatorStyle: styles.tabindicatorStyle,
|
indicatorStyle: styles.tabindicatorStyle,
|
||||||
}}
|
}}
|
||||||
initialRouteName="albums">
|
initialRouteName="albums">
|
||||||
<Tab.Screen name="albums" component={AlbumsTab} options={{ tabBarLabel: 'Albums' }} />
|
<Tab.Screen name="albums" component={AlbumsTab} options={{ tabBarLabel: t('album.name', { count: 2 }) }} />
|
||||||
<Tab.Screen name="artists" component={ArtistsTab} options={{ tabBarLabel: 'Artists' }} />
|
<Tab.Screen name="artists" component={ArtistsTab} options={{ tabBarLabel: t('artist.name', { count: 2 }) }} />
|
||||||
<Tab.Screen name="playlists" component={PlaylistsTab} options={{ tabBarLabel: 'Playlists' }} />
|
<Tab.Screen
|
||||||
|
name="playlists"
|
||||||
|
component={PlaylistsTab}
|
||||||
|
options={{ tabBarLabel: t('playlist.name', { count: 2 }) }}
|
||||||
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
tabBar: {
|
tabBar: {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { withSuspense } from '@app/components/withSuspense'
|
||||||
import BottomTabNavigator from '@app/navigation/BottomTabNavigator'
|
import BottomTabNavigator from '@app/navigation/BottomTabNavigator'
|
||||||
import NowPlayingQueue from '@app/screens/NowPlayingQueue'
|
import NowPlayingQueue from '@app/screens/NowPlayingQueue'
|
||||||
import NowPlayingView from '@app/screens/NowPlayingView'
|
import NowPlayingView from '@app/screens/NowPlayingView'
|
||||||
@ -5,32 +6,37 @@ import colors from '@app/styles/colors'
|
|||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { DarkTheme, NavigationContainer } from '@react-navigation/native'
|
import { DarkTheme, NavigationContainer } from '@react-navigation/native'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
const NowPlayingStack = createNativeStackNavigator()
|
const NowPlayingStack = createNativeStackNavigator()
|
||||||
|
|
||||||
const NowPlayingNavigator = () => (
|
const NowPlayingNavigator = withSuspense(() => {
|
||||||
<NowPlayingStack.Navigator>
|
const { t } = useTranslation('resources.queue')
|
||||||
<NowPlayingStack.Screen name="main" component={NowPlayingView} options={{ headerShown: false }} />
|
|
||||||
<NowPlayingStack.Screen
|
return (
|
||||||
name="queue"
|
<NowPlayingStack.Navigator>
|
||||||
component={NowPlayingQueue}
|
<NowPlayingStack.Screen name="main" component={NowPlayingView} options={{ headerShown: false }} />
|
||||||
options={{
|
<NowPlayingStack.Screen
|
||||||
title: 'Queue',
|
name="queue"
|
||||||
headerStyle: {
|
component={NowPlayingQueue}
|
||||||
backgroundColor: colors.gradient.high,
|
options={{
|
||||||
},
|
title: t('name'),
|
||||||
headerTitleStyle: {
|
headerStyle: {
|
||||||
fontSize: 18,
|
backgroundColor: colors.gradient.high,
|
||||||
fontFamily: font.semiBold,
|
},
|
||||||
color: colors.text.primary,
|
headerTitleStyle: {
|
||||||
},
|
fontSize: 18,
|
||||||
headerHideShadow: true,
|
fontFamily: font.semiBold,
|
||||||
headerTintColor: 'white',
|
color: colors.text.primary,
|
||||||
}}
|
},
|
||||||
/>
|
headerHideShadow: true,
|
||||||
</NowPlayingStack.Navigator>
|
headerTintColor: 'white',
|
||||||
)
|
}}
|
||||||
|
/>
|
||||||
|
</NowPlayingStack.Navigator>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const RootStack = createNativeStackNavigator()
|
const RootStack = createNativeStackNavigator()
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import GradientScrollView from '@app/components/GradientScrollView'
|
|||||||
import Header from '@app/components/Header'
|
import Header from '@app/components/Header'
|
||||||
import HeaderBar from '@app/components/HeaderBar'
|
import HeaderBar from '@app/components/HeaderBar'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
|
import { withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
import { useQueryArtist, useQueryArtistTopSongs } from '@app/hooks/query'
|
import { useQueryArtist, useQueryArtistTopSongs } from '@app/hooks/query'
|
||||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||||
import { Album, Song } from '@app/models/library'
|
import { Album, Song } from '@app/models/library'
|
||||||
@ -15,6 +16,7 @@ import { useLayout } from '@react-native-community/hooks'
|
|||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import equal from 'fast-deep-equal/es6/react'
|
import equal from 'fast-deep-equal/es6/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
import { useAnimatedScrollHandler, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
||||||
|
|
||||||
@ -42,53 +44,63 @@ const AlbumItem = React.memo<{
|
|||||||
)
|
)
|
||||||
}, equal)
|
}, equal)
|
||||||
|
|
||||||
const TopSongs = React.memo<{
|
const TopSongs = withSuspenseMemo<{
|
||||||
songs: Song[]
|
songs: Song[]
|
||||||
name: string
|
name: string
|
||||||
}>(({ songs, name }) => {
|
}>(
|
||||||
const { setQueue, isReady, contextId } = useSetQueue('artist', songs)
|
({ songs, name }) => {
|
||||||
|
const { setQueue, isReady, contextId } = useSetQueue('artist', songs)
|
||||||
|
const { t } = useTranslation('resources.song.lists')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header>Top Songs</Header>
|
<Header>{t('artistTopSongs')}</Header>
|
||||||
{songs.slice(0, 5).map((s, i) => (
|
{songs.slice(0, 5).map((s, i) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={i}
|
key={i}
|
||||||
item={s}
|
item={s}
|
||||||
contextId={contextId}
|
contextId={contextId}
|
||||||
queueId={i}
|
queueId={i}
|
||||||
showArt={true}
|
showArt={true}
|
||||||
subtitle={s.album}
|
subtitle={s.album}
|
||||||
onPress={() => setQueue({ title: name, playTrack: i })}
|
onPress={() => setQueue({ title: name, playTrack: i })}
|
||||||
disabled={!isReady}
|
disabled={!isReady}
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}, equal)
|
|
||||||
|
|
||||||
const ArtistAlbums = React.memo<{
|
|
||||||
albums: Album[]
|
|
||||||
}>(({ albums }) => {
|
|
||||||
const albumsLayout = useLayout()
|
|
||||||
|
|
||||||
const sortedAlbums = [...albums]
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
|
||||||
|
|
||||||
const albumSize = albumsLayout.width / 2 - styles.contentContainer.paddingHorizontal / 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header>Albums</Header>
|
|
||||||
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
|
||||||
{sortedAlbums.map(a => (
|
|
||||||
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</>
|
||||||
</>
|
)
|
||||||
)
|
},
|
||||||
}, equal)
|
null,
|
||||||
|
equal,
|
||||||
|
)
|
||||||
|
|
||||||
|
const ArtistAlbums = withSuspenseMemo<{
|
||||||
|
albums: Album[]
|
||||||
|
}>(
|
||||||
|
({ albums }) => {
|
||||||
|
const albumsLayout = useLayout()
|
||||||
|
const { t } = useTranslation('resources.album')
|
||||||
|
|
||||||
|
const sortedAlbums = [...albums]
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
.sort((a, b) => (b.year || 0) - (a.year || 0))
|
||||||
|
|
||||||
|
const albumSize = albumsLayout.width / 2 - styles.contentContainer.paddingHorizontal / 2
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header>{t('name', { count: 1 })}</Header>
|
||||||
|
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
||||||
|
{sortedAlbums.map(a => (
|
||||||
|
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
equal,
|
||||||
|
)
|
||||||
|
|
||||||
const ArtistViewFallback = React.memo(() => (
|
const ArtistViewFallback = React.memo(() => (
|
||||||
<GradientBackground style={styles.fallback}>
|
<GradientBackground style={styles.fallback}>
|
||||||
|
|||||||
@ -3,25 +3,20 @@ import CoverArt from '@app/components/CoverArt'
|
|||||||
import GradientScrollView from '@app/components/GradientScrollView'
|
import GradientScrollView from '@app/components/GradientScrollView'
|
||||||
import Header from '@app/components/Header'
|
import Header from '@app/components/Header'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
|
import { withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
import { useQueryHomeLists } from '@app/hooks/query'
|
import { useQueryHomeLists } from '@app/hooks/query'
|
||||||
import { Album } from '@app/models/library'
|
import { Album } from '@app/models/library'
|
||||||
import { useStoreDeep } from '@app/state/store'
|
import { useStoreDeep } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { GetAlbumList2TypeBase, GetAlbumListType } from '@app/subsonic/params'
|
import { GetAlbumList2TypeBase } from '@app/subsonic/params'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import equal from 'fast-deep-equal/es6/react'
|
import equal from 'fast-deep-equal/es6/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'
|
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
|
||||||
const titles: { [key in GetAlbumListType]?: string } = {
|
|
||||||
recent: 'Recently Played',
|
|
||||||
random: 'Random Albums',
|
|
||||||
frequent: 'Frequently Played',
|
|
||||||
starred: 'Starred Albums',
|
|
||||||
}
|
|
||||||
|
|
||||||
const AlbumItem = React.memo<{
|
const AlbumItem = React.memo<{
|
||||||
album: Album
|
album: Album
|
||||||
}>(({ album }) => {
|
}>(({ album }) => {
|
||||||
@ -49,6 +44,12 @@ const AlbumItem = React.memo<{
|
|||||||
)
|
)
|
||||||
}, equal)
|
}, equal)
|
||||||
|
|
||||||
|
const CategoryHeader = withSuspenseMemo<{ type: string }>(({ type }) => {
|
||||||
|
const { t } = useTranslation('resources.album.lists')
|
||||||
|
console.log('type', type, t(type))
|
||||||
|
return <Header style={styles.header}>{t(type)}</Header>
|
||||||
|
})
|
||||||
|
|
||||||
const Category = React.memo<{
|
const Category = React.memo<{
|
||||||
type: string
|
type: string
|
||||||
albums: Album[]
|
albums: Album[]
|
||||||
@ -74,7 +75,7 @@ const Category = React.memo<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.category}>
|
<View style={styles.category}>
|
||||||
<Header style={styles.header}>{titles[type as GetAlbumListType] || ''}</Header>
|
<CategoryHeader type={type} />
|
||||||
{albums.length > 0 ? <Albums /> : <Nothing />}
|
{albums.length > 0 ? <Albums /> : <Nothing />}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import { AlbumContextPressable } from '@app/components/ContextMenu'
|
import { AlbumContextPressable } from '@app/components/ContextMenu'
|
||||||
import CoverArt from '@app/components/CoverArt'
|
import CoverArt from '@app/components/CoverArt'
|
||||||
import FilterButton, { OptionData } from '@app/components/FilterButton'
|
import FilterButton from '@app/components/FilterButton'
|
||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
|
import { withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
import { useQueryAlbumList } from '@app/hooks/query'
|
import { useQueryAlbumList } from '@app/hooks/query'
|
||||||
import { Album } from '@app/models/library'
|
import { Album } from '@app/models/library'
|
||||||
import { useStore, useStoreDeep } from '@app/state/store'
|
import { useStore, useStoreDeep } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { GetAlbumList2Type, GetAlbumList2TypeBase } from '@app/subsonic/params'
|
import { GetAlbumList2TypeBase } from '@app/subsonic/params'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import equal from 'fast-deep-equal/es6/react'
|
import equal from 'fast-deep-equal/es6/react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||||
|
|
||||||
const AlbumItem = React.memo<{
|
const AlbumItem = React.memo<{
|
||||||
@ -53,23 +55,36 @@ const AlbumListRenderItem: React.FC<{
|
|||||||
item: { album: Album; size: number; height: number }
|
item: { album: Album; size: number; height: number }
|
||||||
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
|
}> = ({ item }) => <AlbumItem album={item.album} size={item.size} height={item.height} />
|
||||||
|
|
||||||
const filterOptions: OptionData[] = [
|
const filterValues: GetAlbumList2TypeBase[] = [
|
||||||
{ text: 'By Name', value: 'alphabeticalByName' },
|
'alphabeticalByName', //
|
||||||
{ text: 'By Artist', value: 'alphabeticalByArtist' },
|
'alphabeticalByArtist',
|
||||||
{ text: 'Newest', value: 'newest' },
|
'newest',
|
||||||
{ text: 'Frequent', value: 'frequent' },
|
'frequent',
|
||||||
{ text: 'Recent', value: 'recent' },
|
'recent',
|
||||||
{ text: 'Starred', value: 'starred' },
|
'starred',
|
||||||
{ text: 'Random', value: 'random' },
|
'random',
|
||||||
// { text: 'By Year...', value: 'byYear' },
|
|
||||||
// { text: 'By Genre...', value: 'byGenre' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const AlbumsList = () => {
|
const AlbumFilterButton = withSuspenseMemo(() => {
|
||||||
const filter = useStoreDeep(store => store.settings.screens.library.albumsFilter)
|
const { t } = useTranslation('resources.album.lists')
|
||||||
const setFilter = useStore(store => store.setLibraryAlbumFilter)
|
const filterType = useStoreDeep(store => store.settings.screens.library.albumsFilter.type)
|
||||||
|
const setFilterType = useStore(store => store.setLibraryAlbumFilterType)
|
||||||
|
|
||||||
const { isLoading, data, fetchNextPage, refetch } = useQueryAlbumList(filter.type as GetAlbumList2TypeBase, 300)
|
return (
|
||||||
|
<FilterButton
|
||||||
|
data={filterValues.map(value => ({ value, text: t(value) }))}
|
||||||
|
value={filterType}
|
||||||
|
onSelect={selection => {
|
||||||
|
setFilterType(selection as GetAlbumList2TypeBase)
|
||||||
|
}}
|
||||||
|
title={t('sort')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const AlbumsList = () => {
|
||||||
|
const filterType = useStoreDeep(store => store.settings.screens.library.albumsFilter.type)
|
||||||
|
const { isLoading, data, fetchNextPage, refetch } = useQueryAlbumList(filterType as GetAlbumList2TypeBase, 300)
|
||||||
|
|
||||||
const layout = useWindowDimensions()
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
@ -91,16 +106,7 @@ const AlbumsList = () => {
|
|||||||
onEndReachedThreshold={6}
|
onEndReachedThreshold={6}
|
||||||
windowSize={5}
|
windowSize={5}
|
||||||
/>
|
/>
|
||||||
<FilterButton
|
<AlbumFilterButton />
|
||||||
data={filterOptions}
|
|
||||||
value={filter.type}
|
|
||||||
onSelect={selection => {
|
|
||||||
setFilter({
|
|
||||||
...filter,
|
|
||||||
type: selection as GetAlbumList2Type,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,42 @@
|
|||||||
import FilterButton, { OptionData } from '@app/components/FilterButton'
|
import FilterButton from '@app/components/FilterButton'
|
||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
|
import { withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
import { useQueryArtists } from '@app/hooks/query'
|
import { useQueryArtists } from '@app/hooks/query'
|
||||||
import { Artist } from '@app/models/library'
|
import { Artist } from '@app/models/library'
|
||||||
import { ArtistFilterType } from '@app/models/settings'
|
import { ArtistFilterType } from '@app/models/settings'
|
||||||
import { useStore, useStoreDeep } from '@app/state/store'
|
import { useStore, useStoreDeep } from '@app/state/store'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
|
||||||
const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||||
<ListItem item={item} showArt={true} showStar={false} listStyle="big" style={styles.listItem} />
|
<ListItem item={item} showArt={true} showStar={false} listStyle="big" style={styles.listItem} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const filterOptions: OptionData[] = [
|
const filterValues: ArtistFilterType[] = [
|
||||||
{ text: 'By Name', value: 'alphabeticalByName' },
|
'alphabeticalByName', //
|
||||||
{ text: 'Starred', value: 'starred' },
|
'starred',
|
||||||
{ text: 'Random', value: 'random' },
|
'random',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const ArtistFilterButton = withSuspenseMemo(() => {
|
||||||
|
const { t } = useTranslation('resources.artist.lists')
|
||||||
|
const filterType = useStoreDeep(store => store.settings.screens.library.artistsFilter.type)
|
||||||
|
const setFilterType = useStore(store => store.setLibraryArtistFilterType)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterButton
|
||||||
|
data={filterValues.map(value => ({ value, text: t(value) }))}
|
||||||
|
value={filterType}
|
||||||
|
onSelect={selection => setFilterType(selection as ArtistFilterType)}
|
||||||
|
title={t('sort')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ArtistsList = () => {
|
||||||
const filter = useStoreDeep(store => store.settings.screens.library.artistsFilter)
|
const filterType = useStore(store => store.settings.screens.library.artistsFilter.type)
|
||||||
const setFilter = useStore(store => store.setLibraryArtistFiler)
|
|
||||||
|
|
||||||
const { isLoading, data, refetch } = useQueryArtists()
|
const { isLoading, data, refetch } = useQueryArtists()
|
||||||
const [sortedList, setSortedList] = useState<Artist[]>([])
|
const [sortedList, setSortedList] = useState<Artist[]>([])
|
||||||
@ -32,7 +48,7 @@ const ArtistsList = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const list = Object.values(data.byId)
|
const list = Object.values(data.byId)
|
||||||
switch (filter.type) {
|
switch (filterType) {
|
||||||
case 'random':
|
case 'random':
|
||||||
setSortedList([...list].sort(() => Math.random() - 0.5))
|
setSortedList([...list].sort(() => Math.random() - 0.5))
|
||||||
break
|
break
|
||||||
@ -46,7 +62,7 @@ const ArtistsList = () => {
|
|||||||
setSortedList([...list])
|
setSortedList([...list])
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [filter.type, data])
|
}, [filterType, data])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
@ -60,16 +76,7 @@ const ArtistsList = () => {
|
|||||||
windowSize={3}
|
windowSize={3}
|
||||||
contentMarginTop={6}
|
contentMarginTop={6}
|
||||||
/>
|
/>
|
||||||
<FilterButton
|
<ArtistFilterButton />
|
||||||
data={filterOptions}
|
|
||||||
value={filter.type}
|
|
||||||
onSelect={selection => {
|
|
||||||
setFilter({
|
|
||||||
...filter,
|
|
||||||
type: selection as ArtistFilterType,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import HeaderBar from '@app/components/HeaderBar'
|
|||||||
import ImageGradientBackground from '@app/components/ImageGradientBackground'
|
import ImageGradientBackground from '@app/components/ImageGradientBackground'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import PressableOpacity from '@app/components/PressableOpacity'
|
||||||
import { PressableStar } from '@app/components/Star'
|
import { PressableStar } from '@app/components/Star'
|
||||||
|
import { withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
import { useNext, usePause, usePlay, usePrevious, useSeekTo } from '@app/hooks/trackplayer'
|
import { useNext, usePause, usePlay, usePrevious, useSeekTo } from '@app/hooks/trackplayer'
|
||||||
import { mapTrackExtToSong } from '@app/models/map'
|
import { mapTrackExtToSong } from '@app/models/map'
|
||||||
import { QueueContextType, TrackExt } from '@app/models/trackplayer'
|
import { TrackExt } from '@app/models/trackplayer'
|
||||||
import { useStore, useStoreDeep } from '@app/state/store'
|
import { useStore, useStoreDeep } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
@ -13,6 +14,7 @@ import formatDuration from '@app/util/formatDuration'
|
|||||||
import Slider from '@react-native-community/slider'
|
import Slider from '@react-native-community/slider'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ActivityIndicator, StyleSheet, Text, TextStyle, View } from 'react-native'
|
import { ActivityIndicator, StyleSheet, Text, TextStyle, View } from 'react-native'
|
||||||
import { NativeStackScreenProps } from 'react-native-screens/native-stack'
|
import { NativeStackScreenProps } from 'react-native-screens/native-stack'
|
||||||
import { RepeatMode, State } from 'react-native-track-player'
|
import { RepeatMode, State } from 'react-native-track-player'
|
||||||
@ -21,32 +23,27 @@ import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
|||||||
import Icon from 'react-native-vector-icons/Ionicons'
|
import Icon from 'react-native-vector-icons/Ionicons'
|
||||||
import IconMatCom from 'react-native-vector-icons/MaterialCommunityIcons'
|
import IconMatCom from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||||
|
|
||||||
function getContextName(type?: QueueContextType) {
|
const NowPlayingHeader = withSuspenseMemo<{
|
||||||
switch (type) {
|
|
||||||
case 'album':
|
|
||||||
return 'Album'
|
|
||||||
case 'artist':
|
|
||||||
return 'Top Songs'
|
|
||||||
case 'playlist':
|
|
||||||
return 'Playlist'
|
|
||||||
case 'song':
|
|
||||||
return 'Search Results'
|
|
||||||
default:
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NowPlayingHeader = React.memo<{
|
|
||||||
track?: TrackExt
|
track?: TrackExt
|
||||||
}>(({ track }) => {
|
}>(({ track }) => {
|
||||||
const queueName = useStore(store => store.queueName)
|
const queueName = useStore(store => store.queueName)
|
||||||
const queueContextType = useStore(store => store.queueContextType)
|
const queueContextType = useStore(store => store.queueContextType)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (!track) {
|
if (!track) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
let contextName = getContextName(queueContextType)
|
let contextName: string
|
||||||
|
if (queueContextType === 'album') {
|
||||||
|
contextName = t('resources.album.name')
|
||||||
|
} else if (queueContextType === 'artist') {
|
||||||
|
contextName = t('resources.song.lists.artistTopSongs')
|
||||||
|
} else if (queueContextType === 'playlist') {
|
||||||
|
contextName = t('resources.playlist.name')
|
||||||
|
} else if (queueContextType === 'song') {
|
||||||
|
contextName = t('search.nowPlayingContext')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import Header from '@app/components/Header'
|
|||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
import TextInput from '@app/components/TextInput'
|
import TextInput from '@app/components/TextInput'
|
||||||
|
import { withSuspense, withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
import { useQuerySearchResults } from '@app/hooks/query'
|
import { useQuerySearchResults } from '@app/hooks/query'
|
||||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||||
import { Album, Artist, SearchResults, Song } from '@app/models/library'
|
import { Album, Artist, SearchResults, Song } from '@app/models/library'
|
||||||
@ -13,6 +14,7 @@ import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
|||||||
import equal from 'fast-deep-equal/es6/react'
|
import equal from 'fast-deep-equal/es6/react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
InteractionManager,
|
InteractionManager,
|
||||||
@ -39,56 +41,68 @@ const SongItem = React.memo<{ item: Song }>(({ item }) => {
|
|||||||
)
|
)
|
||||||
}, equal)
|
}, equal)
|
||||||
|
|
||||||
const ResultsCategory = React.memo<{
|
const ResultsCategory = withSuspenseMemo<{
|
||||||
name: string
|
name: string
|
||||||
query: string
|
query: string
|
||||||
items: (Artist | Album | Song)[]
|
items: (Artist | Album | Song)[]
|
||||||
type: 'artist' | 'album' | 'song'
|
type: 'artist' | 'album' | 'song'
|
||||||
}>(({ name, query, type, items }) => {
|
}>(
|
||||||
const navigation = useNavigation()
|
({ name, query, type, items }) => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
const { t } = useTranslation('search')
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return <></>
|
return <></>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header>{name}</Header>
|
<Header>{name}</Header>
|
||||||
{items.map(a =>
|
{items.map(a =>
|
||||||
type === 'song' ? (
|
type === 'song' ? (
|
||||||
<SongItem key={a.id} item={a as Song} />
|
<SongItem key={a.id} item={a as Song} />
|
||||||
) : (
|
) : (
|
||||||
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
|
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{items.length === 5 && (
|
{items.length === 5 && (
|
||||||
<Button
|
<Button
|
||||||
title="More..."
|
title={t('moreResults')}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
style={styles.more}
|
style={styles.more}
|
||||||
onPress={() => navigation.navigate('results', { query, type: items[0].itemType })}
|
onPress={() => navigation.navigate('results', { query, type: items[0].itemType })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}, equal)
|
},
|
||||||
|
null,
|
||||||
|
equal,
|
||||||
|
)
|
||||||
|
|
||||||
const Results = React.memo<{
|
const Results = withSuspenseMemo<{
|
||||||
results: SearchResults
|
results: SearchResults
|
||||||
query: string
|
query: string
|
||||||
}>(({ results, query }) => {
|
}>(
|
||||||
return (
|
({ results, query }) => {
|
||||||
<>
|
const { t } = useTranslation('resources')
|
||||||
<ResultsCategory name="Artists" query={query} type={'artist'} items={results.artists} />
|
|
||||||
<ResultsCategory name="Albums" query={query} type={'album'} items={results.albums} />
|
|
||||||
<ResultsCategory name="Songs" query={query} type={'song'} items={results.songs} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}, equal)
|
|
||||||
|
|
||||||
const Search = () => {
|
return (
|
||||||
|
<>
|
||||||
|
<ResultsCategory name={t('artist.name', { count: 2 })} query={query} type={'artist'} items={results.artists} />
|
||||||
|
<ResultsCategory name={t('album.name', { count: 2 })} query={query} type={'album'} items={results.albums} />
|
||||||
|
<ResultsCategory name={t('song.name', { count: 2 })} query={query} type={'song'} items={results.songs} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
equal,
|
||||||
|
)
|
||||||
|
|
||||||
|
const Search = withSuspense(() => {
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const { data, isLoading } = useQuerySearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 })
|
const { data, isLoading } = useQuerySearchResults({ query, albumCount: 5, artistCount: 5, songCount: 5 })
|
||||||
|
const { t } = useTranslation('search')
|
||||||
|
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
const searchBarRef = useRef<ReactTextInput>(null)
|
const searchBarRef = useRef<ReactTextInput>(null)
|
||||||
@ -140,7 +154,7 @@ const Search = () => {
|
|||||||
<TextInput
|
<TextInput
|
||||||
ref={searchBarRef}
|
ref={searchBarRef}
|
||||||
style={styles.textInput}
|
style={styles.textInput}
|
||||||
placeholder="Search"
|
placeholder={t('inputPlaceholder')}
|
||||||
value={text}
|
value={text}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
/>
|
/>
|
||||||
@ -154,7 +168,7 @@ const Search = () => {
|
|||||||
</View>
|
</View>
|
||||||
</GradientScrollView>
|
</GradientScrollView>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
scroll: {
|
scroll: {
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
|
import { withSuspense } from '@app/components/withSuspense'
|
||||||
import { useQuerySearchResults } from '@app/hooks/query'
|
import { useQuerySearchResults } from '@app/hooks/query'
|
||||||
import { useSetQueue } from '@app/hooks/trackplayer'
|
import { useSetQueue } from '@app/hooks/trackplayer'
|
||||||
import { Album, Artist, Song } from '@app/models/library'
|
import { Album, Artist, Song } from '@app/models/library'
|
||||||
import { Search3Params } from '@app/subsonic/params'
|
import { Search3Params } from '@app/subsonic/params'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
|
|
||||||
type SearchListItemType = Album | Song | Artist
|
type SearchListItemType = Album | Song | Artist
|
||||||
@ -52,11 +54,12 @@ const ResultsListItem: React.FC<{ item: SearchListItemType }> = ({ item }) => {
|
|||||||
|
|
||||||
const SearchResultsRenderItem: React.FC<{ item: SearchListItemType }> = ({ item }) => <ResultsListItem item={item} />
|
const SearchResultsRenderItem: React.FC<{ item: SearchListItemType }> = ({ item }) => <ResultsListItem item={item} />
|
||||||
|
|
||||||
const SearchResultsView: React.FC<{
|
const SearchResultsView = withSuspense<{
|
||||||
query: string
|
query: string
|
||||||
type: 'album' | 'artist' | 'song'
|
type: 'album' | 'artist' | 'song'
|
||||||
}> = ({ query, type }) => {
|
}>(({ query, type }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
const { t } = useTranslation('search')
|
||||||
|
|
||||||
const size = 100
|
const size = 100
|
||||||
const params: Search3Params = { query }
|
const params: Search3Params = { query }
|
||||||
@ -82,7 +85,7 @@ const SearchResultsView: React.FC<{
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
title: `Search: "${query}"`,
|
title: t('headerTitle', { query }),
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
@ -102,7 +105,7 @@ const SearchResultsView: React.FC<{
|
|||||||
windowSize={5}
|
windowSize={5}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
listItem: {
|
listItem: {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import Button from '@app/components/Button'
|
import Button from '@app/components/Button'
|
||||||
import GradientScrollView from '@app/components/GradientScrollView'
|
import GradientScrollView from '@app/components/GradientScrollView'
|
||||||
|
import SettingsSwitch from '@app/components/SettingsSwitch'
|
||||||
|
import { withSuspense } from '@app/components/withSuspense'
|
||||||
import { Server } from '@app/models/settings'
|
import { Server } from '@app/models/settings'
|
||||||
import { useStore, useStoreDeep } from '@app/state/store'
|
import { useStore, useStoreDeep } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
@ -8,15 +10,16 @@ import toast from '@app/util/toast'
|
|||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import md5 from 'md5'
|
import md5 from 'md5'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native'
|
import { StyleSheet, Text, TextInput, View, ViewStyle } from 'react-native'
|
||||||
import uuid from 'react-native-uuid'
|
import uuid from 'react-native-uuid'
|
||||||
import SettingsSwitch from '@app/components/SettingsSwitch'
|
|
||||||
|
|
||||||
const PASSWORD_PLACEHOLDER = 'PASSWORD_PLACEHOLDER'
|
const PASSWORD_PLACEHOLDER = 'PASSWORD_PLACEHOLDER'
|
||||||
|
|
||||||
const ServerView: React.FC<{
|
const ServerView = withSuspense<{
|
||||||
id?: string
|
id?: string
|
||||||
}> = ({ id }) => {
|
}>(({ id }) => {
|
||||||
|
const { t } = useTranslation('settings.servers')
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const activeServerId = useStore(store => store.settings.activeServerId)
|
const activeServerId = useStore(store => store.settings.activeServerId)
|
||||||
const servers = useStoreDeep(store => store.settings.servers)
|
const servers = useStoreDeep(store => store.settings.servers)
|
||||||
@ -134,15 +137,16 @@ const ServerView: React.FC<{
|
|||||||
|
|
||||||
const ping = async () => {
|
const ping = async () => {
|
||||||
const res = await pingServer(potential)
|
const res = await pingServer(potential)
|
||||||
if (res) {
|
toast(
|
||||||
toast(`Connection to ${potential.address} OK!`)
|
t(`messages.${res ? 'connectionOk' : 'connectionFailed'}`, {
|
||||||
} else {
|
address: potential.address,
|
||||||
toast(`Connection to ${potential.address} failed, check settings or server`)
|
interpolation: { escapeValue: false },
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
setTesting(false)
|
setTesting(false)
|
||||||
}
|
}
|
||||||
ping()
|
ping()
|
||||||
}, [createServer, pingServer])
|
}, [createServer, pingServer, t])
|
||||||
|
|
||||||
const disableControls = useCallback(() => {
|
const disableControls = useCallback(() => {
|
||||||
return !validate() || testing
|
return !validate() || testing
|
||||||
@ -169,7 +173,7 @@ const ServerView: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<GradientScrollView style={styles.scroll}>
|
<GradientScrollView style={styles.scroll}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Text style={styles.inputTitle}>Address</Text>
|
<Text style={styles.inputTitle}>{t('fields.address')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholderTextColor="grey"
|
placeholderTextColor="grey"
|
||||||
@ -182,7 +186,7 @@ const ServerView: React.FC<{
|
|||||||
onChangeText={setAddress}
|
onChangeText={setAddress}
|
||||||
onBlur={formatAddress}
|
onBlur={formatAddress}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.inputTitle}>Username</Text>
|
<Text style={styles.inputTitle}>{t('fields.username')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholderTextColor="grey"
|
placeholderTextColor="grey"
|
||||||
@ -195,7 +199,7 @@ const ServerView: React.FC<{
|
|||||||
value={username}
|
value={username}
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.inputTitle}>Password</Text>
|
<Text style={styles.inputTitle}>{t('fields.password')}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
placeholderTextColor="grey"
|
placeholderTextColor="grey"
|
||||||
@ -210,11 +214,11 @@ const ServerView: React.FC<{
|
|||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
/>
|
/>
|
||||||
<SettingsSwitch
|
<SettingsSwitch
|
||||||
title="Force plain text password"
|
title={t('options.forcePlaintextPassword.title')}
|
||||||
subtitle={
|
subtitle={
|
||||||
usePlainPassword
|
usePlainPassword
|
||||||
? 'Send password in plain text (legacy, make sure your connection is secure!)'
|
? t('options.forcePlaintextPassword.descriptionOn')
|
||||||
: 'Send password as token + salt'
|
: t('options.forcePlaintextPassword.descriptionOff')
|
||||||
}
|
}
|
||||||
value={usePlainPassword}
|
value={usePlainPassword}
|
||||||
setValue={togglePlainPassword}
|
setValue={togglePlainPassword}
|
||||||
@ -222,21 +226,21 @@ const ServerView: React.FC<{
|
|||||||
<Button
|
<Button
|
||||||
disabled={disableControls()}
|
disabled={disableControls()}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
title="Test Connection"
|
title={t('actions.testConnection')}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
onPress={test}
|
onPress={test}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={disableControls()}
|
disabled={disableControls()}
|
||||||
style={[styles.button, styles.delete, deleteStyle]}
|
style={[styles.button, styles.delete, deleteStyle]}
|
||||||
title="Delete"
|
title={t('actions.delete')}
|
||||||
onPress={remove}
|
onPress={remove}
|
||||||
/>
|
/>
|
||||||
<Button disabled={disableControls()} style={styles.button} title="Save" onPress={save} />
|
<Button disabled={disableControls()} style={styles.button} title={t('actions.save')} onPress={save} />
|
||||||
</View>
|
</View>
|
||||||
</GradientScrollView>
|
</GradientScrollView>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
scroll: {
|
scroll: {
|
||||||
|
|||||||
@ -5,13 +5,15 @@ import PressableOpacity from '@app/components/PressableOpacity'
|
|||||||
import SettingsItem from '@app/components/SettingsItem'
|
import SettingsItem from '@app/components/SettingsItem'
|
||||||
import SettingsSwitch from '@app/components/SettingsSwitch'
|
import SettingsSwitch from '@app/components/SettingsSwitch'
|
||||||
import TextInput from '@app/components/TextInput'
|
import TextInput from '@app/components/TextInput'
|
||||||
import { useSwitchActiveServer, useResetImageCache } from '@app/hooks/settings'
|
import { withSuspenseMemo } from '@app/components/withSuspense'
|
||||||
|
import { useResetImageCache, useSwitchActiveServer } from '@app/hooks/settings'
|
||||||
import { Server } from '@app/models/settings'
|
import { Server } from '@app/models/settings'
|
||||||
import { useStore, useStoreDeep } from '@app/state/store'
|
import { useStore, useStoreDeep } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { useNavigation } from '@react-navigation/core'
|
import { useNavigation } from '@react-navigation/core'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { KeyboardTypeOptions, Linking, Modal, Pressable, StyleSheet, Text, View } from 'react-native'
|
import { KeyboardTypeOptions, Linking, Modal, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
@ -73,25 +75,23 @@ const ModalChoice = React.memo<{
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function bitrateString(bitrate: number): string {
|
const BitrateModal = withSuspenseMemo<{
|
||||||
return bitrate === 0 ? 'Unlimited' : `${bitrate}kbps`
|
|
||||||
}
|
|
||||||
|
|
||||||
const BitrateModal = React.memo<{
|
|
||||||
title: string
|
title: string
|
||||||
bitrate: number
|
bitrate: number
|
||||||
setBitrate: (bitrate: number) => void
|
setBitrate: (bitrate: number) => void
|
||||||
}>(({ title, bitrate, setBitrate }) => {
|
}>(({ title, bitrate, setBitrate }) => {
|
||||||
|
const { t } = useTranslation('settings.network.values')
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
|
|
||||||
const toggleModal = useCallback(() => setVisible(!visible), [visible])
|
const toggleModal = useCallback(() => setVisible(!visible), [visible])
|
||||||
|
|
||||||
|
const bitrateText = useCallback((value: number) => (value === 0 ? t('unlimitedKbps') : t('kbps', { value })), [t])
|
||||||
|
|
||||||
const BitrateChoice: React.FC<{ value: number }> = useCallback(
|
const BitrateChoice: React.FC<{ value: number }> = useCallback(
|
||||||
({ value }) => {
|
({ value }) => {
|
||||||
const text = bitrateString(value)
|
|
||||||
return (
|
return (
|
||||||
<ModalChoice
|
<ModalChoice
|
||||||
text={text}
|
text={bitrateText(value)}
|
||||||
value={value}
|
value={value}
|
||||||
setValue={setBitrate}
|
setValue={setBitrate}
|
||||||
closeModal={toggleModal}
|
closeModal={toggleModal}
|
||||||
@ -99,12 +99,12 @@ const BitrateModal = React.memo<{
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[bitrate, toggleModal, setBitrate],
|
[bitrate, toggleModal, setBitrate, bitrateText],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsItem title={title} subtitle={bitrateString(bitrate)} onPress={toggleModal} />
|
<SettingsItem title={title} subtitle={bitrateText(bitrate)} onPress={toggleModal} />
|
||||||
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
|
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
|
||||||
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
|
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
|
||||||
<View style={styles.centeredView}>
|
<View style={styles.centeredView}>
|
||||||
@ -135,9 +135,9 @@ const SettingsTextModal = React.memo<{
|
|||||||
title: string
|
title: string
|
||||||
value: string
|
value: string
|
||||||
setValue: (text: string) => void
|
setValue: (text: string) => void
|
||||||
getUnit?: (text: string) => string
|
subtitle: (value: string) => string
|
||||||
keyboardType?: KeyboardTypeOptions
|
keyboardType?: KeyboardTypeOptions
|
||||||
}>(({ title, value, setValue, getUnit, keyboardType }) => {
|
}>(({ title, value, setValue, subtitle, keyboardType }) => {
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
const [inputText, setInputText] = useState(value)
|
const [inputText, setInputText] = useState(value)
|
||||||
|
|
||||||
@ -148,16 +148,9 @@ const SettingsTextModal = React.memo<{
|
|||||||
toggleModal()
|
toggleModal()
|
||||||
}, [inputText, setValue, toggleModal])
|
}, [inputText, setValue, toggleModal])
|
||||||
|
|
||||||
const getSubtitle = useCallback(() => {
|
|
||||||
if (!getUnit) {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return value + ' ' + getUnit(value)
|
|
||||||
}, [getUnit, value])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsItem title={title} subtitle={getSubtitle()} onPress={toggleModal} />
|
<SettingsItem title={title} subtitle={subtitle(value)} onPress={toggleModal} />
|
||||||
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
|
<Modal animationType="fade" transparent={true} visible={visible} onRequestClose={toggleModal}>
|
||||||
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
|
<Pressable style={styles.modalBackdrop} onPress={toggleModal}>
|
||||||
<View style={styles.centeredView}>
|
<View style={styles.centeredView}>
|
||||||
@ -183,15 +176,9 @@ const SettingsTextModal = React.memo<{
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function secondsUnit(seconds: string): string {
|
const SettingsContent = withSuspenseMemo(() => {
|
||||||
const numberValue = parseFloat(seconds)
|
const { t } = useTranslation('settings')
|
||||||
if (Math.abs(numberValue) !== 1) {
|
|
||||||
return 'seconds'
|
|
||||||
}
|
|
||||||
return 'second'
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsContent = React.memo(() => {
|
|
||||||
const servers = useStoreDeep(store => store.settings.servers)
|
const servers = useStoreDeep(store => store.settings.servers)
|
||||||
const scrobble = useStore(store => store.settings.scrobble)
|
const scrobble = useStore(store => store.settings.scrobble)
|
||||||
const setScrobble = useStore(store => store.setScrobble)
|
const setScrobble = useStore(store => store.setScrobble)
|
||||||
@ -221,66 +208,78 @@ const SettingsContent = React.memo(() => {
|
|||||||
const setMinBufferText = useCallback((text: string) => setMinBuffer(parseFloat(text)), [setMinBuffer])
|
const setMinBufferText = useCallback((text: string) => setMinBuffer(parseFloat(text)), [setMinBuffer])
|
||||||
const setMaxBufferText = useCallback((text: string) => setMaxBuffer(parseFloat(text)), [setMaxBuffer])
|
const setMaxBufferText = useCallback((text: string) => setMaxBuffer(parseFloat(text)), [setMaxBuffer])
|
||||||
|
|
||||||
|
const secondsText = useCallback((value: string) => t('network.values.seconds', { value }), [t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<Header>Servers</Header>
|
<Header>{t('servers.name')}</Header>
|
||||||
{Object.values(servers).map(s => (
|
{Object.values(servers).map(s => (
|
||||||
<ServerItem key={s.id} server={s} />
|
<ServerItem key={s.id} server={s} />
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
title="Add Server"
|
title={t('servers.actions.add')}
|
||||||
onPress={() => navigation.navigate('server')}
|
onPress={() => navigation.navigate('server')}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
/>
|
/>
|
||||||
<Header style={styles.header}>Network</Header>
|
<Header style={styles.header}>{t('network.name')}</Header>
|
||||||
<BitrateModal title="Maximum bitrate (Wi-Fi)" bitrate={maxBitrateWifi} setBitrate={setMaxBitrateWifi} />
|
<BitrateModal
|
||||||
<BitrateModal title="Maximum bitrate (mobile)" bitrate={maxBitrateMobile} setBitrate={setMaxBitrateMobile} />
|
title={t('network.options.maxBitrateWifi.title')}
|
||||||
|
bitrate={maxBitrateWifi}
|
||||||
|
setBitrate={setMaxBitrateWifi}
|
||||||
|
/>
|
||||||
|
<BitrateModal
|
||||||
|
title={t('network.options.maxBitrateMobile.title')}
|
||||||
|
bitrate={maxBitrateMobile}
|
||||||
|
setBitrate={setMaxBitrateMobile}
|
||||||
|
/>
|
||||||
<SettingsTextModal
|
<SettingsTextModal
|
||||||
title="Minimum buffer time"
|
title={t('network.options.minBuffer.title')}
|
||||||
value={minBuffer.toString()}
|
value={minBuffer.toString()}
|
||||||
setValue={setMinBufferText}
|
setValue={setMinBufferText}
|
||||||
getUnit={secondsUnit}
|
subtitle={secondsText}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
/>
|
/>
|
||||||
<SettingsTextModal
|
<SettingsTextModal
|
||||||
title="Maximum buffer time"
|
title={t('network.options.maxBuffer.title')}
|
||||||
value={maxBuffer.toString()}
|
value={maxBuffer.toString()}
|
||||||
setValue={setMaxBufferText}
|
setValue={setMaxBufferText}
|
||||||
getUnit={secondsUnit}
|
subtitle={secondsText}
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
/>
|
/>
|
||||||
<Header style={styles.header}>Music</Header>
|
<Header style={styles.header}>{t('music.name')}</Header>
|
||||||
<SettingsSwitch
|
<SettingsSwitch
|
||||||
title="Scrobble plays"
|
title={t('music.options.scrobble.title')}
|
||||||
subtitle={scrobble ? 'Scrobble play history' : "Don't scrobble play history"}
|
subtitle={scrobble ? t('music.options.scrobble.descriptionOn') : t('music.options.scrobble.descriptionOff')}
|
||||||
value={scrobble}
|
value={scrobble}
|
||||||
setValue={setScrobble}
|
setValue={setScrobble}
|
||||||
/>
|
/>
|
||||||
<Header style={styles.header}>Reset</Header>
|
<Header style={styles.header}>{t('reset.name')}</Header>
|
||||||
<Button
|
<Button
|
||||||
disabled={clearing}
|
disabled={clearing}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
title="Clear Image Cache"
|
title={t('reset.actions.clearImageCache')}
|
||||||
onPress={clear}
|
onPress={clear}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
/>
|
/>
|
||||||
<Header style={styles.header}>About</Header>
|
<Header style={styles.header}>{t('about.name')}</Header>
|
||||||
<Text style={styles.text}>
|
<Text style={styles.text}>
|
||||||
<Text style={styles.bold}>Subtracks</Text> version {version}
|
<Text style={styles.bold}>Subtracks</Text> {t('about.version', { version })}
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
disabled={clearing}
|
disabled={clearing}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
title="Project Homepage"
|
title={t('about.actions.projectHomepage')}
|
||||||
onPress={() => Linking.openURL('https://github.com/austinried/subtracks')}
|
onPress={() => Linking.openURL('https://github.com/austinried/subtracks')}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
disabled={clearing}
|
disabled={clearing}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
title="Licenses"
|
title={t('about.actions.licenses')}
|
||||||
onPress={() => navigation.navigate('web', { uri: 'file:///android_asset/licenses.html' })}
|
onPress={() =>
|
||||||
|
navigation.navigate('web', { uri: 'file:///android_asset/licenses.html', title: t('about.actions.licenses') })
|
||||||
|
}
|
||||||
buttonStyle="hollow"
|
buttonStyle="hollow"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -56,10 +56,8 @@ const SongListDetails = React.memo<{
|
|||||||
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
|
const [headerColor, setHeaderColor] = useState<string | undefined>(undefined)
|
||||||
|
|
||||||
const _songs = [...(songs || [])]
|
const _songs = [...(songs || [])]
|
||||||
let typeName = ''
|
|
||||||
|
|
||||||
if (type === 'album') {
|
if (type === 'album') {
|
||||||
typeName = 'Album'
|
|
||||||
if (_songs.some(s => s.track === undefined)) {
|
if (_songs.some(s => s.track === undefined)) {
|
||||||
_songs.sort((a, b) => a.title.localeCompare(b.title))
|
_songs.sort((a, b) => a.title.localeCompare(b.title))
|
||||||
} else {
|
} else {
|
||||||
@ -69,8 +67,6 @@ const SongListDetails = React.memo<{
|
|||||||
return aVal - bVal
|
return aVal - bVal
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
typeName = 'Playlist'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { setQueue, isReady, contextId } = useSetQueue(type, _songs)
|
const { setQueue, isReady, contextId } = useSetQueue(type, _songs)
|
||||||
@ -125,7 +121,7 @@ const SongListDetails = React.memo<{
|
|||||||
<ListPlayerControls
|
<ListPlayerControls
|
||||||
style={styles.controls}
|
style={styles.controls}
|
||||||
songs={_songs}
|
songs={_songs}
|
||||||
typeName={typeName}
|
listType={type}
|
||||||
play={play(undefined, false)}
|
play={play(undefined, false)}
|
||||||
shuffle={play(undefined, true)}
|
shuffle={play(undefined, true)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
import React from 'react'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
import { WebView } from 'react-native-webview'
|
import { WebView } from 'react-native-webview'
|
||||||
|
|
||||||
const WebViewScreen: React.FC<{
|
const WebViewScreen: React.FC<{
|
||||||
uri: string
|
uri: string
|
||||||
}> = ({ uri }) => {
|
title?: string
|
||||||
|
}> = ({ uri, title }) => {
|
||||||
|
const navigation = useNavigation()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
navigation.setOptions({ title })
|
||||||
|
}, [navigation, title])
|
||||||
|
|
||||||
return <WebView source={{ uri }} />
|
return <WebView source={{ uri }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { AlbumFilterSettings, ArtistFilterSettings, Server } from '@app/models/settings'
|
import { AlbumFilterSettings, ArtistFilterSettings, ArtistFilterType, Server } from '@app/models/settings'
|
||||||
import { ById } from '@app/models/state'
|
import { ById } from '@app/models/state'
|
||||||
import { GetStore, SetStore } from '@app/state/store'
|
import { GetStore, SetStore } from '@app/state/store'
|
||||||
import { SubsonicApiClient } from '@app/subsonic/api'
|
import { SubsonicApiClient } from '@app/subsonic/api'
|
||||||
|
import { GetAlbumList2TypeBase } from '@app/subsonic/params'
|
||||||
import uuid from 'react-native-uuid'
|
import uuid from 'react-native-uuid'
|
||||||
|
|
||||||
export type SettingsSlice = {
|
export type SettingsSlice = {
|
||||||
@ -43,8 +44,8 @@ export type SettingsSlice = {
|
|||||||
|
|
||||||
pingServer: (server?: Server) => Promise<boolean>
|
pingServer: (server?: Server) => Promise<boolean>
|
||||||
|
|
||||||
setLibraryAlbumFilter: (filter: AlbumFilterSettings) => void
|
setLibraryAlbumFilterType: (type: GetAlbumList2TypeBase) => void
|
||||||
setLibraryArtistFiler: (filter: ArtistFilterSettings) => void
|
setLibraryArtistFilterType: (type: ArtistFilterType) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function newCacheBuster(): string {
|
export function newCacheBuster(): string {
|
||||||
@ -216,15 +217,15 @@ export const createSettingsSlice = (set: SetStore, get: GetStore): SettingsSlice
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setLibraryAlbumFilter: filter => {
|
setLibraryAlbumFilterType: type => {
|
||||||
set(state => {
|
set(state => {
|
||||||
state.settings.screens.library.albumsFilter = filter
|
state.settings.screens.library.albumsFilter.type = type
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
setLibraryArtistFiler: filter => {
|
setLibraryArtistFilterType: type => {
|
||||||
set(state => {
|
set(state => {
|
||||||
state.settings.screens.library.artistsFilter = filter
|
state.settings.screens.library.artistsFilter.type = type
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
15
index.js
15
index.js
@ -11,6 +11,21 @@ LogBox.ignoreLogs([
|
|||||||
"[react-native-gesture-handler] Seems like you're using an old API with gesture components, check out new Gestures system!",
|
"[react-native-gesture-handler] Seems like you're using an old API with gesture components, check out new Gestures system!",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
import i18next from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
import { backend, languageDetector } from '@app/i18n'
|
||||||
|
import * as RNLocalize from 'react-native-localize'
|
||||||
|
|
||||||
|
i18next.use(backend).use(languageDetector).use(initReactI18next).init({
|
||||||
|
compatibilityJSON: 'v3',
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
RNLocalize.addEventListener('change', () => {
|
||||||
|
languageDetector.detect(lng => i18next.changeLanguage(lng))
|
||||||
|
})
|
||||||
|
|
||||||
import { AppRegistry } from 'react-native'
|
import { AppRegistry } from 'react-native'
|
||||||
import App from '@app/App'
|
import App from '@app/App'
|
||||||
import { name as appName } from '@app/app.json'
|
import { name as appName } from '@app/app.json'
|
||||||
|
|||||||
@ -31,18 +31,21 @@
|
|||||||
"@xmldom/xmldom": "^0.7.0",
|
"@xmldom/xmldom": "^0.7.0",
|
||||||
"content-disposition": "^0.5.4",
|
"content-disposition": "^0.5.4",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"i18next": "^21.6.16",
|
||||||
"immer": "^9.0.6",
|
"immer": "^9.0.6",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
"react-i18next": "^11.16.6",
|
||||||
"react-native": "0.67.4",
|
"react-native": "0.67.4",
|
||||||
"react-native-blob-util": "https://github.com/austinried/react-native-blob-util.git#android-downloadmanager-progress",
|
"react-native-blob-util": "https://github.com/austinried/react-native-blob-util.git#android-downloadmanager-progress",
|
||||||
"react-native-fs": "^2.18.0",
|
"react-native-fs": "^2.18.0",
|
||||||
"react-native-gesture-handler": "^2.3.2",
|
"react-native-gesture-handler": "^2.3.2",
|
||||||
"react-native-image-colors": "^1.3.0",
|
"react-native-image-colors": "^1.3.0",
|
||||||
"react-native-linear-gradient": "^2.5.6",
|
"react-native-linear-gradient": "^2.5.6",
|
||||||
|
"react-native-localize": "^2.2.1",
|
||||||
"react-native-popup-menu": "^0.15.11",
|
"react-native-popup-menu": "^0.15.11",
|
||||||
"react-native-reanimated": "^2.3.1",
|
"react-native-reanimated": "^2.3.1",
|
||||||
"react-native-safe-area-context": "^3.2.0",
|
"react-native-safe-area-context": "^3.2.0",
|
||||||
|
|||||||
42
yarn.lock
42
yarn.lock
@ -719,6 +719,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.4"
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2":
|
||||||
|
version "7.17.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
|
||||||
|
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.4"
|
||||||
|
|
||||||
"@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2":
|
"@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2":
|
||||||
version "7.17.8"
|
version "7.17.8"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2"
|
||||||
@ -3614,11 +3621,18 @@ html-encoding-sniffer@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-encoding "^1.0.5"
|
whatwg-encoding "^1.0.5"
|
||||||
|
|
||||||
html-escaper@^2.0.0:
|
html-escaper@^2.0.0, html-escaper@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||||
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
|
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
|
||||||
|
|
||||||
|
html-parse-stringify@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
|
||||||
|
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
|
||||||
|
dependencies:
|
||||||
|
void-elements "3.1.0"
|
||||||
|
|
||||||
http-errors@1.8.1:
|
http-errors@1.8.1:
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
|
||||||
@ -3652,6 +3666,13 @@ human-signals@^1.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||||
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
|
||||||
|
|
||||||
|
i18next@^21.6.16:
|
||||||
|
version "21.6.16"
|
||||||
|
resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.16.tgz#8cff8c3ba2ffaf8438a8c83fe284083f15cf3941"
|
||||||
|
integrity sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.17.2"
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
version "0.4.24"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
@ -6016,6 +6037,15 @@ react-freeze@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.0.tgz#b21c65fe1783743007c8c9a2952b1c8879a77354"
|
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.0.tgz#b21c65fe1783743007c8c9a2952b1c8879a77354"
|
||||||
integrity sha512-yQaiOqDmoKqks56LN9MTgY06O0qQHgV4FUrikH357DydArSZHQhl0BJFqGKIZoTqi8JizF9Dxhuk1FIZD6qCaw==
|
integrity sha512-yQaiOqDmoKqks56LN9MTgY06O0qQHgV4FUrikH357DydArSZHQhl0BJFqGKIZoTqi8JizF9Dxhuk1FIZD6qCaw==
|
||||||
|
|
||||||
|
react-i18next@^11.16.6:
|
||||||
|
version "11.16.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.16.6.tgz#e8a07802c391a55e1528673201a2727994787641"
|
||||||
|
integrity sha512-qa76GnvAPafNSxKNN/XMhdCkVN/9Lm+BpzW5+6FE2ctYUemhbglP7oklGmYiJXlG24p9itqzlJDbCi3SNd3jzA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.14.5"
|
||||||
|
html-escaper "^2.0.2"
|
||||||
|
html-parse-stringify "^3.0.1"
|
||||||
|
|
||||||
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1:
|
"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
@ -6078,6 +6108,11 @@ react-native-linear-gradient@^2.5.6:
|
|||||||
resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.5.6.tgz#96215cbc5ec7a01247a20890888aa75b834d44a0"
|
resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.5.6.tgz#96215cbc5ec7a01247a20890888aa75b834d44a0"
|
||||||
integrity sha512-HDwEaXcQIuXXCV70O+bK1rizFong3wj+5Q/jSyifKFLg0VWF95xh8XQgfzXwtq0NggL9vNjPKXa016KuFu+VFg==
|
integrity sha512-HDwEaXcQIuXXCV70O+bK1rizFong3wj+5Q/jSyifKFLg0VWF95xh8XQgfzXwtq0NggL9vNjPKXa016KuFu+VFg==
|
||||||
|
|
||||||
|
react-native-localize@^2.2.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-2.2.1.tgz#6fe646833691c6ee8a474df3c8b069402cb1dba8"
|
||||||
|
integrity sha512-BuPaQWvxLZG1NrCDGqgAnecDrNQu3LED9/Pyl4H2LwTMHcEngXpE5PfVntW2GiLumdr6nUOkWmMnh8PynZqrsw==
|
||||||
|
|
||||||
react-native-popup-menu@^0.15.11:
|
react-native-popup-menu@^0.15.11:
|
||||||
version "0.15.12"
|
version "0.15.12"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-popup-menu/-/react-native-popup-menu-0.15.12.tgz#386852f4245f8d661a5003776989b9b55c9ce381"
|
resolved "https://registry.yarnpkg.com/react-native-popup-menu/-/react-native-popup-menu-0.15.12.tgz#386852f4245f8d661a5003776989b9b55c9ce381"
|
||||||
@ -7393,6 +7428,11 @@ vlq@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"
|
resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"
|
||||||
integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==
|
integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==
|
||||||
|
|
||||||
|
void-elements@3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||||
|
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
|
||||||
|
|
||||||
w3c-hr-time@^1.0.2:
|
w3c-hr-time@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user