Compare commits

...

125 Commits

Author SHA1 Message Date
Heimen Stoffels 0b0663e1aa Translated using Weblate (Dutch)
Currently translated at 46.6% (55 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/nl/
2023-06-20 17:50:11 +00:00
Jhoan Sebastian Espinosa Borrero 7c0032133e Translated using Weblate (Spanish)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/es/
2022-11-22 00:32:44 +00:00
@liimee 70fdfe3236 Translated using Weblate (Indonesian)
Currently translated at 36.4% (43 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/id/
2022-10-05 14:35:04 +00:00
@liimee 7067c6807a Added translation using Weblate (Indonesian) 2022-10-04 13:38:38 +00:00
ghose b024264750 Translated using Weblate (Galician)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/gl/
2022-04-23 13:59:36 +00:00
adil 232fc0aae1 Translated using Weblate (Russian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/ru/
2022-03-10 05:17:09 +00:00
emptyList() e59a369661 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/zh_Hans/
2022-01-22 10:36:45 +00:00
Sergio Varela 5d4944e87a Translated using Weblate (Spanish)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/es/
2021-12-11 15:36:31 +00:00
ghose e1d41ed675 Translated using Weblate (Galician)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/gl/
2021-11-16 19:17:30 +00:00
Allan Nordhøy 9d741bc6eb Translated using Weblate (Norwegian Bokmål)
Currently translated at 98.3% (116 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/nb_NO/
2021-11-13 05:17:30 +00:00
Allan Nordhøy ec1e4ff629 Translated using Weblate (English)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/en/
2021-11-13 05:17:29 +00:00
milotype dfe9783048 Translated using Weblate (Croatian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/hr/
2021-10-25 17:27:49 +00:00
Homer S bfa7a99015 Translated using Weblate (Norwegian Bokmål)
Currently translated at 21.1% (25 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/nb_NO/
2021-08-08 00:04:41 +00:00
Homer S 8cd278be7f Added translation using Weblate (Norwegian Bokmål) 2021-08-06 20:46:41 +00:00
Dignified Silence 9a1dfe59ad Translated using Weblate (Japanese)
Currently translated at 86.4% (102 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/ja/
2021-07-20 19:39:06 +00:00
Dignified Silence 6f24d4906b Translated using Weblate (Japanese)
Currently translated at 22.8% (27 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/ja/
2021-07-18 19:39:05 +00:00
Dignified Silence 1bf7f2ef38 Added translation using Weblate (Japanese) 2021-07-17 18:31:18 +00:00
Creak db171fb406 Translated using Weblate (French)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/fr/
2021-06-29 17:04:09 +00:00
Ryan Harg 5cfb0cbdaf Translated using Weblate (German)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2021-06-29 17:04:08 +00:00
helabasa a77d3c0222 Translated using Weblate (Sinhala)
Currently translated at 8.4% (10 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/si/
2021-03-12 05:13:51 +00:00
helabasa fe014cde1a Added translation using Weblate (Sinhala) 2021-03-11 04:14:53 +00:00
x 641ead2c53 Translated using Weblate (Italian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/it/
2021-02-24 04:27:48 +00:00
Luka Filipović 4972d0de47 Translated using Weblate (Croatian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/hr/
2021-01-11 12:15:56 +00:00
Luka Filipović c6899d7254 Added translation using Weblate (Croatian) 2021-01-10 14:11:17 +00:00
David dccb3f3520 Translated using Weblate (German)
Currently translated at 98.3% (116 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2021-01-10 05:05:28 +00:00
Storozhenko Evgeny Vladimirovich 5febaa5837 Translated using Weblate (Russian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/ru/
2021-01-09 11:29:55 +00:00
anonymous ff8eabb514 Translated using Weblate (Russian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/ru/
2021-01-08 11:08:42 +00:00
vicdorke 509133c654 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/zh_Hans/
2020-12-22 18:55:48 +00:00
Philipp Wolfer f630b0165c Translated using Weblate (German)
Currently translated at 95.7% (113 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-11-18 10:57:41 +00:00
Daniel 0d39d1f628 Translated using Weblate (German)
Currently translated at 95.7% (113 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-11-18 10:57:41 +00:00
ghose 8eac040142 Translated using Weblate (Galician)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/gl/
2020-11-15 05:43:32 +00:00
x d46d599fc3 Translated using Weblate (Italian)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/it/
2020-11-13 18:58:07 +00:00
x a1cf0c5f5b Added translation using Weblate (Italian) 2020-11-12 16:50:21 +00:00
ghose f188b5c449 Translated using Weblate (Galician)
Currently translated at 34.7% (41 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/gl/
2020-11-09 18:15:09 +00:00
ghose 13cd81825a Added translation using Weblate (Galician) 2020-11-09 08:04:17 +00:00
serxoz 1397bdd449 Translated using Weblate (Spanish)
Currently translated at 99.1% (117 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/es/
2020-10-24 14:56:26 +00:00
Daniel 95fd3a0a6a Translated using Weblate (German)
Currently translated at 89.8% (106 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-10-09 13:12:00 +00:00
Dominik Danelski ef67fa65c0 Translated using Weblate (Polish)
Currently translated at 100.0% (118 of 118 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/pl/
2020-10-02 02:07:40 +00:00
Dominik Danelski e20e941c7e Added translation using Weblate (Polish) 2020-10-01 10:50:06 +00:00
vicdorke 6d718747db Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (113 of 113 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/zh_Hans/
2020-09-30 16:57:20 +00:00
Antoine POPINEAU a7b469d690
#15: added "Add to playlist" action from the Now Playing screen. 2020-09-26 18:34:13 +02:00
Antoine POPINEAU 6bdefa1936
#81: added save current queue to playlist. 2020-09-26 18:23:12 +02:00
Antoine POPINEAU 785fa6ce19
#84: remove presumably unnecessary trigger to Event.QueueChanged. 2020-09-26 17:39:24 +02:00
Antoine POPINEAU 300cc54e97
Remove binary AAR for FLAC and OPUS codecs and use jitpack dependencies. 2020-09-07 09:58:57 +02:00
Antoine POPINEAU 7feac4e400
Changed background color when rearranging playlist items. 2020-09-06 15:36:14 +02:00
Antoine POPINEAU b0747658ae
Add toast when added to playlist. 2020-09-06 15:23:45 +02:00
Antoine POPINEAU ab654a08c4
#15: Enable reordering of playlist tracks. Fixed an algorithmic issue with reordering of queue items. 2020-09-06 14:56:46 +02:00
Antoine POPINEAU d2a981c368
#15: add Add to playlist in queue item overflow menu. 2020-09-06 14:56:45 +02:00
Antoine POPINEAU 049822005e
#15: implemented removing track from playlist. 2020-09-06 14:56:44 +02:00
Antoine POPINEAU d796fca26b
#15: Enabled "Add to playlist" in the search screen. Localized strings and improved UI. 2020-09-06 14:56:43 +02:00
Antoine POPINEAU 54d4dc2235
#15: initial support for adding tracks to a playlist. 2020-09-06 14:56:43 +02:00
Antoine POPINEAU 64ea222f08
#76: fixed Shuffle and Clear queue actions in landscape mode. 2020-09-05 16:12:22 +02:00
Ventura Pérez García 1380d1d2b9 Translated using Weblate (Spanish)
Currently translated at 100.0% (110 of 110 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/es/
2020-09-03 23:34:13 +00:00
Antoine POPINEAU e60814d28f
#52: Properly handle content filter selection and state. 2020-09-03 22:55:10 +02:00
Antoine POPINEAU ce8d956cee
#52: implemented UI and functionnal filtering for all, me and subscribed. Funkwhale still does not seem to respect that. 2020-09-03 22:55:09 +02:00
Ventura Pérez García 1e73ef6ee4 Added translation using Weblate (Spanish) 2020-09-03 11:50:17 +00:00
Antoine POPINEAU 63c8dbe09e
#65: abide by order preference when using "Queue" overflow menu option. 2020-09-02 14:24:59 +02:00
Antoine POPINEAU 9b0c8b0bf6
#65: added an option to select preference towards playback order. 2020-09-02 12:45:37 +02:00
Antoine POPINEAU b87766dad2
#66: Fixed behavior on queue shuffling, clearing and end of queue on no-repeat. 2020-09-02 12:05:31 +02:00
Antoine POPINEAU 50c8dac297
#66: add queue actions to clear or shuffle the queue. 2020-09-01 22:16:36 +02:00
Antoine POPINEAU 1dd38e87fb
Update dependencies. 2020-09-01 21:04:41 +02:00
Antoine POPINEAU 4cf77404a1
#73: fix track sorting order by fixing a mistake and taking disc_number into consideration. 2020-09-01 18:25:10 +02:00
Antoine POPINEAU 9beb5e6641
Prepare 1.0.21. 2020-08-29 15:25:26 +02:00
Antoine POPINEAU 998dab0fb5
Do not delete downloads on authentication error (prepare for new authentication system). 2020-08-29 15:14:59 +02:00
dulz 0056faee8e Translated using Weblate (French)
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/fr/
2020-08-14 16:27:24 +00:00
Arne 964c510312 Translated using Weblate (German)
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-08-11 12:56:41 +00:00
Keunes f999745a0c Translated using Weblate (Dutch)
Currently translated at 74.7% (77 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/nl/
2020-08-09 02:07:21 +00:00
Antoine POPINEAU 002ebec7ce
Do not delete downloaded tracks on log out.
Downloaded tracks can be quite precious when you have lots of them (as
redownloading all of them can be costly in terms of time and money).
This prevents downloaded tracks from being deleted along with your
session.

This must be included in 0.21, so we can safely implement Funkwhale's
new authentication mechanism in 0.22.
2020-08-08 14:58:50 +02:00
Antoine POPINEAU d76f76a222
Use the new schema for cover art URLs. 2020-08-08 14:51:39 +02:00
Antoine POPINEAU f062e62299
Apply current track style to playlist tracks. 2020-08-03 15:30:42 +02:00
Antoine POPINEAU d2e472d770
Update media session metadata as soon as track changes. 2020-08-03 15:30:41 +02:00
vicdorke 04d0dd9c09 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/zh_Hans/
2020-07-28 03:33:18 +00:00
Arne 3dafb1c51f Translated using Weblate (German)
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 06:40:07 +00:00
anonymous cbb147fc4b Translated using Weblate (German)
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:44:06 +00:00
Arne Schlag 748ef0d935 Translated using Weblate (German)
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:44:06 +00:00
anonymous 2c672ecbfa Translated using Weblate (German)
Currently translated at 97.0% (100 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:42:34 +00:00
Arne Schlag 79140f829e Translated using Weblate (German)
Currently translated at 97.0% (100 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:42:33 +00:00
anonymous 326fcefa62 Translated using Weblate (German)
Currently translated at 96.1% (99 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:41:18 +00:00
Arne Schlag 2c657ee85a Translated using Weblate (German)
Currently translated at 96.1% (99 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:41:18 +00:00
Arne Schlag c2ac66d992 Translated using Weblate (German)
Currently translated at 86.4% (89 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:39:50 +00:00
anonymous 684e11d904 Translated using Weblate (German)
Currently translated at 86.4% (89 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/de/
2020-07-24 01:39:49 +00:00
Bread Factory 0bec180cc5 Translated using Weblate (Russian)
Currently translated at 100.0% (103 of 103 strings)

Translation: Otter/Otter Android app
Translate-URL: https://translate.funkwhale.audio/projects/otter/android/ru/
2020-07-15 05:05:52 +00:00
Bread Factory 89db2a3880 Added translation using Weblate (Russian) 2020-07-15 04:10:07 +00:00
Antoine POPINEAU a7968e9a87
Fix dynamically changing between own music and all music. Dynamically reload all tabs instead of only active ones. 2020-07-13 10:31:36 +02:00
Antoine POPINEAU 5c684b6e67
Fixed audio focus stealing. 2020-07-12 20:46:33 +02:00
Antoine POPINEAU 85e9f14e2a
Cleaned up DisableableFrameLayout. 2020-07-12 19:05:51 +02:00
Antoine POPINEAU 1e62cc1f4e
Now Playing view: do not cancel fling event on touch, disable view behind it when opened. 2020-07-12 18:55:52 +02:00
Antoine POPINEAU b0640cf1b2
Streamline the way the media session is controled across devices. 2020-07-12 18:28:50 +02:00
Antoine POPINEAU e7cb5e4c6e
QueueNavigator now returns queue index. 2020-07-12 15:19:33 +02:00
Antoine POPINEAU 7035f073f2
Changed accent color. 2020-07-11 21:08:25 +02:00
Antoine POPINEAU 931cd0b42d
Stop PlayerService when logging out. 2020-07-11 21:05:19 +02:00
Antoine POPINEAU ba31a4efcf
Some tracks do not have album, this fixes a crash trying to retrieve their album cover. Changed the placeholder album cover to be less aggressive to the eye. 2020-07-11 21:01:09 +02:00
Antoine POPINEAU 9fb9d45e05
Add favorite and info button in landscape Now Playing view. 2020-07-11 19:30:10 +02:00
Antoine POPINEAU 8d7836172b
Reorganized Now Playing view to be more legible. 2020-07-11 19:24:38 +02:00
Antoine POPINEAU 308e7d7567
Improve performance of recycler views and prevent flickering on state change. 2020-07-11 18:15:40 +02:00
Antoine POPINEAU 7d95618ff5
Allow track downloading from the search results. 2020-07-11 17:41:41 +02:00
Antoine POPINEAU e4da4af3f3
Prevent keyboard to pop over result pages. 2020-07-11 17:28:01 +02:00
Antoine POPINEAU b9e9272336
Optimized workflow between two searches. 2020-07-11 17:17:26 +02:00
Antoine POPINEAU 61fdb116ad
Fixed an issue where favorited tracks would not show up erroneously in track lists. 2020-07-11 16:56:09 +02:00
Antoine POPINEAU d75e8ae17f
Add a dedicated support email on the Play Store. Mention GitHub and Matrix in the description [skip ci]. 2020-07-11 15:11:15 +02:00
Antoine POPINEAU dd86988518
Explicitely remove notification when paused and the app is swiped up. 2020-07-11 14:16:22 +02:00
Antoine POPINEAU b6b9e4c053
Reattach the detached service notification when the app is swiped (if not playing) for it be removed with the application. 2020-07-11 12:58:25 +02:00
Antoine POPINEAU eb6b7a807b
Fixed album position in track info and ensure safe callback to current recycler view. 2020-07-10 21:03:48 +02:00
Antoine POPINEAU 3a81d26cd9
Renamed some components to refer to Otter instead of Funkwhale. 2020-07-10 20:40:18 +02:00
Antoine POPINEAU 28949a8e17
Fixed loading wheel. 2020-07-10 20:37:28 +02:00
Antoine POPINEAU bc1e911b41
Globalize the use of caching for main sections data, improved handling of loading more pages. 2020-07-10 20:28:44 +02:00
Antoine POPINEAU 57692f2e42
Added copyright and license information (#58). 2020-07-10 20:25:35 +02:00
Antoine POPINEAU fe224b097a
Re-enabled media session on service start. Do not condition radio resumption to having a cookie, since those are only valid when authenticated anonymously. 2020-07-10 18:46:49 +02:00
Antoine POPINEAU 080c07eeab
Specify in the app name when using a develop build. 2020-07-10 17:23:02 +02:00
Antoine POPINEAU b34810d631
Allow media session resuming from media buttons when service is killed. 2020-07-10 17:18:29 +02:00
Antoine POPINEAU b14b703f05
Fix an issue where always retrieving favorites from the network could be really costly and introduce stack overflows (#60). 2020-07-10 15:50:56 +02:00
Antoine POPINEAU 4ecb607f45
Let the media session live when playback is paused.
As per Android policy and internal logic, we stopped the playback
foreground service when playback was paused. This made our PlayService
elligible for garbage collection by the OS. This had the consequences of
not allowing someone to pause playback and resume it after some time.
Android would always kill the service after around one minute.

This commit, on supported Android version (7.0+) detaches the
notification when stopping the foreground service, leaving the
notification in place even when the service is killed, allowing the user
to resume playback whenever they please.

We also had to move the MediaSession out of the service, for it to
remain alive between service killing and resurrection.
2020-07-09 23:01:35 +02:00
Antoine POPINEAU a3f84cc56c
Add an application ID suffix for development versions. 2020-07-09 23:00:50 +02:00
Antoine POPINEAU 4b2cf10e78
Fix IO thread performing UI task on some occasion (#59). 2020-07-09 10:45:52 +02:00
Antoine POPINEAU 5d397ab1fe
Sort radios, playlists and favorites by name. 2020-07-08 23:23:14 +02:00
Antoine POPINEAU f3bbca9c27
Fixed caching of metadata. 2020-07-08 23:21:47 +02:00
Antoine POPINEAU 37d5c7b7be
Load data from network on resume if cached data is empty. 2020-07-08 23:00:10 +02:00
Antoine POPINEAU 97bb621d7f
Enable network security setting to allow for user-configured CAs. 2020-07-08 22:20:38 +02:00
Antoine POPINEAU b2e6ec43a8
Improved loading of new and cached items.
Scrolling through large lists was a pain. The next page would only load
when the end of the list was reached, stopping the scroll action while the new
page was fetched.

This commits adds two items:

 * Artists, albums and playlists do not refresh data on resume, only
   using cached data until manually refreshed.
 * When manually refreshed, we initially fetch a few pages instead
   of only one. Also, on scroll, we try as best as we can to always keep
   10 pages (pages as in screen estate) worth of data loaded.
2020-07-08 22:11:50 +02:00
Antoine POPINEAU de0a494b43
Do not transform URLs to HTTPS now that we support cleartext connections. 2020-07-08 15:12:49 +02:00
Antoine POPINEAU 0facf09c94
Do not list artists without albums.
As of now, Otter cannot list albumless tracks (for example, tracks in
compilation, appearing under the original artist, but not part of an
album published by this artist). This created a lot of "empty" artists
(with no albums) in the Artists section of the app.

This may be rolled back if we are some day able to list "orphan"
tracks.
2020-07-08 14:09:48 +02:00
Antoine POPINEAU 2c4f8a4329
Added margin on the left of album release date. 2020-07-08 14:08:59 +02:00
Antoine POPINEAU e17c706ae3
Release date can be null, changed model and added checks. 2020-07-08 14:08:35 +02:00
Antoine POPINEAU 37f4b1da9e
Sort artists and global albums by name/title. Sort an artist's albums by release date. Display the release year in the albums view (#54). 2020-07-08 13:19:47 +02:00
Antoine POPINEAU b0d7ff393d
Changed track metadata reporting method so it could work similarly across devices (notification, ambient display, lockscreen, watches, ...) (#55). 2020-07-08 12:46:52 +02:00
98 changed files with 2964 additions and 629 deletions

View File

@ -63,6 +63,10 @@ android {
buildTypes {
getByName("debug") {
isDebuggable = true
applicationIdSuffix = ".dev"
manifestPlaceholders = mapOf(
"app_name" to "Otter (develop)"
)
resValue("string", "debug.hostname", props.getProperty("debug.hostname", ""))
resValue("string", "debug.username", props.getProperty("debug.username", ""))
@ -70,6 +74,10 @@ android {
}
getByName("release") {
manifestPlaceholders = mapOf(
"app_name" to "Otter"
)
if (props.hasProperty("signing.store")) {
signingConfig = signingConfigs.getByName("release")
}
@ -111,26 +119,32 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7")
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.core:core-ktx:1.5.0-alpha01")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05")
implementation("androidx.appcompat:appcompat:1.2.0")
implementation("androidx.core:core-ktx:1.5.0-alpha02")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("com.google.android.material:material:1.3.0-alpha01")
implementation("com.android.support.constraint:constraint-layout:1.1.3")
implementation("com.google.android.material:material:1.3.0-alpha02")
implementation("com.android.support.constraint:constraint-layout:2.0.1")
implementation("com.google.android.exoplayer:exoplayer-core:2.11.5")
implementation("com.google.android.exoplayer:exoplayer-ui:2.11.5")
implementation("com.google.android.exoplayer:extension-mediasession:2.11.5")
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:2.11.4") {
isTransitive = false
}
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:2.11.4" ){
isTransitive = false
}
implementation("com.aliassadi:power-preference-lib:1.4.1")
implementation("com.github.kittinunf.fuel:fuel:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-android:2.1.0")
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
implementation("com.google.code.gson:gson:2.8.5")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.squareup.picasso:picasso:2.71828")
implementation("jp.wasabeef:picasso-transformations:2.2.1")
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.apognu.otter">
<uses-permission android:name="android.permission.INTERNET" />
@ -11,48 +12,70 @@
android:name="com.github.apognu.otter.Otter"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:label="${app_name}"
android:networkSecurityConfig="@xml/security"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:name="com.github.apognu.otter.activities.SplashActivity"
android:name=".activities.SplashActivity"
android:launchMode="singleInstance"
android:noHistory="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.github.apognu.otter.activities.LoginActivity"
android:name=".activities.LoginActivity"
android:configChanges="screenSize|orientation"
android:launchMode="singleInstance" />
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
<activity
android:name="com.github.apognu.otter.activities.SearchActivity"
android:launchMode="singleTop" />
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
<service android:name="com.github.apognu.otter.playback.PlayerService" />
<activity android:name=".activities.MainActivity" />
<activity
android:name=".activities.SearchActivity"
android:launchMode="singleTop" />
<activity android:name=".activities.DownloadsActivity" />
<activity android:name=".activities.SettingsActivity" />
<activity android:name=".activities.LicencesActivity" />
<service
android:name=".playback.PlayerService"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<service
android:name=".playback.PinService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
<receiver android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
</application>

View File

@ -2,6 +2,7 @@ package com.github.apognu.otter
import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import com.github.apognu.otter.playback.MediaSession
import com.github.apognu.otter.playback.QueueManager
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.database.ExoDatabaseProvider
@ -60,6 +61,8 @@ class Otter : Application() {
}
}
val mediaSession = MediaSession(this)
override fun onCreate() {
super.onCreate()
@ -84,8 +87,6 @@ class Otter : Application() {
}
cacheDir.resolve("picasso-cache").deleteRecursively()
exoDownloadManager.removeAllDownloads()
}
inner class CrashReportHandler : Thread.UncaughtExceptionHandler {

View File

@ -6,6 +6,7 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.*
@ -18,7 +19,9 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.*
import com.github.apognu.otter.playback.MediaControlsManager
@ -28,6 +31,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.Repository
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.views.DisableableFrameLayout
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
import com.google.android.exoplayer2.Player
@ -51,7 +55,8 @@ class MainActivity : AppCompatActivity() {
}
private val favoriteRepository = FavoritesRepository(this)
private val favoriteCheckRepository = FavoritedRepository(this)
private val favoritedRepository = FavoritedRepository(this)
private var menu: Menu? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -76,6 +81,18 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
(container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
if (now_playing.isOpened()) {
now_playing.close()
return@setShouldRegisterTouch false
}
true
}
favoritedRepository.update(this, lifecycleScope)
startService(Intent(this, PlayerService::class.java))
DownloadService.start(this, PinService::class.java)
@ -131,10 +148,22 @@ class MainActivity : AppCompatActivity() {
super.onBackPressed()
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
this.menu = menu
return super.onPrepareOptionsMenu(menu)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.toolbar, menu)
menu?.findItem(R.id.nav_only_my_music)?.isChecked = Settings.getScope() == "me"
menu?.findItem(R.id.nav_all_music)?.let {
it.isChecked = Settings.getScopes().contains("all")
it.isEnabled = !it.isChecked
}
menu?.findItem(R.id.nav_my_music)?.isChecked = Settings.getScopes().contains("me")
menu?.findItem(R.id.nav_followed)?.isChecked = Settings.getScopes().contains("subscribed")
return true
}
@ -155,15 +184,61 @@ class MainActivity : AppCompatActivity() {
R.id.nav_queue -> launchDialog(QueueFragment())
R.id.nav_search -> startActivity(Intent(this, SearchActivity::class.java))
R.id.nav_only_my_music -> {
item.isChecked = !item.isChecked
R.id.nav_all_music, R.id.nav_my_music, R.id.nav_followed -> {
menu?.let { menu ->
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
item.actionView = View(this)
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?) = false
override fun onMenuItemActionCollapse(item: MenuItem?) = false
})
when (item.isChecked) {
true -> PowerPreference.getDefaultFile().set("scope", "me")
false -> PowerPreference.getDefaultFile().set("scope", "all")
item.isChecked = !item.isChecked
val scopes = Settings.getScopes().toMutableSet()
val new = when (item.itemId) {
R.id.nav_my_music -> "me"
R.id.nav_followed -> "subscribed"
else -> {
menu.findItem(R.id.nav_all_music).isEnabled = false
menu.findItem(R.id.nav_my_music).isChecked = false
menu.findItem(R.id.nav_followed).isChecked = false
PowerPreference.getDefaultFile().set("scope", "all")
EventBus.send(Event.ListingsChanged)
return false
}
}
menu.findItem(R.id.nav_all_music).let {
it.isChecked = false
it.isEnabled = true
}
scopes.remove("all")
when (item.isChecked) {
true -> scopes.add(new)
false -> scopes.remove(new)
}
if (scopes.isEmpty()) {
menu.findItem(R.id.nav_all_music).let {
it.isChecked = true
it.isEnabled = false
}
scopes.add("all")
}
PowerPreference.getDefaultFile().set("scope", scopes.joinToString(","))
EventBus.send(Event.ListingsChanged)
return false
}
EventBus.send(Event.ListingsChanged)
}
R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java))
R.id.settings -> startActivityForResult(Intent(this, SettingsActivity::class.java), 0)
@ -177,8 +252,11 @@ class MainActivity : AppCompatActivity() {
if (resultCode == ResultCode.LOGOUT.code) {
Intent(this, LoginActivity::class.java).apply {
Otter.get().deleteAllData()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
stopService(Intent(this@MainActivity, PlayerService::class.java))
startActivity(this)
finish()
}
@ -212,7 +290,7 @@ class MainActivity : AppCompatActivity() {
EventBus.get().collect { message ->
when (message) {
is Event.LogOut -> {
PowerPreference.clearAllData()
Otter.get().deleteAllData()
startActivity(Intent(this@MainActivity, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
@ -286,7 +364,26 @@ class MainActivity : AppCompatActivity() {
lifecycleScope.launch(Main) {
CommandBus.get().collect { command ->
when (command) {
is Command.StartService -> {
Build.VERSION_CODES.O.onApi(
{
startForegroundService(Intent(this@MainActivity, PlayerService::class.java).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
},
{
startService(Intent(this@MainActivity, PlayerService::class.java).apply {
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
})
}
)
}
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.tracks)
}
}
}
}
@ -340,14 +437,14 @@ class MainActivity : AppCompatActivity() {
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
.fit()
.centerCrop()
.into(now_playing_cover)
now_playing_details_cover?.let { now_playing_details_cover ->
Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
.fit()
.centerCrop()
.transform(RoundedCornersTransformation(16, 0))
@ -361,7 +458,7 @@ class MainActivity : AppCompatActivity() {
}.widthPixels
val backgroundCover = Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
.get()
.run { Bitmap.createScaledBitmap(this, width, width, false).toDrawable(resources) }
.apply {
@ -392,7 +489,7 @@ class MainActivity : AppCompatActivity() {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.track_info_artist -> ArtistsFragment.openAlbums(this@MainActivity, track.artist, art = track.album.cover.original)
R.id.track_info_artist -> ArtistsFragment.openAlbums(this@MainActivity, track.artist, art = track.album?.cover())
R.id.track_info_album -> AlbumsFragment.openTracks(this@MainActivity, track.album)
R.id.track_info_details -> TrackInfoDetailsFragment.new(track).show(supportFragmentManager, "dialog")
}
@ -408,7 +505,7 @@ class MainActivity : AppCompatActivity() {
}
now_playing_details_favorite?.let { now_playing_details_favorite ->
favoriteCheckRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _ ->
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
lifecycleScope.launch(Main) {
track.favorite = favorites.contains(track.id)
@ -436,6 +533,10 @@ class MainActivity : AppCompatActivity() {
favoriteRepository.fetch(Repository.Origin.Network.origin)
}
now_playing_details_add_to_playlist.setOnClickListener {
CommandBus.send(Command.AddToPlaylist(listOf(track)))
}
}
}
}

View File

@ -3,17 +3,22 @@ package com.github.apognu.otter.activities
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.SearchAdapter
import com.github.apognu.otter.fragments.AddToPlaylistDialog
import com.github.apognu.otter.fragments.AlbumsFragment
import com.github.apognu.otter.fragments.ArtistsFragment
import com.github.apognu.otter.repositories.*
import com.github.apognu.otter.utils.Album
import com.github.apognu.otter.utils.Artist
import com.github.apognu.otter.utils.untilNetwork
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.offline.Download
import kotlinx.android.synthetic.main.activity_search.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
import java.util.*
@ -37,12 +42,35 @@ class SearchActivity : AppCompatActivity() {
results.layoutManager = LinearLayoutManager(this)
results.adapter = it
}
search.requestFocus()
}
override fun onResume() {
super.onResume()
search.requestFocus()
lifecycleScope.launch(Dispatchers.Main) {
CommandBus.get().collect { command ->
when (command) {
is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
AddToPlaylistDialog.show(this@SearchActivity, lifecycleScope, command.tracks)
}
}
}
}
lifecycleScope.launch(Dispatchers.IO) {
EventBus.get().collect { message ->
when (message) {
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
}
}
}
artistsRepository = ArtistsSearchRepository(this@SearchActivity, "")
albumsRepository = AlbumsSearchRepository(this@SearchActivity, "")
tracksRepository = TracksSearchRepository(this@SearchActivity, "")
favoritesRepository = FavoritesRepository(this@SearchActivity)
search.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(rawQuery: String?): Boolean {
@ -53,10 +81,9 @@ class SearchActivity : AppCompatActivity() {
val query = URLEncoder.encode(it, "UTF-8")
tracksRepository = TracksSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
albumsRepository = AlbumsSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
artistsRepository = ArtistsSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
favoritesRepository = FavoritesRepository(this@SearchActivity)
artistsRepository.query = query.toLowerCase(Locale.ROOT)
albumsRepository.query = query.toLowerCase(Locale.ROOT)
tracksRepository.query = query.toLowerCase(Locale.ROOT)
search_spinner.visibility = View.VISIBLE
search_empty.visibility = View.GONE
@ -67,21 +94,21 @@ class SearchActivity : AppCompatActivity() {
adapter.tracks.clear()
adapter.notifyDataSetChanged()
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _ ->
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ ->
done++
adapter.artists.addAll(artists)
refresh()
}
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _ ->
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _, _ ->
done++
adapter.albums.addAll(albums)
refresh()
}
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _ ->
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _, _ ->
done++
adapter.tracks.addAll(tracks)
@ -110,6 +137,19 @@ class SearchActivity : AppCompatActivity() {
}
}
private suspend fun refreshDownloadedTrack(download: Download) {
if (download.state == Download.STATE_COMPLETED) {
download.getMetadata()?.let { info ->
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
withContext(Dispatchers.Main) {
adapter.tracks[match.second].downloaded = true
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second))
}
}
}
}
}
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
override fun onArtistClick(holder: View?, artist: Artist) {
ArtistsFragment.openAlbums(this@SearchActivity, artist)

View File

@ -112,6 +112,14 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
}
preferenceManager.findPreference<ListPreference>("play_order")?.let {
it.summary = when (it.value) {
"shuffle" -> activity.getString(R.string.settings_play_order_shuffle_summary)
"in_order" -> activity.getString(R.string.settings_play_order_in_order_summary)
else -> activity.getString(R.string.settings_play_order_shuffle_summary)
}
}
preferenceManager.findPreference<ListPreference>("night_mode")?.let {
when (it.value) {
"on" -> {

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.github.apognu.otter.Otter
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Settings
@ -20,6 +21,8 @@ class SplashActivity : AppCompatActivity() {
}
false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
Otter.get().deleteAllData()
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
startActivity(this)

View File

@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.Album
import com.github.apognu.otter.utils.maybeLoad
import com.github.apognu.otter.utils.maybeNormalizeUrl
@ -15,11 +15,13 @@ import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_album.view.*
import kotlinx.android.synthetic.main.row_artist.view.art
class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickListener) : FunkwhaleAdapter<Album, AlbumsAdapter.ViewHolder>() {
class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickListener) : OtterAdapter<Album, AlbumsAdapter.ViewHolder>() {
interface OnAlbumClickListener {
fun onClick(view: View?, album: Album)
}
override fun getItemId(position: Int): Long = data[position].id.toLong()
override fun getItemCount() = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -34,19 +36,28 @@ class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickLis
val album = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(album.cover.original))
.maybeLoad(maybeNormalizeUrl(album.cover()))
.fit()
.transform(RoundedCornersTransformation(8, 0))
.into(holder.art)
holder.title.text = album.title
holder.artist.text = album.artist.name
holder.release_date.visibility = View.GONE
album.release_date?.split('-')?.getOrNull(0)?.let { year ->
if (year.isNotEmpty()) {
holder.release_date.visibility = View.VISIBLE
holder.release_date.text = year
}
}
}
inner class ViewHolder(view: View, private val listener: OnAlbumClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
val art = view.art
val title = view.title
val artist = view.artist
val release_date = view.release_date
override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition])

View File

@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.Album
import com.github.apognu.otter.utils.maybeLoad
import com.github.apognu.otter.utils.maybeNormalizeUrl
@ -14,11 +14,13 @@ import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_album_grid.view.*
class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClickListener) : FunkwhaleAdapter<Album, AlbumsGridAdapter.ViewHolder>() {
class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClickListener) : OtterAdapter<Album, AlbumsGridAdapter.ViewHolder>() {
interface OnAlbumClickListener {
fun onClick(view: View?, album: Album)
}
override fun getItemId(position: Int): Long = data[position].id.toLong()
override fun getItemCount() = data.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
@ -33,7 +35,7 @@ class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClic
val album = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(album.cover.original))
.maybeLoad(maybeNormalizeUrl(album.cover()))
.fit()
.placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0))

View File

@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.Artist
import com.github.apognu.otter.utils.maybeLoad
import com.github.apognu.otter.utils.maybeNormalizeUrl
@ -14,14 +14,32 @@ import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_artist.view.*
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : FunkwhaleAdapter<Artist, ArtistsAdapter.ViewHolder>() {
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() {
private var active: List<Artist> = mutableListOf()
interface OnArtistClickListener {
fun onClick(holder: View?, artist: Artist)
}
override fun getItemCount() = data.size
init {
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
active = data.filter { it.albums?.isNotEmpty() ?: false }
override fun getItemId(position: Int) = data[position].id.toLong()
super.onChanged()
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
active = data.filter { it.albums?.isNotEmpty() ?: false }
super.onItemRangeInserted(positionStart, itemCount)
}
})
}
override fun getItemCount() = active.size
override fun getItemId(position: Int) = active[position].id.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
@ -32,12 +50,12 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val artist = data[position]
val artist = active[position]
artist.albums?.let { albums ->
if (albums.isNotEmpty()) {
Picasso.get()
.maybeLoad(maybeNormalizeUrl(albums[0].cover.original))
.maybeLoad(maybeNormalizeUrl(albums[0].cover?.urls?.original))
.fit()
.transform(RoundedCornersTransformation(8, 0))
.into(holder.art)
@ -59,7 +77,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
val albums = view.albums
override fun onClick(view: View?) {
listener.onClick(view, data[layoutPosition])
listener.onClick(view, active[layoutPosition])
}
}
}

View File

@ -79,7 +79,7 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false)
Download.STATE_FAILED -> {
Track(download.id, download.title, Artist(0, download.artist, listOf()), Album(0, Album.Artist(""), "", Covers("")), 0, listOf(Track.Upload(download.contentId, 0, 0))).also {
Track.fromDownload(download).also {
PinService.download(context, it)
}
}

View File

@ -4,8 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Typeface
import android.os.Build
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -13,14 +11,14 @@ import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.*
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
import java.util.*
class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : FunkwhaleAdapter<Track, FavoritesAdapter.ViewHolder>() {
class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : OtterAdapter<Track, FavoritesAdapter.ViewHolder>() {
interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean)
}
@ -46,7 +44,7 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
val favorite = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(favorite.album.cover.original))
.maybeLoad(maybeNormalizeUrl(favorite.album?.cover()))
.fit()
.placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0))

View File

@ -3,26 +3,30 @@ package com.github.apognu.otter.adapters
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.view.*
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.*
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
import java.util.*
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : FunkwhaleAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, private val playlistListener: OnPlaylistListener? = null) : OtterAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean)
}
interface OnPlaylistListener {
fun onMoveTrack(from: Int, to: Int)
fun onRemoveTrackFromPlaylist(track: Track, index: Int)
}
private lateinit var touchHelper: ItemTouchHelper
var currentTrack: Track? = null
@ -36,10 +40,8 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
if (fromQueue) {
touchHelper = ItemTouchHelper(TouchHelperCallback()).also {
it.attachToRecyclerView(recyclerView)
}
touchHelper = ItemTouchHelper(TouchHelperCallback()).also {
it.attachToRecyclerView(recyclerView)
}
}
@ -56,7 +58,7 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
val track = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.track.album.cover.original))
.maybeLoad(maybeNormalizeUrl(track.track.album?.cover()))
.fit()
.placeholder(R.drawable.cover)
.transform(RoundedCornersTransformation(16, 0))
@ -65,19 +67,14 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
holder.title.text = track.track.title
holder.artist.text = track.track.artist.name
Build.VERSION_CODES.P.onApi(
{
holder.title.setTypeface(holder.title.typeface, Typeface.DEFAULT.weight)
holder.artist.setTypeface(holder.artist.typeface, Typeface.DEFAULT.weight)
},
{
holder.title.setTypeface(holder.title.typeface, Typeface.NORMAL)
holder.artist.setTypeface(holder.artist.typeface, Typeface.NORMAL)
})
context?.let {
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
}
if (track.track == currentTrack) {
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
if (track.track == currentTrack || track.track.current) {
context?.let {
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
}
}
context?.let {
@ -99,13 +96,16 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
holder.actions.setOnClickListener {
context?.let { context ->
PopupMenu(context, holder.actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
inflate(if (fromQueue) R.menu.row_queue else R.menu.row_track)
inflate(R.menu.row_track)
menu.findItem(R.id.track_remove_from_playlist).isVisible = true
setOnMenuItemClickListener {
when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track.track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track.track))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track.track))
R.id.track_remove_from_playlist -> playlistListener?.onRemoveTrackFromPlaylist(track.track, position)
}
true
@ -116,16 +116,14 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
}
}
if (fromQueue) {
holder.handle.visibility = View.VISIBLE
holder.handle.visibility = View.VISIBLE
holder.handle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(holder)
}
true
holder.handle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(holder)
}
true
}
}
@ -135,13 +133,12 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
Collections.swap(data, i, i + 1)
}
} else {
for (i in newPosition.downTo(oldPosition)) {
for (i in oldPosition.downTo(newPosition + 1)) {
Collections.swap(data, i, i - 1)
}
}
notifyItemMoved(oldPosition, newPosition)
CommandBus.send(Command.MoveFromQueue(oldPosition, newPosition))
}
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
@ -154,20 +151,18 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
val actions = view.actions
override fun onClick(view: View?) {
when (fromQueue) {
true -> CommandBus.send(Command.PlayTrack(layoutPosition))
false -> {
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
CommandBus.send(Command.ReplaceQueue(this.map { it.track }))
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
CommandBus.send(Command.ReplaceQueue(this.map { it.track }))
context.toast("All tracks were added to your queue")
}
}
context.toast("All tracks were added to your queue")
}
}
}
inner class TouchHelperCallback : ItemTouchHelper.Callback() {
var from = -1
var to = -1
override fun isLongPressDragEnabled() = false
override fun isItemViewSwipeEnabled() = false
@ -176,6 +171,9 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
if (from == -1) from = viewHolder.adapterPosition
to = target.adapterPosition
onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
@ -185,7 +183,9 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
viewHolder?.itemView?.background = ColorDrawable(Color.argb(255, 100, 100, 100))
context?.let {
viewHolder?.itemView?.background = ColorDrawable(context.getColor(R.color.colorSelected))
}
}
super.onSelectedChanged(viewHolder, actionState)
@ -194,6 +194,13 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
viewHolder.itemView.background = ColorDrawable(Color.TRANSPARENT)
if (from != -1 && to != -1 && from != to) {
playlistListener?.onMoveTrack(from, to)
from = -1
to = -1
}
super.clearView(recyclerView, viewHolder)
}
}

View File

@ -4,16 +4,17 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.Playlist
import com.github.apognu.otter.utils.toDurationString
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_playlist.view.*
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : FunkwhaleAdapter<Playlist, PlaylistsAdapter.ViewHolder>() {
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : OtterAdapter<Playlist, PlaylistsAdapter.ViewHolder>() {
interface OnPlaylistClickListener {
fun onClick(holder: View?, playlist: Playlist)
}
@ -36,6 +37,15 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
holder.name.text = playlist.name
holder.summary.text = context?.resources?.getQuantityString(R.plurals.playlist_description, playlist.tracks_count, playlist.tracks_count, toDurationString(playlist.duration.toLong())) ?: ""
context?.let {
ContextCompat.getDrawable(context, R.drawable.cover).let {
holder.cover_top_left.setImageDrawable(it)
holder.cover_top_right.setImageDrawable(it)
holder.cover_bottom_left.setImageDrawable(it)
holder.cover_bottom_right.setImageDrawable(it)
}
}
playlist.album_covers.shuffled().take(4).forEachIndexed { index, url ->
val imageView = when (index) {
0 -> holder.cover_top_left

View File

@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.utils.EventBus
@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : FunkwhaleAdapter<Radio, RadiosAdapter.ViewHolder>() {
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : OtterAdapter<Radio, RadiosAdapter.ViewHolder>() {
interface OnRadioClickListener {
fun onClick(holder: ViewHolder, radio: Radio)
}
@ -57,9 +57,13 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
}
}
override fun getItemCount() = instanceRadios.size + data.size + 2
override fun getItemId(position: Int) = when (getItemViewType(position)) {
RowType.InstanceRadio.ordinal -> (-position - 1).toLong()
RowType.Header.ordinal -> Long.MIN_VALUE
else -> getRadioAt(position).id.toLong()
}
override fun getItemId(position: Int) = data[position].id.toLong()
override fun getItemCount() = instanceRadios.size + data.size + 2
override fun getItemViewType(position: Int): Int {
return when {

View File

@ -214,6 +214,8 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
when (it.itemId) {
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
R.id.track_add_to_playlist -> CommandBus.send(Command.AddToPlaylist(listOf(track)))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
}
@ -228,6 +230,15 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
}
}
fun getPositionOf(type: ResultType, position: Int): Int {
return when (type) {
ResultType.Artist -> position + 1
ResultType.Album -> position + artists.size + 2
ResultType.Track -> artists.size + albums.size + SECTION_COUNT + position
else -> 0
}
}
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
val handle = view.handle
val cover = view.cover

View File

@ -4,20 +4,20 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.view.*
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.fragments.FunkwhaleAdapter
import com.github.apognu.otter.fragments.OtterAdapter
import com.github.apognu.otter.utils.*
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.row_track.view.*
import java.util.*
class TracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : FunkwhaleAdapter<Track, TracksAdapter.ViewHolder>() {
class TracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : OtterAdapter<Track, TracksAdapter.ViewHolder>() {
interface OnFavoriteListener {
fun onToggleFavorite(id: Int, state: Boolean)
}
@ -26,11 +26,9 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
var currentTrack: Track? = null
override fun getItemCount() = data.size
override fun getItemId(position: Int): Long = data[position].id.toLong()
override fun getItemId(position: Int): Long {
return data[position].id.toLong()
}
override fun getItemCount() = data.size
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
@ -55,7 +53,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
val track = data[position]
Picasso.get()
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
.fit()
.transform(RoundedCornersTransformation(8, 0))
.into(holder.cover)
@ -64,12 +62,12 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
holder.artist.text = track.artist.name
context?.let {
holder.itemView.background = context.getDrawable(R.drawable.ripple)
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
}
if (track == currentTrack || track.current) {
context?.let {
holder.itemView.background = context.getDrawable(R.drawable.current)
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
}
}
@ -117,6 +115,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
R.id.track_add_to_playlist -> CommandBus.send(Command.AddToPlaylist(listOf(track)))
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
}
@ -147,7 +146,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
Collections.swap(data, i, i + 1)
}
} else {
for (i in newPosition.downTo(oldPosition)) {
for (i in oldPosition.downTo(newPosition + 1)) {
Collections.swap(data, i, i - 1)
}
}

View File

@ -0,0 +1,105 @@
package com.github.apognu.otter.fragments
import android.app.Activity
import android.app.AlertDialog
import android.view.View
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.PlaylistsAdapter
import com.github.apognu.otter.repositories.ManagementPlaylistsRepository
import com.github.apognu.otter.utils.*
import com.google.gson.Gson
import kotlinx.android.synthetic.main.dialog_add_to_playlist.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
object AddToPlaylistDialog {
fun show(activity: Activity, lifecycleScope: CoroutineScope, tracks: List<Track>) {
val dialog = AlertDialog.Builder(activity).run {
setTitle(activity.getString(R.string.playlist_add_to))
setView(activity.layoutInflater.inflate(R.layout.dialog_add_to_playlist, null))
create()
}
dialog.show()
val repository = ManagementPlaylistsRepository(activity)
dialog.name.editText?.addTextChangedListener {
dialog.create.isEnabled = !(dialog.name.editText?.text?.trim()?.isBlank() ?: true)
}
dialog.create.setOnClickListener {
val name = dialog.name.editText?.text?.toString()?.trim() ?: ""
if (name.isEmpty()) return@setOnClickListener
lifecycleScope.launch(IO) {
repository.new(name)?.let { id ->
repository.add(id, tracks)
withContext(Main) {
Toast.makeText(activity, activity.getString(R.string.playlist_added_to, name), Toast.LENGTH_SHORT).show()
}
dialog.dismiss()
}
}
}
val adapter = PlaylistsAdapter(activity, object : PlaylistsAdapter.OnPlaylistClickListener {
override fun onClick(holder: View?, playlist: Playlist) {
repository.add(playlist.id, tracks)
Toast.makeText(activity, activity.getString(R.string.playlist_added_to, playlist.name), Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
})
dialog.playlists.layoutManager = LinearLayoutManager(activity)
dialog.playlists.adapter = adapter
repository.apply {
var first = true
fetch().untilNetwork(lifecycleScope) { data, isCache, _, hasMore ->
if (isCache) {
adapter.data = data.toMutableList()
adapter.notifyDataSetChanged()
return@untilNetwork
}
if (first) {
adapter.data.clear()
first = false
}
adapter.data.addAll(data)
lifecycleScope.launch(IO) {
try {
Cache.set(
context,
cacheId,
Gson().toJson(cache(adapter.data)).toByteArray()
)
} catch (e: ConcurrentModificationException) {
}
}
if (!hasMore) {
adapter.notifyDataSetChanged()
first = false
}
}
}
}
}

View File

@ -30,11 +30,12 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
override val viewRes = R.layout.fragment_albums
override val recycler: RecyclerView get() = albums
override val alwaysRefresh = false
lateinit var artistTracksRepository: ArtistTracksRepository
private lateinit var artistTracksRepository: ArtistTracksRepository
var artistId = 0
var artistName = ""
@ -42,7 +43,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
companion object {
fun new(artist: Artist, _art: String? = null): AlbumsFragment {
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.albums[0].cover.original else ""
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.cover() else ""
return AlbumsFragment().apply {
arguments = bundleOf(
@ -53,7 +54,11 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
}
}
fun openTracks(context: Context?, album: Album, fragment: Fragment? = null) {
fun openTracks(context: Context?, album: Album?, fragment: Fragment? = null) {
if (album == null) {
return
}
(context as? MainActivity)?.let {
fragment?.let { fragment ->
fragment.onViewPager {

View File

@ -15,10 +15,11 @@ import com.github.apognu.otter.utils.Album
import com.github.apognu.otter.utils.AppContext
import kotlinx.android.synthetic.main.fragment_albums_grid.*
class AlbumsGridFragment : FunkwhaleFragment<Album, AlbumsGridAdapter>() {
class AlbumsGridFragment : OtterFragment<Album, AlbumsGridAdapter>() {
override val viewRes = R.layout.fragment_albums_grid
override val recycler: RecyclerView get() = albums
override val layoutManager get() = GridLayoutManager(context, 3)
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -18,9 +18,10 @@ import com.github.apognu.otter.utils.Artist
import com.github.apognu.otter.utils.onViewPager
import kotlinx.android.synthetic.main.fragment_artists.*
class ArtistsFragment : FunkwhaleFragment<Artist, ArtistsAdapter>() {
class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
override val viewRes = R.layout.fragment_artists
override val recycler: RecyclerView get() = artists
override val alwaysRefresh = false
companion object {
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) {

View File

@ -16,9 +16,10 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() {
class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
override val viewRes = R.layout.fragment_favorites
override val recycler: RecyclerView get() = favorites
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -40,6 +40,20 @@ class LandscapeQueueFragment : Fragment() {
queue?.visibility = View.GONE
placeholder?.visibility = View.VISIBLE
queue_shuffle.setOnClickListener {
CommandBus.send(Command.ShuffleQueue)
}
queue_save.setOnClickListener {
adapter?.data?.let {
CommandBus.send(Command.AddToPlaylist(it))
}
}
queue_clear.setOnClickListener {
CommandBus.send(Command.ClearQueue)
}
refresh()
}

View File

@ -8,12 +8,10 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.github.apognu.otter.repositories.HttpUpstream
import com.github.apognu.otter.repositories.Repository
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.Event
import com.github.apognu.otter.utils.EventBus
import com.github.apognu.otter.utils.untilNetwork
import com.github.apognu.otter.utils.*
import com.google.gson.Gson
import kotlinx.android.synthetic.main.fragment_artists.*
import kotlinx.coroutines.Dispatchers.IO
@ -23,19 +21,30 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
abstract class FunkwhaleAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
var data: MutableList<D> = mutableListOf()
init {
super.setHasStableIds(true)
}
abstract override fun getItemId(position: Int): Long
}
abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment() {
abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
companion object {
const val OFFSCREEN_PAGES = 20
}
abstract val viewRes: Int
abstract val recycler: RecyclerView
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
open val alwaysRefresh = true
lateinit var repository: Repository<D, *>
lateinit var adapter: A
private var initialFetched = false
private var moreLoading = false
private var listener: Job? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -46,32 +55,23 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
super.onViewCreated(view, savedInstanceState)
recycler.layoutManager = layoutManager
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
recycler.adapter = adapter
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
if (recycler.computeVerticalScrollOffset() > 0 && !recycler.canScrollVertically(1)) {
val offset = recycler.computeVerticalScrollOffset()
if (!moreLoading && offset > 0 && needsMoreOffscreenPages()) {
moreLoading = true
fetch(Repository.Origin.Network.origin, adapter.data.size)
}
}
}
}
fetch(Repository.Origin.Cache.origin)
if (adapter.data.isEmpty()) {
fetch(Repository.Origin.Network.origin)
}
}
override fun onResume() {
super.onResume()
swiper?.setOnRefreshListener {
fetch(Repository.Origin.Network.origin)
}
if (listener == null) {
listener = lifecycleScope.launch(IO) {
EventBus.get().collect { event ->
@ -84,6 +84,25 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
}
}
}
fetch(Repository.Origin.Cache.origin)
if (alwaysRefresh && adapter.data.isEmpty()) {
fetch(Repository.Origin.Network.origin)
}
}
override fun onResume() {
super.onResume()
swiper?.setOnRefreshListener {
fetch(Repository.Origin.Network.origin)
}
}
fun update() {
swiper?.isRefreshing = true
fetch(Repository.Origin.Network.origin)
}
open fun onDataFetched(data: List<D>) {}
@ -91,20 +110,32 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) {
var first = size == 0
if (upstreams == Repository.Origin.Network.origin) {
swiper?.isRefreshing = true
if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
lifecycleScope.launch(Main) {
swiper?.isRefreshing = true
}
}
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, hasMore ->
moreLoading = true
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
if (isCache && data.isEmpty()) {
moreLoading = false
return@untilNetwork fetch(Repository.Origin.Network.origin)
}
lifecycleScope.launch(Main) {
if (isCache) {
moreLoading = false
adapter.data = data.toMutableList()
adapter.notifyDataSetChanged()
return@launch
}
if (first && data.isNotEmpty()) {
if (first) {
adapter.data.clear()
}
@ -112,22 +143,38 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
adapter.data.addAll(data)
if (!hasMore) {
swiper?.isRefreshing = false
withContext(IO) {
if (adapter.data.isNotEmpty()) {
try {
repository.cacheId?.let { cacheId ->
Cache.set(
context,
cacheId,
Gson().toJson(repository.cache(adapter.data)).toByteArray()
)
}
} catch (e: ConcurrentModificationException) {
}
withContext(IO) {
try {
repository.cacheId?.let { cacheId ->
Cache.set(
context,
cacheId,
Gson().toJson(repository.cache(adapter.data)).toByteArray()
)
}
} catch (e: ConcurrentModificationException) {
}
}
if (hasMore) {
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) {
if (first || needsMoreOffscreenPages()) {
fetch(Repository.Origin.Network.origin, adapter.data.size)
} else {
moreLoading = false
}
} else {
moreLoading = false
}
}
}
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
when (upstream.behavior) {
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
HttpUpstream.Behavior.Single -> if (!hasMore) swiper?.isRefreshing = false
}
}
@ -142,4 +189,15 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
}
}
}
private fun needsMoreOffscreenPages(): Boolean {
view?.let {
val offset = recycler.computeVerticalScrollOffset()
val left = recycler.computeVerticalScrollRange() - recycler.height - offset
return left < (recycler.height * OFFSCREEN_PAGES)
}
return false
}
}

View File

@ -10,7 +10,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.PlaylistTracksAdapter
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.ManagementPlaylistsRepository
import com.github.apognu.otter.repositories.PlaylistTracksRepository
import com.github.apognu.otter.repositories.Repository
import com.github.apognu.otter.utils.*
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
@ -19,11 +21,12 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAdapter>() {
class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapter>() {
override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks
lateinit var favoritesRepository: FavoritesRepository
lateinit var playlistsRepository: ManagementPlaylistsRepository
var albumId = 0
var albumArtist = ""
@ -53,9 +56,10 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
albumCover = getString("albumCover") ?: ""
}
adapter = PlaylistTracksAdapter(context, FavoriteListener())
adapter = PlaylistTracksAdapter(context, FavoriteListener(), PlaylistListener())
repository = PlaylistTracksRepository(context, albumId)
favoritesRepository = FavoritesRepository(context)
playlistsRepository = ManagementPlaylistsRepository(context)
watchEventBus()
}
@ -126,7 +130,7 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
}
override fun onDataFetched(data: List<PlaylistTrack>) {
data.map { it.track.album }.toSet().map { it.cover.original }.take(4).forEachIndexed { index, url ->
data.map { it.track.album }.toSet().map { it?.cover() }.take(4).forEachIndexed { index, url ->
val imageView = when (index) {
0 -> cover_top_left
1 -> cover_top_right
@ -181,4 +185,17 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
}
}
}
inner class PlaylistListener : PlaylistTracksAdapter.OnPlaylistListener {
override fun onMoveTrack(from: Int, to: Int) {
playlistsRepository.move(albumId, from, to)
}
override fun onRemoveTrackFromPlaylist(track: Track, index: Int) {
lifecycleScope.launch(Main) {
playlistsRepository.remove(albumId, track, index)
update()
}
}
}
}

View File

@ -14,9 +14,10 @@ import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Playlist
import kotlinx.android.synthetic.main.fragment_playlists.*
class PlaylistsFragment : FunkwhaleFragment<Playlist, PlaylistsAdapter>() {
class PlaylistsFragment : OtterFragment<Playlist, PlaylistsAdapter>() {
override val viewRes = R.layout.fragment_playlists
override val recycler: RecyclerView get() = playlists
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -62,6 +62,20 @@ class QueueFragment : BottomSheetDialogFragment() {
included.queue?.visibility = View.GONE
placeholder?.visibility = View.VISIBLE
queue_shuffle.setOnClickListener {
CommandBus.send(Command.ShuffleQueue)
}
queue_save.setOnClickListener {
adapter?.data?.let {
CommandBus.send(Command.AddToPlaylist(it))
}
}
queue_clear.setOnClickListener {
CommandBus.send(Command.ClearQueue)
}
refresh()
}

View File

@ -13,9 +13,10 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class RadiosFragment : FunkwhaleFragment<Radio, RadiosAdapter>() {
class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() {
override val viewRes = R.layout.fragment_radios
override val recycler: RecyclerView get() = radios
override val alwaysRefresh = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@ -22,8 +22,10 @@ class TrackInfoDetailsFragment : DialogFragment() {
return TrackInfoDetailsFragment().apply {
arguments = bundleOf(
"artistName" to track.artist.name,
"albumTitle" to track.album.title,
"albumTitle" to track.album?.title,
"trackTitle" to track.title,
"trackCopyright" to track.copyright,
"trackLicense" to track.license,
"trackPosition" to track.position,
"trackDuration" to track.bestUpload()?.duration?.toLong()?.let { toDurationString(it, showSeconds = true) },
"trackBitrate" to track.bestUpload()?.bitrate?.let { "${it / 1000} Kbps" },
@ -48,8 +50,10 @@ class TrackInfoDetailsFragment : DialogFragment() {
properties.add(Pair(R.string.track_info_details_artist, getString("artistName")))
properties.add(Pair(R.string.track_info_details_album, getString("albumTitle")))
properties.add(Pair(R.string.track_info_details_track_title, getString("trackTitle")))
properties.add(Pair(R.string.track_info_details_track_copyright, getString("trackCopyright")))
properties.add(Pair(R.string.track_info_details_track_license, getString("trackLicense")))
properties.add(Pair(R.string.track_info_details_track_duration, getString("trackDuration")))
properties.add(Pair(R.string.track_info_details_track_position, getString("trackPosition")))
properties.add(Pair(R.string.track_info_details_track_position, getInt("trackPosition").toString()))
properties.add(Pair(R.string.track_info_details_track_bitrate, getString("trackBitrate")))
properties.add(Pair(R.string.track_info_details_track_instance, getString("trackInstance")))
}

View File

@ -3,16 +3,19 @@ package com.github.apognu.otter.fragments
import android.os.Bundle
import android.view.Gravity
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.github.apognu.otter.R
import com.github.apognu.otter.adapters.TracksAdapter
import com.github.apognu.otter.repositories.FavoritedRepository
import com.github.apognu.otter.repositories.FavoritesRepository
import com.github.apognu.otter.repositories.TracksRepository
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.offline.Download
import com.preference.PowerPreference
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
import kotlinx.android.synthetic.main.fragment_tracks.*
@ -22,11 +25,12 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
class TracksFragment : OtterFragment<Track, TracksAdapter>() {
override val viewRes = R.layout.fragment_tracks
override val recycler: RecyclerView get() = tracks
lateinit var favoritesRepository: FavoritesRepository
lateinit var favoritedRepository: FavoritedRepository
private var albumId = 0
private var albumArtist = ""
@ -40,7 +44,7 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
"albumId" to album.id,
"albumArtist" to album.artist.name,
"albumTitle" to album.title,
"albumCover" to album.cover.original
"albumCover" to album.cover()
)
}
}
@ -59,6 +63,7 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
adapter = TracksAdapter(context, FavoriteListener())
repository = TracksRepository(context, albumId)
favoritesRepository = FavoritesRepository(context)
favoritedRepository = FavoritedRepository(context)
watchEventBus()
}
@ -104,8 +109,16 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
}
}
when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> play.text = getString(R.string.playback_play)
else -> play.text = getString(R.string.playback_shuffle)
}
play.setOnClickListener {
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data))
else -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
}
context.toast("All tracks were added to your queue")
}
@ -115,10 +128,25 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
PopupMenu(context, actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
inflate(R.menu.album)
menu.findItem(R.id.play_secondary)?.let { item ->
when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> item.title = getString(R.string.playback_shuffle)
else -> item.title = getString(R.string.playback_play)
}
}
setOnMenuItemClickListener {
when (it.itemId) {
R.id.play_secondary -> when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
else -> CommandBus.send(Command.ReplaceQueue(adapter.data))
}
R.id.add_to_queue -> {
CommandBus.send(Command.AddToQueue(adapter.data))
when (PowerPreference.getDefaultFile().getString("play_order")) {
"in_order" -> CommandBus.send(Command.AddToQueue(adapter.data))
else -> CommandBus.send(Command.AddToQueue(adapter.data.shuffled()))
}
context.toast("All tracks were added to your queue")
}
@ -185,6 +213,7 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
adapter.currentTrack = track.apply {
current = true
}
adapter.notifyDataSetChanged()
}
}

View File

@ -3,18 +3,19 @@ package com.github.apognu.otter.playback
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.media.MediaMetadata
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media.app.NotificationCompat.MediaStyle
import androidx.media.session.MediaButtonReceiver
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R
import com.github.apognu.otter.activities.MainActivity
import com.github.apognu.otter.utils.*
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Track
import com.github.apognu.otter.utils.maybeNormalizeUrl
import com.squareup.picasso.Picasso
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.Default
@ -23,9 +24,6 @@ import kotlinx.coroutines.launch
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
companion object {
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
const val NOTIFICATION_ACTION_PREVIOUS = 1
const val NOTIFICATION_ACTION_TOGGLE = 2
const val NOTIFICATION_ACTION_NEXT = 3
}
private var notification: Notification? = null
@ -43,18 +41,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
val openIntent = Intent(context, MainActivity::class.java).apply { action = NOTIFICATION_ACTION_OPEN_QUEUE.toString() }
val openPendingIntent = PendingIntent.getActivity(context, 0, openIntent, 0)
val coverUrl = maybeNormalizeUrl(track.album.cover.original)
val cover = coverUrl?.run { Picasso.get().load(coverUrl) }
mediaSession.setMetadata(MediaMetadataCompat.Builder().apply {
putString(MediaMetadata.METADATA_KEY_ARTIST, track.artist.name)
putString(MediaMetadata.METADATA_KEY_TITLE, track.title)
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
cover?.let {
try { putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, it.get()) } catch (_: Exception) {}
}
}.build())
val coverUrl = maybeNormalizeUrl(track.album?.cover())
notification = NotificationCompat.Builder(
context,
@ -69,11 +56,16 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
)
.setSmallIcon(R.drawable.ottershape)
.run {
if (cover != null) {
try { setLargeIcon(cover.get()) } catch (_: Exception) {}
coverUrl?.let {
try {
setLargeIcon(Picasso.get().load(coverUrl).get())
} catch (_: Exception) {
}
this
} else this
return@run this
}
this
}
.setContentTitle(track.title)
.setContentText(track.artist.name)
@ -82,58 +74,43 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
.addAction(
action(
R.drawable.previous, context.getString(R.string.control_previous),
NOTIFICATION_ACTION_PREVIOUS
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
.addAction(
action(
stateIcon, context.getString(R.string.control_toggle),
NOTIFICATION_ACTION_TOGGLE
PlaybackStateCompat.ACTION_PLAY_PAUSE
)
)
.addAction(
action(
R.drawable.next, context.getString(R.string.control_next),
NOTIFICATION_ACTION_NEXT
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
.build()
notification?.let {
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
if (playing) {
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
} else {
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
}
}
if (playing) tick()
Otter.get().mediaSession.connector.invalidateMediaSessionMetadata()
}
}
}
fun tick() {
notification?.let {
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
}
fun remove() {
NotificationManagerCompat.from(context).cancel(AppContext.NOTIFICATION_MEDIA_CONTROL)
}
private fun action(icon: Int, title: String, id: Int): NotificationCompat.Action {
val intent = Intent(context, MediaControlActionReceiver::class.java).apply { action = id.toString() }
val pendingIntent = PendingIntent.getBroadcast(context, id, intent, 0)
return NotificationCompat.Action.Builder(icon, title, pendingIntent).build()
}
}
class MediaControlActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
MediaControlsManager.NOTIFICATION_ACTION_PREVIOUS.toString() -> CommandBus.send(
Command.PreviousTrack
)
MediaControlsManager.NOTIFICATION_ACTION_TOGGLE.toString() -> CommandBus.send(
Command.ToggleState
)
MediaControlsManager.NOTIFICATION_ACTION_NEXT.toString() -> CommandBus.send(
Command.NextTrack
)
private fun action(icon: Int, title: String, id: Long): NotificationCompat.Action {
return MediaButtonReceiver.buildMediaButtonPendingIntent(context, id).run {
NotificationCompat.Action.Builder(icon, title, this).build()
}
}
}

View File

@ -0,0 +1,88 @@
package com.github.apognu.otter.playback
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.github.apognu.otter.utils.Command
import com.github.apognu.otter.utils.CommandBus
import com.google.android.exoplayer2.ControlDispatcher
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
class MediaSession(private val context: Context) {
var active = false
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
setActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_SEEK_TO or
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
)
}
val session: MediaSessionCompat by lazy {
MediaSessionCompat(context, context.packageName).apply {
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
setPlaybackState(playbackStateBuilder.build())
isActive = true
active = true
}
}
val connector: MediaSessionConnector by lazy {
MediaSessionConnector(session).also {
it.setQueueNavigator(OtterQueueNavigator())
it.setMediaButtonEventHandler { _, _, intent ->
if (!active) {
context.startService(Intent(context, PlayerService::class.java).apply {
action = intent.action
intent.extras?.let { extras -> putExtras(extras) }
})
return@setMediaButtonEventHandler true
}
false
}
}
}
}
class OtterQueueNavigator : MediaSessionConnector.QueueNavigator {
override fun onSkipToQueueItem(player: Player, controlDispatcher: ControlDispatcher, id: Long) {
CommandBus.send(Command.PlayTrack(id.toInt()))
}
override fun onCurrentWindowIndexChanged(player: Player) {}
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
override fun getSupportedQueueNavigatorActions(player: Player): Long {
return PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
}
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) {
CommandBus.send(Command.NextTrack)
}
override fun getActiveQueueItemId(player: Player?) = player?.currentWindowIndex?.toLong() ?: 0
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) {
CommandBus.send(Command.PreviousTrack)
}
override fun onTimelineChanged(player: Player) {}
}

View File

@ -8,28 +8,33 @@ import android.content.IntentFilter
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.MediaMetadata
import android.os.Build
import android.os.IBinder
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.MediaMetadataCompat
import android.view.KeyEvent
import androidx.core.app.NotificationManagerCompat
import androidx.media.session.MediaButtonReceiver
import com.github.apognu.otter.Otter
import com.github.apognu.otter.R
import com.github.apognu.otter.utils.*
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import kotlinx.coroutines.CoroutineScope
import com.squareup.picasso.Picasso
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
class PlayerService : Service() {
companion object {
const val INITIAL_COMMAND_KEY = "start_command"
}
private var started = false
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
@ -40,9 +45,10 @@ class PlayerService : Service() {
private lateinit var queue: QueueManager
private lateinit var mediaControlsManager: MediaControlsManager
private lateinit var mediaSession: MediaSessionCompat
private lateinit var player: SimpleExoPlayer
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
private lateinit var playerEventListener: PlayerEventListener
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
@ -51,13 +57,30 @@ class PlayerService : Service() {
private lateinit var radioPlayer: RadioPlayer
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!started) watchEventBus()
intent?.action?.let {
if (it == Intent.ACTION_MEDIA_BUTTON) {
intent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (hasAudioFocus(true)) MediaButtonReceiver.handleIntent(Otter.get().mediaSession.session, intent)
Unit
}
else -> MediaButtonReceiver.handleIntent(Otter.get().mediaSession.session, intent)
}
}
}
}
if (!started) {
watchEventBus()
}
started = true
return START_STICKY
}
@SuppressLint("NewApi")
override fun onCreate() {
super.onCreate()
@ -66,7 +89,7 @@ class PlayerService : Service() {
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Build.VERSION_CODES.O.onApi {
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
setAudioAttributes(AudioAttributes.Builder().run {
setUsage(AudioAttributes.USAGE_MEDIA)
@ -82,11 +105,7 @@ class PlayerService : Service() {
}
}
mediaSession = MediaSessionCompat(this, applicationContext.packageName).apply {
isActive = true
}
mediaControlsManager = MediaControlsManager(this, scope, mediaSession)
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
player = SimpleExoPlayer.Builder(this).build().apply {
playWhenReady = false
@ -94,33 +113,25 @@ class PlayerService : Service() {
playerEventListener = PlayerEventListener().also {
addListener(it)
}
}
MediaSessionConnector(mediaSession).also {
it.setPlayer(this)
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
mediaButtonEvent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
if (key.action == KeyEvent.ACTION_UP) {
when (key.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY -> state(true)
KeyEvent.KEYCODE_MEDIA_PAUSE -> state(false)
KeyEvent.KEYCODE_MEDIA_NEXT -> player.next()
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> previousTrack()
}
}
}
Otter.get().mediaSession.active = true
true
}
Otter.get().mediaSession.connector.apply {
setPlayer(player)
setMediaMetadataProvider {
buildTrackMetadata(queue.current())
}
}
if (queue.current > -1) {
player.prepare(queue.datasources, true, true)
player.prepare(queue.datasources)
Cache.get(this, "progress")?.let { progress ->
player.seekTo(queue.current, progress.readLine().toLong())
val (current, duration, percent) = progress(true)
val (current, duration, percent) = getProgress(true)
ProgressBus.send(current, duration, percent)
}
@ -134,8 +145,6 @@ class PlayerService : Service() {
CommandBus.get().collect { command ->
when (command) {
is Command.RefreshService -> {
EventBus.send(Event.QueueChanged)
if (queue.metadata.isNotEmpty()) {
CommandBus.send(Command.RefreshTrack(queue.current()))
EventBus.send(Event.StateChanged(player.playWhenReady))
@ -148,7 +157,7 @@ class PlayerService : Service() {
queue.replace(command.queue)
player.prepare(queue.datasources, true, true)
state(true)
setPlaybackState(true)
CommandBus.send(Command.RefreshTrack(queue.current()))
}
@ -162,24 +171,23 @@ class PlayerService : Service() {
queue.current = command.index
player.seekTo(command.index, C.TIME_UNSET)
state(true)
setPlaybackState(true)
CommandBus.send(Command.RefreshTrack(queue.current()))
}
is Command.ToggleState -> toggle()
is Command.SetState -> state(command.state)
is Command.ToggleState -> togglePlayback()
is Command.SetState -> setPlaybackState(command.state)
is Command.NextTrack -> {
player.next()
is Command.NextTrack -> skipToNextTrack()
is Command.PreviousTrack -> skipToPreviousTrack()
is Command.Seek -> seek(command.progress)
Cache.set(this@PlayerService, "progress", "0".toByteArray())
ProgressBus.send(0, 0, 0)
is Command.ClearQueue -> {
queue.clear()
player.stop()
}
is Command.PreviousTrack -> previousTrack()
is Command.Seek -> progress(command.progress)
is Command.ClearQueue -> queue.clear()
is Command.ShuffleQueue -> queue.shuffle()
is Command.PlayRadio -> {
queue.clear()
@ -191,10 +199,6 @@ class PlayerService : Service() {
is Command.PinTrack -> PinService.download(this@PlayerService, command.track)
is Command.PinTracks -> command.tracks.forEach { PinService.download(this@PlayerService, it) }
}
if (player.playWhenReady) {
mediaControlsManager.tick()
}
}
}
@ -212,7 +216,7 @@ class PlayerService : Service() {
while (true) {
delay(1000)
val (current, duration, percent) = progress()
val (current, duration, percent) = getProgress()
if (player.playWhenReady) {
ProgressBus.send(current, duration, percent)
@ -223,8 +227,19 @@ class PlayerService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if (!player.playWhenReady) {
NotificationManagerCompat.from(this).cancelAll()
stopSelf()
}
}
@SuppressLint("NewApi")
override fun onDestroy() {
scope.cancel()
try {
unregisterReceiver(headphonesUnpluggedReceiver)
} catch (_: Exception) {
@ -241,23 +256,18 @@ class PlayerService : Service() {
audioManager.abandonAudioFocus(audioFocusChangeListener)
})
mediaSession.isActive = false
mediaSession.release()
player.removeListener(playerEventListener)
state(false)
setPlaybackState(false)
player.release()
stopForeground(true)
stopSelf()
Otter.get().mediaSession.active = false
super.onDestroy()
}
@SuppressLint("NewApi")
private fun state(state: Boolean) {
private fun setPlaybackState(state: Boolean) {
if (!state) {
val (progress, _, _) = progress()
val (progress, _, _) = getProgress()
Cache.set(this@PlayerService, "progress", progress.toString().toByteArray())
}
@ -266,6 +276,76 @@ class PlayerService : Service() {
player.prepare(queue.datasources)
}
if (hasAudioFocus(state)) {
player.playWhenReady = state
EventBus.send(Event.StateChanged(state))
}
}
private fun togglePlayback() {
setPlaybackState(!player.playWhenReady)
}
private fun skipToPreviousTrack() {
if (player.currentPosition > 5000) {
return player.seekTo(0)
}
player.previous()
}
private fun skipToNextTrack() {
player.next()
Cache.set(this@PlayerService, "progress", "0".toByteArray())
ProgressBus.send(0, 0, 0)
}
private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
if (!player.playWhenReady && !force) return progressCache
return queue.current()?.bestUpload()?.let { upload ->
val current = player.currentPosition
val duration = upload.duration.toFloat()
val percent = ((current / (duration * 1000)) * 100).toInt()
progressCache = Triple(current.toInt(), duration.toInt(), percent)
progressCache
} ?: Triple(0, 0, 0)
}
private fun seek(value: Int) {
val duration = ((queue.current()?.bestUpload()?.duration ?: 0) * (value.toFloat() / 100)) * 1000
progressCache = Triple(duration.toInt(), queue.current()?.bestUpload()?.duration ?: 0, value)
player.seekTo(duration.toLong())
}
private fun buildTrackMetadata(track: Track?): MediaMetadataCompat {
track?.let {
val coverUrl = maybeNormalizeUrl(track.album?.cover())
return mediaMetadataBuilder.apply {
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist.name)
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
try {
runBlocking(IO) {
this@apply.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, Picasso.get().load(coverUrl).get())
}
} catch (e: Exception) {
}
}.build()
}
return mediaMetadataBuilder.build()
}
@SuppressLint("NewApi")
private fun hasAudioFocus(state: Boolean): Boolean {
var allowed = !state
if (!allowed) {
@ -291,46 +371,10 @@ class PlayerService : Service() {
)
}
if (allowed) {
player.playWhenReady = state
EventBus.send(Event.StateChanged(state))
}
}
private fun toggle() {
state(!player.playWhenReady)
}
private fun previousTrack() {
if (player.currentPosition > 5000) {
return player.seekTo(0)
}
player.previous()
}
private fun progress(force: Boolean = false): Triple<Int, Int, Int> {
if (!player.playWhenReady && !force) return progressCache
return queue.current()?.bestUpload()?.let { upload ->
val current = player.currentPosition
val duration = upload.duration.toFloat()
val percent = ((current / (duration * 1000)) * 100).toInt()
progressCache = Triple(current.toInt(), duration.toInt(), percent)
progressCache
} ?: Triple(0, 0, 0)
}
private fun progress(value: Int) {
val duration = ((queue.current()?.bestUpload()?.duration ?: 0) * (value.toFloat() / 100)) * 1000
progressCache = Triple(duration.toInt(), queue.current()?.bestUpload()?.duration ?: 0, value)
player.seekTo(duration.toLong())
return allowed
}
@SuppressLint("NewApi")
inner class PlayerEventListener : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
super.onPlayerStateChanged(playWhenReady, playbackState)
@ -346,7 +390,20 @@ class PlayerService : Service() {
when (playbackState) {
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true))
Player.STATE_ENDED -> EventBus.send(Event.PlaybackStopped)
Player.STATE_ENDED -> {
setPlaybackState(false)
queue.current = 0
player.seekTo(0, C.TIME_UNSET)
ProgressBus.send(0, 0, 0)
}
Player.STATE_IDLE -> {
setPlaybackState(false)
return EventBus.send(Event.PlaybackStopped)
}
}
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
@ -355,9 +412,14 @@ class PlayerService : Service() {
false -> {
EventBus.send(Event.Buffering(false))
if (playbackState == Player.STATE_READY) {
mediaControlsManager.updateNotification(queue.current(), false)
stopForeground(false)
Build.VERSION_CODES.N.onApi(
{ stopForeground(STOP_FOREGROUND_DETACH) },
{ stopForeground(false) }
)
when (playbackState) {
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), false)
Player.STATE_IDLE -> mediaControlsManager.remove()
}
}
}
@ -366,8 +428,10 @@ class PlayerService : Service() {
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
super.onTracksChanged(trackGroups, trackSelections)
queue.current = player.currentWindowIndex
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
if (queue.current != player.currentWindowIndex) {
queue.current = player.currentWindowIndex
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
}
if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
scope.launch(IO) {
@ -410,18 +474,18 @@ class PlayerService : Service() {
AudioManager.AUDIOFOCUS_GAIN -> {
player.volume = 1f
state(stateWhenLostFocus)
setPlaybackState(stateWhenLostFocus)
stateWhenLostFocus = false
}
AudioManager.AUDIOFOCUS_LOSS -> {
stateWhenLostFocus = false
state(false)
setPlaybackState(false)
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
stateWhenLostFocus = player.playWhenReady
state(false)
setPlaybackState(false)
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
@ -431,4 +495,4 @@ class PlayerService : Service() {
}
}
}
}
}

View File

@ -178,5 +178,34 @@ class QueueManager(val context: Context) {
metadata = mutableListOf()
datasources.clear()
current = -1
persist()
}
fun shuffle() {
if (metadata.size < 2) return
if (current == -1) {
replace(metadata.shuffled())
} else {
move(current, 0)
current = 0
val shuffled =
metadata
.drop(1)
.shuffled()
while (metadata.size > 1) {
datasources.removeMediaSource(metadata.size - 1)
metadata.removeAt(metadata.size - 1)
}
append(shuffled)
}
persist()
EventBus.send(Event.QueueChanged)
}
}

View File

@ -38,11 +38,11 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
Cache.get(context, "radio_type")?.readLine()?.let { radio_type ->
Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
Cache.get(context, "radio_cookie")?.readLine()?.let { radio_cookie ->
currentRadio = Radio(radio_id, radio_type, "", "")
session = radio_session
cookie = radio_cookie
}
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
currentRadio = Radio(radio_id, radio_type, "", "")
session = radio_session
cookie = cachedCookie
}
}
}
@ -121,7 +121,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
.authorize()
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
val favorites = favoritedRepository.fetch(Repository.Origin.Network.origin)
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
.map { it.data }
.toList()
.flatten()

View File

@ -4,7 +4,7 @@ import android.content.Context
import com.github.apognu.otter.utils.Album
import com.github.apognu.otter.utils.AlbumsCache
import com.github.apognu.otter.utils.AlbumsResponse
import com.github.apognu.otter.utils.FunkwhaleResponse
import com.github.apognu.otter.utils.OtterResponse
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import java.io.BufferedReader
@ -17,10 +17,10 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
override val upstream: Upstream<Album> by lazy {
val url =
if (artistId == null) "/api/v1/albums/?playable=true"
else "/api/v1/albums/?playable=true&artist=$artistId"
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
HttpUpstream<Album, FunkwhaleResponse<Album>>(
HttpUpstream<Album, OtterResponse<Album>>(
HttpUpstream.Behavior.Progressive,
url,
object : TypeToken<AlbumsResponse>() {}.type

View File

@ -1,7 +1,7 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.FunkwhaleResponse
import com.github.apognu.otter.utils.OtterResponse
import com.github.apognu.otter.utils.Track
import com.github.apognu.otter.utils.TracksCache
import com.github.apognu.otter.utils.TracksResponse
@ -11,7 +11,7 @@ import java.io.BufferedReader
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
override val cacheId = "tracks-artist-$artistId"
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)

View File

@ -4,14 +4,14 @@ import android.content.Context
import com.github.apognu.otter.utils.Artist
import com.github.apognu.otter.utils.ArtistsCache
import com.github.apognu.otter.utils.ArtistsResponse
import com.github.apognu.otter.utils.FunkwhaleResponse
import com.github.apognu.otter.utils.OtterResponse
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.reflect.TypeToken
import java.io.BufferedReader
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
override val cacheId = "artists"
override val upstream = HttpUpstream<Artist, FunkwhaleResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true", object : TypeToken<ArtistsResponse>() {}.type)
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
override fun cache(data: List<Artist>) = ArtistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)

View File

@ -8,6 +8,7 @@ import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -15,11 +16,13 @@ import java.io.BufferedReader
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
override val cacheId = "favorites.v2"
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true", object : TypeToken<TracksResponse>() {}.type)
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
private val favoritedRepository = FavoritedRepository(context)
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
@ -51,6 +54,8 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
favoritedRepository.update(context, scope)
}
}
@ -68,14 +73,22 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
favoritedRepository.update(context, scope)
}
}
}
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
override val cacheId = "favorited"
override val upstream = HttpUpstream<Int, FunkwhaleResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
override fun cache(data: List<Int>) = FavoritedCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
fun update(context: Context?, scope: CoroutineScope) {
fetch(Origin.Network.origin).untilNetwork(scope, IO) { favorites, _, _, _ ->
Cache.set(context, cacheId, Gson().toJson(cache(favorites)).toByteArray())
}
}
}

View File

@ -18,7 +18,7 @@ import java.io.Reader
import java.lang.reflect.Type
import kotlin.math.ceil
class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
enum class Behavior {
Single, AtOnce, Progressive
}
@ -33,7 +33,7 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
.buildUpon()
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
.appendQueryParameter("page", page.toString())
.appendQueryParameter("scope", Settings.getScope())
.appendQueryParameter("scope", Settings.getScopes().joinToString(","))
.build()
.toString()
@ -41,24 +41,27 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
{ response ->
val data = response.getData()
if (behavior == Behavior.Progressive || response.next == null) {
emit(Repository.Response(Repository.Origin.Network, data, false))
} else {
emit(Repository.Response(Repository.Origin.Network, data, true))
when (behavior) {
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
fetch(size + data.size).collect { emit(it) }
else -> {
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
if (response.next != null) fetch(size + data.size).collect { emit(it) }
}
}
},
{ error ->
when (error.exception) {
is RefreshError -> EventBus.send(Event.LogOut)
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), false))
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
}
}
)
}.flowOn(IO)
class GenericDeserializer<T : FunkwhaleResponse<*>>(val type: Type) : ResponseDeserializable<T> {
class GenericDeserializer<T : OtterResponse<*>>(val type: Type) : ResponseDeserializable<T> {
override fun deserialize(reader: Reader): T? {
return Gson().fromJson(reader, type)
}

View File

@ -1,7 +1,7 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.FunkwhaleResponse
import com.github.apognu.otter.utils.OtterResponse
import com.github.apognu.otter.utils.PlaylistTrack
import com.github.apognu.otter.utils.PlaylistTracksCache
import com.github.apognu.otter.utils.PlaylistTracksResponse
@ -14,7 +14,7 @@ import java.io.BufferedReader
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
override val cacheId = "tracks-playlist-$playlistId"
override val upstream = HttpUpstream<PlaylistTrack, FunkwhaleResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)

View File

@ -1,18 +1,99 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.FunkwhaleResponse
import com.github.apognu.otter.utils.Playlist
import com.github.apognu.otter.utils.PlaylistsCache
import com.github.apognu.otter.utils.PlaylistsResponse
import com.github.apognu.otter.utils.*
import com.github.kittinunf.fuel.Fuel
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.BufferedReader
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
override val cacheId = "tracks-playlists"
override val upstream = HttpUpstream<Playlist, FunkwhaleResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true", object : TypeToken<PlaylistsResponse>() {}.type)
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
}
class ManagementPlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
override val cacheId = "tracks-playlists-management"
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.AtOnce, "/api/v1/playlists/?scope=me&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
suspend fun new(name: String): Int? {
val body = mapOf("name" to name, "privacy_level" to "me")
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
val (_, response, result) = request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitObjectResponseResult(gsonDeserializerOf(Playlist::class.java))
if (response.statusCode != 201) return null
return result.get().id
}
fun add(id: Int, tracks: List<Track>) {
val body = PlaylistAdd(tracks.map { it.id }, false)
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/add/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
scope.launch(Dispatchers.IO) {
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
}
}
suspend fun remove(id: Int, track: Track, index: Int) {
val body = mapOf("index" to index)
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/remove/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
}
fun move(id: Int, from: Int, to: Int) {
val body = mapOf("from" to from, "to" to to)
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/move/")).apply {
if (!Settings.isAnonymous()) {
header("Authorization", "Bearer ${Settings.getAccessToken()}")
}
}
scope.launch(Dispatchers.IO) {
request
.header("Content-Type", "application/json")
.body(Gson().toJson(body))
.awaitByteArrayResponseResult()
}
}
}

View File

@ -1,7 +1,7 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.FunkwhaleResponse
import com.github.apognu.otter.utils.OtterResponse
import com.github.apognu.otter.utils.Radio
import com.github.apognu.otter.utils.RadiosCache
import com.github.apognu.otter.utils.RadiosResponse
@ -11,7 +11,7 @@ import java.io.BufferedReader
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
override val cacheId = "radios"
override val upstream = HttpUpstream<Radio, FunkwhaleResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/", object : TypeToken<RadiosResponse>() {}.type)
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
override fun cache(data: List<Radio>) = RadiosCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)

View File

@ -1,6 +1,7 @@
package com.github.apognu.otter.repositories
import android.content.Context
import com.github.apognu.otter.utils.AppContext
import com.github.apognu.otter.utils.Cache
import com.github.apognu.otter.utils.CacheItem
import kotlinx.coroutines.CoroutineScope
@ -8,6 +9,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import java.io.BufferedReader
import kotlin.math.ceil
interface Upstream<D> {
fun fetch(size: Int = 0): Flow<Repository.Response<D>>
@ -21,7 +23,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
Network(0b10)
}
data class Response<D>(val origin: Origin, val data: List<D>, val hasMore: Boolean)
data class Response<D>(val origin: Origin, val data: List<D>, val page: Int, val hasMore: Boolean)
abstract val context: Context?
abstract val cacheId: String?
@ -39,17 +41,19 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
cacheId?.let { cacheId ->
Cache.get(context, cacheId)?.let { reader ->
uncache(reader)?.let { cache ->
emit(Response(Origin.Cache, cache.data, false))
return@flow emit(Response(Origin.Cache, cache.data, ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(), false))
}
}
return@flow emit(Response(Origin.Cache, listOf(), 1, false))
}
}.flowOn(IO)
private fun fromNetwork(size: Int) = flow {
upstream
.fetch(size)
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.hasMore) }
.collect { response -> emit(Response(Origin.Network, response.data, response.hasMore)) }
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
}
protected open fun onDataFetched(data: List<D>) = data

View File

@ -10,15 +10,16 @@ import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import java.io.BufferedReader
class TracksSearchRepository(override val context: Context?, query: String) : Repository<Track, TracksCache>() {
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<Track, TracksCache>() {
override val cacheId: String? = null
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
override val upstream: Upstream<Track>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
val favorites = FavoritedRepository(context).fetch(Origin.Network.origin)
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
.map { it.data }
.toList()
.flatten()
@ -40,17 +41,19 @@ class TracksSearchRepository(override val context: Context?, query: String) : Re
}
}
class ArtistsSearchRepository(override val context: Context?, query: String) : Repository<Artist, ArtistsCache>() {
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<Artist, ArtistsCache>() {
override val cacheId: String? = null
override val upstream = HttpUpstream<Artist, FunkwhaleResponse<Artist>>(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
override val upstream: Upstream<Artist>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
override fun cache(data: List<Artist>) = ArtistsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
}
class AlbumsSearchRepository(override val context: Context?, query: String) : Repository<Album, AlbumsCache>() {
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<Album, AlbumsCache>() {
override val cacheId: String? = null
override val upstream = HttpUpstream<Album, FunkwhaleResponse<Album>>(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
override val upstream: Upstream<Album>
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
override fun cache(data: List<Album>) = AlbumsCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)

View File

@ -13,7 +13,7 @@ import java.io.BufferedReader
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
override val cacheId = "tracks-album-$albumId"
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId", object : TypeToken<TracksResponse>() {}.type)
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
override fun cache(data: List<Track>) = TracksCache(data)
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
@ -38,7 +38,7 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
}
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
val favorites = FavoritedRepository(context).fetch(Origin.Network.origin)
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
.map { it.data }
.toList()
.flatten()
@ -56,6 +56,6 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
}
track
}.sortedBy { it.position }
}.sortedWith(compareBy({ it.disc_number }, { it.position }))
}
}

View File

@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.launch
sealed class Command {
class StartService(val command: Command) : Command()
object RefreshService : Command()
object ToggleState : Command()
@ -21,11 +22,13 @@ sealed class Command {
class Seek(val progress: Int) : Command()
class AddToQueue(val tracks: List<Track>) : Command()
class AddToPlaylist(val tracks: List<Track>) : Command()
class PlayNext(val track: Track) : Command()
class ReplaceQueue(val queue: List<Track>, val fromRadio: Boolean = false) : Command()
class RemoveFromQueue(val track: Track) : Command()
class MoveFromQueue(val oldPosition: Int, val newPosition: Int) : Command()
object ClearQueue : Command()
object ShuffleQueue : Command()
class PlayRadio(val radio: Radio) : Command()
class SetRepeatMode(val mode: Int) : Command()

View File

@ -17,10 +17,10 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, hasMore: Boolean) -> Unit) {
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, page: Int, hasMore: Boolean) -> Unit) {
scope.launch(context) {
collect { data ->
callback(data.data, data.origin == Repository.Origin.Cache, data.hasMore)
callback(data.data, data.origin == Repository.Origin.Cache, data.page, data.hasMore)
}
}
}

View File

@ -17,46 +17,47 @@ class RadiosCache(data: List<Radio>) : CacheItem<Radio>(data)
class FavoritedCache(data: List<Int>) : CacheItem<Int>(data)
class QueueCache(data: List<Track>) : CacheItem<Track>(data)
abstract class FunkwhaleResponse<D : Any> {
abstract class OtterResponse<D : Any> {
abstract val count: Int
abstract val next: String?
abstract fun getData(): List<D>
}
data class UserResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() {
data class UserResponse(override val count: Int, override val next: String?, val results: List<Artist>) : OtterResponse<Artist>() {
override fun getData() = results
}
data class ArtistsResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() {
data class ArtistsResponse(override val count: Int, override val next: String?, val results: List<Artist>) : OtterResponse<Artist>() {
override fun getData() = results
}
data class AlbumsResponse(override val count: Int, override val next: String?, val results: AlbumList) : FunkwhaleResponse<Album>() {
data class AlbumsResponse(override val count: Int, override val next: String?, val results: AlbumList) : OtterResponse<Album>() {
override fun getData() = results
}
data class TracksResponse(override val count: Int, override val next: String?, val results: List<Track>) : FunkwhaleResponse<Track>() {
data class TracksResponse(override val count: Int, override val next: String?, val results: List<Track>) : OtterResponse<Track>() {
override fun getData() = results
}
data class FavoritedResponse(override val count: Int, override val next: String?, val results: List<Favorited>) : FunkwhaleResponse<Int>() {
data class FavoritedResponse(override val count: Int, override val next: String?, val results: List<Favorited>) : OtterResponse<Int>() {
override fun getData() = results.map { it.track }
}
data class PlaylistsResponse(override val count: Int, override val next: String?, val results: List<Playlist>) : FunkwhaleResponse<Playlist>() {
data class PlaylistsResponse(override val count: Int, override val next: String?, val results: List<Playlist>) : OtterResponse<Playlist>() {
override fun getData() = results
}
data class PlaylistTracksResponse(override val count: Int, override val next: String?, val results: List<PlaylistTrack>) : FunkwhaleResponse<PlaylistTrack>() {
data class PlaylistTracksResponse(override val count: Int, override val next: String?, val results: List<PlaylistTrack>) : OtterResponse<PlaylistTrack>() {
override fun getData() = results
}
data class RadiosResponse(override val count: Int, override val next: String?, val results: List<Radio>) : FunkwhaleResponse<Radio>() {
data class RadiosResponse(override val count: Int, override val next: String?, val results: List<Radio>) : OtterResponse<Radio>() {
override fun getData() = results
}
data class Covers(val original: String)
data class Covers(val urls: CoverUrls)
data class CoverUrls(val original: String)
typealias AlbumList = List<Album>
@ -70,11 +71,12 @@ data class Album(
val id: Int,
val artist: Artist,
val title: String,
val cover: Covers
val cover: Covers?,
val release_date: String?
) : SearchResult {
data class Artist(val name: String)
override fun cover() = cover.original
override fun cover() = cover?.urls?.original
override fun title() = title
override fun subtitle() = artist.name
}
@ -86,27 +88,40 @@ data class Artist(
) : SearchResult {
data class Album(
val title: String,
val cover: Covers
val cover: Covers?
)
override fun cover() = albums?.getOrNull(0)?.cover?.original
override fun cover(): String? = albums?.getOrNull(0)?.cover?.urls?.original
override fun title() = name
override fun subtitle() = "Artist"
}
data class Track(
val id: Int,
val id: Int = 0,
val title: String,
val artist: Artist,
val album: Album,
val position: Int,
val uploads: List<Upload>
val album: Album?,
val disc_number: Int = 0,
val position: Int = 0,
val uploads: List<Upload> = listOf(),
val copyright: String? = null,
val license: String? = null
) : SearchResult {
var current: Boolean = false
var favorite: Boolean = false
var cached: Boolean = false
var downloaded: Boolean = false
companion object {
fun fromDownload(download: DownloadInfo): Track = Track(
id = download.id,
title = download.title,
artist = Artist(0, download.artist, listOf()),
album = Album(0, Album.Artist(""), "", Covers(CoverUrls("")), ""),
uploads = listOf(Upload(download.contentId, 0, 0))
)
}
data class Upload(
val listen_url: String,
val duration: Int,
@ -130,7 +145,7 @@ data class Track(
}
}
override fun cover() = album.cover.original
override fun cover() = album?.cover?.urls?.original
override fun title() = title
override fun subtitle() = artist.name
}

View File

@ -5,7 +5,6 @@ import android.widget.Toast
import com.google.android.exoplayer2.util.Log
import com.preference.PowerPreference
import java.net.URI
import java.net.URL
fun Context?.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
if (this != null) {
@ -13,12 +12,29 @@ fun Context?.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
}
}
fun Any.log(message: String) {
Log.d("FUNKWHALE", "${this.javaClass.simpleName}: $message")
private fun logClassName(): String {
val known = setOf(
"dalvik.system.VMStack",
"java.lang.Thread",
"com.github.apognu.otter.utils.UtilKt"
)
Thread.currentThread().stackTrace.forEach {
if (!known.contains(it.className)) {
val className = it.className.split('.').last()
val line = it.lineNumber
return "$className:$line"
}
}
return "UNKNOWN"
}
fun Any.log() {
Log.d("FUNKWHALE", this.toString())
fun Any?.log(prefix: String? = null) {
prefix?.let {
Log.d("OTTER", "${logClassName()} - $prefix: $this")
} ?: Log.d("OTTER", "${logClassName()} - $this")
}
fun maybeNormalizeUrl(rawUrl: String?): String? {
@ -35,9 +51,7 @@ fun mustNormalizeUrl(rawUrl: String): String {
val fallbackHost = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
val uri = URI(rawUrl).takeIf { it.host != null } ?: URI("$fallbackHost$rawUrl")
return uri.toURL().run {
URL("https", host, file)
}.toString()
return uri.toString()
}
fun toDurationString(duration: Long, showSeconds: Boolean = false): String {
@ -61,5 +75,6 @@ object Settings {
fun getAccessToken(): String = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token", "")
fun isAnonymous() = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getBoolean("anonymous", false)
fun areExperimentsEnabled() = PowerPreference.getDefaultFile().getBoolean("experiments", false)
fun getScope() = PowerPreference.getDefaultFile().getString("scope", "all")
fun getScopes() = PowerPreference.getDefaultFile().getString("scope", "all").split(",")
}

View File

@ -0,0 +1,26 @@
package com.github.apognu.otter.views
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
class DisableableFrameLayout : FrameLayout {
var callback: ((MotionEvent?) -> Boolean)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style)
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
callback?.let {
return !it(event)
}
return false
}
fun setShouldRegisterTouch(callback: (event: MotionEvent?) -> Boolean) {
this.callback = callback
}
}

View File

@ -72,7 +72,7 @@ class NowPlayingView : MaterialCardView {
}
inner class OnGestureDetection : GestureDetector.SimpleOnGestureListener() {
var maxHeight = 0
private var maxHeight = 0
private var minHeight = 0
private var maxMargin = 0
@ -100,8 +100,6 @@ class NowPlayingView : MaterialCardView {
initialTouchY = e.rawY
lastTouchY = e.rawY
flingAnimator?.cancel()
return true
}

View File

@ -1 +1 @@
apognu@gmail.com
otter@support.popineau.eu

View File

@ -0,0 +1 @@
https://github.com/apognu/otter

View File

@ -2,5 +2,8 @@ Otter is a simple music player that allows you to stream the audio content of yo
This app requires an account on a Funkwhale instance to work.
You can get support or take a part in Otter's development by visiting our GitHub project or join us on Matrix.
Source code : https://github.com/apognu/otter
Matrix room: https://matrix.to/#/#otter:matrix.org
Funkwhale : https://funkwhale.audio

View File

@ -1,6 +1,8 @@
Otter est un lecteur de musique basique qui vous permet de profiter du contenu audio de votre instance Funkwhale.
Cette application nécessite un compte sur un instance Funkwhale pour fonctionner.
Cette application nécessite un compte sur une instance Funkwhale pour fonctionner.
Vous pouvez obtenir de l'aide ou participer au développement d'Otter en vous rendant sur notre projet GitHub ou nous rejoindre sur Matrix.
Code source : https://github.com/apognu/otter
Funkwhale : https://funkwhale.audio

View File

@ -1 +1 @@
../../../../../../fastlane/metadata/android/en-US/changelogs/1000020.txt
../../../../../../fastlane/metadata/android/en-US/changelogs/1000021.txt

View File

@ -1 +1 @@
../../../../../../fastlane/metadata/android/fr-FR/changelogs/1000020.txt
../../../../../../fastlane/metadata/android/fr-FR/changelogs/1000021.txt

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/elevatedSurface" />
<padding android:top="5dp" android:left="5dp" android:right="5dp" android:bottom="5dp" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="3dp" />
<solid android:color="@color/ripple" />
<padding android:top="4dp" android:left="8dp" android:right="8dp" android:bottom="4dp" />
</shape>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M4,10h12v2L4,12zM4,6h12v2L4,8zM4,14h8v2L4,16zM14,14v6l5,-3z"/>
</vector>

View File

@ -5,6 +5,7 @@
android:id="@+id/now_playing_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/elevatedSurface"
android:orientation="vertical">
<LinearLayout
@ -97,24 +98,67 @@
android:id="@+id/now_playing_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:orientation="vertical"
android:paddingTop="16dp">
android:paddingStart="32dp"
android:paddingTop="16dp"
android:paddingEnd="32dp">
<TextView
android:id="@+id/now_playing_details_title"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
android:textSize="18sp"
tools:text="Supermassive Black Hole" />
android:orientation="horizontal">
<TextView
android:id="@+id/now_playing_details_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/now_playing_details_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
android:textSize="18sp"
tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/now_playing_details_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
</LinearLayout>
<ImageButton
android:id="@+id/now_playing_details_add_to_playlist"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/add_to_playlist" />
<ImageButton
android:id="@+id/now_playing_details_favorite"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/favorite" />
<ImageButton
android:id="@+id/now_playing_details_info"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_track_info"
android:src="@drawable/more"
android:tint="@color/controlForeground" />
</LinearLayout>
<SeekBar
android:id="@+id/now_playing_details_progress"

View File

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
<com.github.apognu.otter.views.DisableableFrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/name"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:hint="@string/playlist_add_to_new"
app:boxStrokeColor="@color/controlForeground"
app:hintTextColor="@color/controlForeground"
app:placeholderTextColor="@color/controlForeground">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/create"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginHorizontal="16dp"
android:enabled="false"
android:text="@string/playlist_add_to_create"
android:textColor="@color/controlForeground"
app:rippleColor="@color/ripple" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/playlists"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:itemCount="10"
tools:listitem="@layout/row_playlist" />
</LinearLayout>

View File

@ -5,6 +5,7 @@
android:id="@+id/now_playing_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/elevatedSurface"
android:orientation="vertical">
<LinearLayout
@ -126,18 +127,10 @@
android:layout_height="32dp"
android:layout_gravity="top|end"
android:layout_margin="8dp"
android:background="@drawable/circle"
android:contentDescription="@string/alt_track_info"
android:src="@drawable/more" />
<ImageButton
android:id="@+id/now_playing_details_favorite"
style="@style/IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/favorite" />
android:src="@drawable/more"
android:tint="@color/controlForeground" />
</FrameLayout>
@ -150,19 +143,52 @@
android:orientation="vertical"
android:paddingTop="16dp">
<TextView
android:id="@+id/now_playing_details_title"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
android:textSize="18sp"
tools:text="Supermassive Black Hole" />
android:orientation="horizontal">
<TextView
android:id="@+id/now_playing_details_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/now_playing_details_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/itemTitle"
android:textSize="18sp"
tools:text="Supermassive Black Hole" />
<TextView
android:id="@+id/now_playing_details_artist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Muse" />
</LinearLayout>
<ImageButton
android:id="@+id/now_playing_details_add_to_playlist"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/playlist_add_to"
android:src="@drawable/add_to_playlist" />
<ImageButton
android:id="@+id/now_playing_details_favorite"
style="@style/IconButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:contentDescription="@string/alt_album_cover"
android:src="@drawable/favorite" />
</LinearLayout>
<SeekBar
android:id="@+id/now_playing_details_progress"
@ -204,8 +230,8 @@
<ImageButton
android:id="@+id/now_playing_details_previous"
style="@style/IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/control_previous"
android:src="@drawable/previous" />
@ -222,8 +248,8 @@
<ImageButton
android:id="@+id/now_playing_details_next"
style="@style/IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/control_next"
android:src="@drawable/next" />

View File

@ -1,8 +1,60 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface"
android:layout_height="wrap_content">
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="8dp"
android:paddingVertical="4dp">
<Button
android:id="@+id/queue_save"
style="@style/Widget.MaterialComponents.Button.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:backgroundTint="@color/colorPrimary"
android:layout_weight="1"
android:text="@string/playback_queue_save"
android:textColor="@android:color/white"
android:textSize="12sp"
app:icon="@drawable/playlist"
app:iconTint="@android:color/white"
app:rippleColor="@color/ripple" />
<Button
android:id="@+id/queue_shuffle"
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:text="@string/playback_shuffle"
android:textColor="@color/controlForeground"
android:textSize="12sp"
app:icon="@drawable/shuffle"
app:iconTint="@color/controlForeground"
app:rippleColor="@color/ripple" />
<Button
android:id="@+id/queue_clear"
style="@style/AppTheme.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_weight="0"
android:textSize="12sp"
app:icon="@drawable/delete"
app:iconTint="@color/controlForeground"
app:rippleColor="@color/ripple" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queue"
@ -26,4 +78,4 @@
android:visibility="gone"
tools:visibility="visible" />
</FrameLayout>
</LinearLayout>

View File

@ -22,6 +22,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
@ -44,4 +45,12 @@
</LinearLayout>
<TextView
android:id="@+id/release_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="0"
android:background="@drawable/pill" />
</LinearLayout>

View File

@ -8,11 +8,29 @@
android:title="@string/toolbar_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/nav_only_my_music"
android:checkable="true"
android:title="@string/only_my_music"
app:showAsAction="never" />
<item android:title="@string/filters">
<menu>
<group android:checkableBehavior="all">
<item
android:id="@+id/nav_all_music"
android:checkable="true"
android:title="@string/fiters_all"
app:showAsAction="never" />
<item
android:id="@+id/nav_my_music"
android:checkable="true"
android:title="@string/filters_my_music"
app:showAsAction="never" />
<item
android:id="@+id/nav_followed"
android:checkable="true"
android:title="@string/filters_followed"
app:showAsAction="never" />
</group>
</menu>
</item>
<item
android:id="@+id/nav_downloads"

View File

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/play_secondary"
android:title="@string/playback_shuffle" />
<item
android:id="@+id/add_to_queue"
android:title="@string/playback_queue" />

View File

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/track_add_to_playlist"
android:title="@string/playlist_add_to" />
<item
android:id="@+id/queue_remove"
android:title="@string/playback_queue_remove_item" />

View File

@ -9,8 +9,17 @@
android:id="@+id/track_play_next"
android:title="@string/playback_queue_play_next" />
<item
android:id="@+id/track_add_to_playlist"
android:title="@string/playlist_add_to" />
<item
android:id="@+id/track_pin"
android:title="@string/playback_queue_download" />
<item
android:id="@+id/track_remove_from_playlist"
android:visible="false"
android:title="@string/playback_queue_remove_item" />
</menu>

View File

@ -14,11 +14,29 @@
android:title="@string/toolbar_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/nav_only_my_music"
android:checkable="true"
android:title="@string/only_my_music"
app:showAsAction="never" />
<item android:title="@string/filters">
<menu>
<group android:checkableBehavior="all">
<item
android:id="@+id/nav_all_music"
android:checkable="true"
android:title="@string/fiters_all"
app:showAsAction="never" />
<item
android:id="@+id/nav_my_music"
android:checkable="true"
android:title="@string/filters_my_music"
app:showAsAction="never" />
<item
android:id="@+id/nav_followed"
android:checkable="true"
android:title="@string/filters_followed"
app:showAsAction="never" />
</group>
</menu>
</item>
<item
android:id="@+id/nav_downloads"

View File

@ -9,7 +9,7 @@
<string name="login_submit">Anmelden</string>
<string name="login_logging_in">Einloggen</string>
<string name="login_error_hostname">Dies konnte nicht als eine URL verstanden werden</string>
<string name="login_error_hostname_https">Der Zugriff auf die Funkwhale Server sollte über https erfolgen</string>
<string name="login_error_hostname_https">Der Zugriff auf den Funkwhale Server sollte über https erfolgen</string>
<string name="toolbar_search">Suche</string>
<string name="title_settings">Einstellungen</string>
<string name="title_oss_licences">Open Source Lizenz</string>
@ -20,16 +20,16 @@
<string name="settings_media_quality">Medienqualität</string>
<string name="settings_media_quality_quality">Beste Qualität</string>
<string name="settings_media_quality_size">Kleine Dateigröße</string>
<string name="settings_media_quality_summary_quality">Songs mit größerer Dateigröße werden verwendet</string>
<string name="settings_media_quality_summary_quality">Versionen mit größerer Dateigröße werden verwendet</string>
<string name="settings_media_quality_summary_size">Songs mit kleinerer Dateigröße werden verwendet</string>
<string name="settings_media_cache_size">Zwischenspeichergröße</string>
<string name="settings_media_cache_size_summary">%d GB werden für Songs verwendet, welche Offline abspielbar sind</string>
<string name="settings_other">Andere</string>
<string name="settings_night_mode">Nachtmodus</string>
<string name="settings_night_mode">Dunkler Modus</string>
<string name="settings_night_mode_on">Immer eingeschaltet (Dunkelmodus)</string>
<string name="settings_night_mode_on_summary">Nachtmodus wird bevorzugt</string>
<string name="settings_night_mode_on_summary">Dunkler Modus ist immer eingeschaltet</string>
<string name="settings_night_mode_off">Immer ausgeschaltet (Heller Modus)</string>
<string name="settings_night_mode_off_summary">Heller Modus wird bevorzugt</string>
<string name="settings_night_mode_off_summary">Heller Modus ist immer eingeschaltet</string>
<string name="settings_night_mode_system">Folge den Systemeinstellungen</string>
<string name="settings_night_mode_system_summary">Der Nachtmodus wird den Systemeinstellungen folgen</string>
<string name="settings_experiments">Experimentielle Funktionen aktivieren</string>
@ -49,14 +49,14 @@
<string name="playlists">Playlisten</string>
<string name="radios">Sender</string>
<string name="favorites">Favoriten</string>
<string name="playback_media_controls">Mediansteuerung</string>
<string name="playback_media_controls">Mediensteuerung</string>
<string name="playback_media_controls_description">Medienwiedergabe steuern</string>
<string name="playback_shuffle">Zufallswiedergabe</string>
<string name="playback_queue">Warteschlange</string>
<string name="playback_queue_empty">Deine Warteschlange ist leer</string>
<string name="playback_queue_remove_item">Entfernen</string>
<string name="playback_queue_add_item">Zur Warteschlange hinzufügen</string>
<string name="playback_queue_play_next">Als nächstes spielen</string>
<string name="playback_queue_play_next">Als nächstes abspielen</string>
<string name="manage_add_to_favorites">Als Favorit hinzufügen</string>
<string name="control_toggle">Abspielen / Pause</string>
<string name="control_previous">Vorheriger Song</string>
@ -84,13 +84,48 @@
<string name="track_info_details_track_instance">Funkwhale Instanz</string>
<string name="radio_playback_error">Ein Fehler ist beim Abspielen des Radios aufgetreten</string>
<string name="radio_random_title">Zufällig</string>
<string name="radio_random_description">Zufällig ausgewählte Songs, vielleicht entdeckt du neue Musik?</string>
<string name="radio_random_description">Zufällig ausgewählte Songs, vielleicht entdeckst du neue Musik\?</string>
<string name="radio_less_listened_title">Weniger angehört</string>
<string name="radio_less_listened_description">Höre Songs, welche du nicht oft hörst. Perfekt, um ein Gleichgewicht herzustellen.</string>
<string name="radio_less_listened_description">Höre Songs, welche du nicht oft anhörst. Perfekt, um ein Gleichgewicht wieder herzustellen.</string>
<string name="logout_title">Abmelden</string>
<string name="logout_content">Bist du dir sicher, dass du dich aus deiner Funkwhale Instanz abmelden möchtest?</string>
<string name="logout_content">Bist du dir sicher, dass du dich von dieser Funkwhale Instanz abmelden möchtest\?</string>
<plurals name="playlist_description">
<item quantity="one">%1$d Song • %2$s</item>
<item quantity="other">%1$d Songs • %2$s</item>
</plurals>
<string name="title_downloads">Downloads</string>
<string name="login_error_userinfo">Wir konnten keine Informationen zu deinem Nutzer finden</string>
<string name="login_cleartext">Erlaube unverschlüsselte Verbindungen (HTTP)</string>
<string name="radio_your_content_description">Auswahl aus deiner eigenen Bibliothek</string>
<string name="radio_your_content_title">Dein Inhalt</string>
<string name="radio_user_radios">Nutzersender</string>
<string name="radio_instance_radios">Instance Sender</string>
<string name="track_info_details_track_license">Lizenz</string>
<string name="track_info_details_track_copyright">Urheberrecht</string>
<string name="playback_queue_download">Herunterladen</string>
<string name="settings_crash_report_copied">Der letzte Absturzbericht wurde in deine Zwischenablage kopiert</string>
<string name="settings_crash_report_description">Nur Otters Protokolle der letzten 5 Minuten vor dem Crash werden werden gespeichert</string>
<string name="settings_crash_report_title">Kopiere Absturzberichte</string>
<string name="radio_favorites_description">Spielen Sie Ihre Lieblingsmusik in einer nicht endenen Glücksschleife.</string>
<string name="only_my_music">Nur meine Musik</string>
<plurals name="downloads_description">
<item quantity="one">Lade %1$d Lied herunter</item>
<item quantity="other">Lade %1$d Lieder herunter</item>
</plurals>
<string name="settings_play_order_in_order">Alben in reihenfolgen spielen</string>
<string name="playback_queue_save">Speichern</string>
<string name="playlist_add_to_new">Neue Playlist…</string>
<string name="playlist_add_to_create">Playlist erstellen</string>
<string name="filters_my_music">Meine Musik</string>
<string name="settings_play_order">Bevorzugte Wiedergabereihenfolge</string>
<string name="filters">Filter</string>
<string name="playlist_added_to">Zur Wiedergabeliste %s hinzugefügt</string>
<string name="playlist_add_to">Zur Wiedergabeliste hinzufügen</string>
<string name="playback_queue_clear">Leeren</string>
<string name="playback_play">Abspielen</string>
<string name="fiters_all">Alle Musikinhalte</string>
<string name="filters_followed">Gefolgte Inhalte</string>
<string name="settings_play_order_shuffle">Alben zufällig wiedergeben</string>
<string name="settings_play_order_shuffle_summary">Alben werden in zufälliger Reihenfolge abgespielt</string>
<string name="settings_play_order_in_order_summary">Alben der Reihe nach abspielen</string>
</resources>

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="downloads_description">
<item quantity="one">Descargando %1$d pista…</item>
<item quantity="other">Descargando %1$d pistas…</item>
</plurals>
<string name="only_my_music">Sólo mi música</string>
<plurals name="playlist_description">
<item quantity="one">%1$d pista • %2$s</item>
<item quantity="other">%1$d pistas • %2$s</item>
</plurals>
<string name="logout_content">¿Cerrar sesión de este servidor de Funkwhale\?</string>
<string name="logout_title">Cerrar sesión</string>
<string name="radio_less_listened_description">Escucha canciones que no sueles escuchar.</string>
<string name="radio_less_listened_title">Menos reproducidas</string>
<string name="radio_random_description">Canciones completamente aleatorias. ¿Tal vez descubras algo nuevo\?</string>
<string name="radio_random_title">Aleatorio</string>
<string name="radio_favorites_description">Reproduce tus favoritos en un ciclo sin fin de felicidad.</string>
<string name="radio_your_content_description">Selección de tus librerías</string>
<string name="radio_your_content_title">Tu contenido</string>
<string name="radio_user_radios">Radios del usuario</string>
<string name="radio_instance_radios">Radios del servidor</string>
<string name="radio_playback_error">No se puede reproducir esta emisora de radio</string>
<string name="track_info_details_track_instance">Instancia de Funkwhale</string>
<string name="track_info_details_track_bitrate">Ratio de bits</string>
<string name="track_info_details_track_position">Posición en el álbum</string>
<string name="track_info_details_track_duration">Duración</string>
<string name="track_info_details_track_copyright">Copyright</string>
<string name="track_info_details_track_license">Licencia</string>
<string name="track_info_details_track_title">Título</string>
<string name="track_info_details_album">Álbum</string>
<string name="track_info_details_artist">Artista</string>
<string name="track_info_details_title">Detalles de la pista</string>
<string name="track_info_details">Información</string>
<string name="track_info_album">Ir al álbum</string>
<string name="track_info_artist">Ir al artista</string>
<string name="alt_track_info">Información de la pista</string>
<string name="alt_more_options">Más opciones</string>
<string name="alt_album_cover">Portada del álbum</string>
<string name="alt_artist_art">Imagen del artista</string>
<string name="alt_app_logo">Logo de la aplicación</string>
<plurals name="album_count">
<item quantity="one">%d álbum</item>
<item quantity="other">%d álbumes</item>
</plurals>
<string name="error_playback">No se puede reproducir esta pista</string>
<string name="control_next">Siguiente pista</string>
<string name="control_previous">Pista anterior</string>
<string name="control_toggle">Alternar reproducción</string>
<string name="manage_add_to_favorites">Añadir a favoritos</string>
<string name="playback_queue_clear">Borrar cola</string>
<string name="playback_queue_download">Descargar</string>
<string name="playback_queue_play_next">Reproducir justo después</string>
<string name="playback_queue_add_item">Añadir a la cola</string>
<string name="playback_queue_remove_item">Borrar</string>
<string name="playback_queue_empty">Tu cola está vacía</string>
<string name="playback_queue">Cola</string>
<string name="playback_shuffle">Mezclar</string>
<string name="playback_play">Reproducir</string>
<string name="playback_media_controls_description">Control de reproducción del sonido</string>
<string name="playback_media_controls">Control de reproducción</string>
<string name="favorites">Favoritos</string>
<string name="radios">Radios</string>
<string name="playlists">Listas de reproducción</string>
<string name="tracks">Pistas</string>
<string name="albums">Álbumes</string>
<string name="artists">Artistas</string>
<string name="settings_logout">Cerrar sesión</string>
<string name="settings_crash_report_copied">Informe del cierre inesperado copiado en el portapapeles</string>
<string name="settings_crash_report_description">Sólo se recogerán los registros de Otter desde los 5 minutos antes del cierre inesperado</string>
<string name="settings_crash_report_title">Copiar registros de error</string>
<string name="settings_information_license_description">licencia MIT</string>
<string name="settings_information_license_title">Licencia</string>
<string name="settings_version_title">Versión</string>
<string name="settings_information_repository_description">Otter por Antoine POPINEAU (apognu)</string>
<string name="settings_information_repository_title">Repositorio</string>
<string name="settings_information">Información</string>
<string name="settings_experiments_restart_content">Cierre y reinicie la aplicación para utilizar la nueva configuración</string>
<string name="settings_experiments_restart_title">Es necesario reiniciar la aplicación</string>
<string name="settings_experiments_description">Utilízalo bajo tu propio riesgo. Puede congelar o bloquear la aplicación.</string>
<string name="settings_experiments">Características experimentales</string>
<string name="settings_night_mode_system_summary">El modo nocturno seguirá el sistema</string>
<string name="settings_night_mode_system">Seguir el sistema</string>
<string name="settings_night_mode_off_summary">El modo claro siempre estará activado</string>
<string name="settings_night_mode_off">Siempre desactivado (modo claro)</string>
<string name="settings_night_mode_on_summary">El modo oscuro siempre estará activado</string>
<string name="settings_night_mode_on">Siempre activado (modo oscuro)</string>
<string name="settings_night_mode">Modo oscuro</string>
<string name="settings_other">Otros</string>
<string name="settings_play_order_in_order_summary">Prefieres reproducir los álbumes en orden</string>
<string name="settings_play_order_in_order">Reproducir álbumes en orden</string>
<string name="settings_play_order_shuffle_summary">Prefieres mezclar las canciones de un álbum</string>
<string name="settings_play_order_shuffle">Mezclar álbumes</string>
<string name="settings_play_order">Orden de reproducción preferido</string>
<string name="settings_media_cache_size_summary">Se usarán %d GB para guardar las canciones para reproduccir sin conexión</string>
<string name="settings_media_cache_size">Tamaño de la caché para los medios</string>
<string name="settings_media_quality_summary_size">La versión más pequeña será reproducida</string>
<string name="settings_media_quality_summary_quality">La mejor versión disponible será reproducida</string>
<string name="settings_media_quality_size">Tamaño más pequeño</string>
<string name="settings_media_quality_quality">Mejor calidad</string>
<string name="settings_media_quality">Calidad del sonido</string>
<string name="settings_general">General</string>
<string name="search_no_results">No se han encontrado resultados</string>
<string name="search_welcome">Introduzca su búsqueda arriba y pulse enter para buscar en su colección</string>
<string name="search_placeholder">Buscar artistas, álbumes y pistas</string>
<string name="title_oss_licences">Licencias de código abierto</string>
<string name="title_settings">Ajustes</string>
<string name="title_downloads">Descargas</string>
<string name="toolbar_search">Buscar</string>
<string name="login_error_userinfo">No se ha podido recuperar la información del usuario</string>
<string name="login_error_hostname_https">El servidor de Funkwhale debería estar asegurado con HTTPS</string>
<string name="login_error_hostname">Introduzca primero una URL válida</string>
<string name="login_logging_in">Iniciando sesión…</string>
<string name="login_submit">Iniciar sesión</string>
<string name="login_password">Contraseña</string>
<string name="login_username">Nombre de usuario</string>
<string name="login_anonymous">Autenticación anónima</string>
<string name="login_cleartext">Permitir tráfico sin cifrar (HTTP)</string>
<string name="login_hostname">Servidor</string>
<string name="login_welcome">Por favor, introduzca los datos de una instancia de Funkwhale para acceder a su contenido</string>
<string name="filters_followed">Contenido seguido</string>
<string name="filters_my_music">Mi música</string>
<string name="fiters_all">Toda la música</string>
<string name="filters">Filtros</string>
<string name="playlist_added_to">Añadida a la lista de reproducción “%s”</string>
<string name="playlist_add_to_create">Crear lista de reproducción</string>
<string name="playlist_add_to_new">Nueva lista de reproducción…</string>
<string name="playlist_add_to">Añadir a la lista de reproducción</string>
<string name="playback_queue_save">Guardar</string>
</resources>

View File

@ -8,7 +8,7 @@
<string name="login_password">Mot de passe</string>
<string name="login_submit">Se connecter</string>
<string name="login_logging_in">Connexion</string>
<string name="login_error_hostname">Cela ne semble pas être un nom d\hôte valide</string>
<string name="login_error_hostname">Cela ne semble pas être un nom d\'hôte valide</string>
<string name="login_error_hostname_https">Le nom d\'hôte Funkwhale devrait être sécurisé à travers HTTPS</string>
<string name="login_error_userinfo">Nous n\'avons pas pu récupérer les informations à propos de votre utilisateur</string>
<string name="toolbar_search">Rechercher</string>
@ -26,6 +26,11 @@
<string name="settings_media_quality_summary_size">Les pistes les plus légères seront utilisées</string>
<string name="settings_media_cache_size">Taille du cache</string>
<string name="settings_media_cache_size_summary">%d Go seront utilisés pour mettre en cache les pistes pour la lecture hors-ligne</string>
<string name="settings_play_order">Ordre de lecture préféré</string>
<string name="settings_play_order_shuffle">Lecture aléatoire</string>
<string name="settings_play_order_shuffle_summary">Vous préférez écouter les albums aléatoirement</string>
<string name="settings_play_order_in_order">Lecture dans l\'ordre</string>
<string name="settings_play_order_in_order_summary">Vous préférez écouter les albums dans l\'ordre</string>
<string name="settings_other">Autres</string>
<string name="settings_night_mode">Mode nuit</string>
<string name="settings_night_mode_on">Toujours activé (mode sombre)</string>
@ -54,6 +59,7 @@
<string name="playlists">Playlists</string>
<string name="radios">Radios</string>
<string name="favorites">Favoris</string>
<string name="playback_play">Jouer</string>
<string name="playback_media_controls">Contrôle de lecture</string>
<string name="playback_media_controls_description">Contrôler la lecture musicale</string>
<string name="playback_shuffle">Lecture aléatoire</string>
@ -63,6 +69,8 @@
<string name="playback_queue_add_item">Ajouter à la liste de lecture</string>
<string name="playback_queue_play_next">Prochaine écoute</string>
<string name="playback_queue_download">Télécharger</string>
<string name="playback_queue_clear">Effacer</string>
<string name="playback_queue_save">Enregistrer</string>
<string name="manage_add_to_favorites">Ajouter aux favoris</string>
<string name="control_toggle">Lecture / pause</string>
<string name="control_previous">Piste précédente</string>
@ -77,13 +85,15 @@
<string name="alt_album_cover">Couverture de l\'album</string>
<string name="alt_more_options">Plus d\'options</string>
<string name="alt_track_info">Informations sur cette piste</string>
<string name="track_info_artist">Voir l\'artist</string>
<string name="track_info_artist">Voir l\'artiste</string>
<string name="track_info_album">Voir l\'album</string>
<string name="track_info_details">Informations</string>
<string name="track_info_details_title">Détails de la piste</string>
<string name="track_info_details_artist">Artiste</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_track_title">Nom de piste</string>
<string name="track_info_details_track_copyright">Copyright</string>
<string name="track_info_details_track_license">Licence</string>
<string name="track_info_details_track_duration">Durée</string>
<string name="track_info_details_track_position">Position dans l\'album</string>
<string name="track_info_details_track_bitrate">Bitrate</string>
@ -92,7 +102,7 @@
<string name="radio_instance_radios">Radios de l\'instance</string>
<string name="radio_user_radios">Radios des utilisateurs</string>
<string name="radio_your_content_title">Votre contenu</string>
<string name="radio_your_content_description">Une sélection de votre propre bibliothèque.</string>
<string name="radio_your_content_description">Une sélection de vos propres bibliothèques</string>
<string name="radio_favorites_description">Jouez vos morceaux favoris dans une boucle allègre infinie.</string>
<string name="radio_random_title">Aléatoire</string>
<string name="radio_random_description">Choix de pistes totalement aléatoires, vous découvrirez peut-être quelque chose ?</string>
@ -104,7 +114,14 @@
<item quantity="one">%1$d piste • %2$s</item>
<item quantity="other">%1$d pistes • %2$s</item>
</plurals>
<string name="only_my_music">Ma musique seulement</string>
<string name="playlist_add_to">Ajouter à une playlist</string>
<string name="playlist_add_to_new">Nouvelle playlist…</string>
<string name="playlist_add_to_create">Créer playlist</string>
<string name="playlist_added_to">Ajouté à la playlist %s</string>
<string name="filters">Filtres</string>
<string name="fiters_all">Toute la musique</string>
<string name="filters_my_music">Ma musique</string>
<string name="filters_followed">Contenu suivi</string>
<plurals name="downloads_description">
<item quantity="one">Téléchargement de %1$d piste</item>
<item quantity="other">Téléchargement de %1$d pistes</item>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="settings_experiments">Características experimentais</string>
<string name="settings_night_mode_system_summary">O modo noite seguirá ao sistema</string>
<string name="settings_night_mode_system">Seguir ao sistema</string>
<string name="settings_night_mode_off_summary">O modo claro estará activo sempre</string>
<string name="settings_night_mode_off">Desactivado (modo claro)</string>
<string name="settings_night_mode_on_summary">O modo escuro estará sempre activo</string>
<string name="settings_night_mode_on">Activo sempre (modo escuro)</string>
<string name="settings_night_mode">Modo escuro</string>
<string name="settings_other">Outro</string>
<string name="settings_play_order_in_order_summary">Prefires reproducir os álbumes en orde</string>
<string name="settings_play_order_in_order">Reproducir álbumes en orde</string>
<string name="settings_play_order_shuffle_summary">Prefires mesturar as pistas dos álbumes</string>
<string name="settings_play_order_shuffle">Barallar álbumes</string>
<string name="settings_play_order">Orde preferida de reprodución</string>
<string name="settings_media_cache_size_summary">Utilizarase %d GB de almacenaxe para reprodución sen conexión</string>
<string name="settings_media_cache_size">Tamaño da caché multimedia</string>
<string name="settings_media_quality_summary_size">Reproducirase a pista co menor tamaño dispoñible</string>
<string name="settings_media_quality_summary_quality">Reproducirase a mellor calidade dispoñible</string>
<string name="settings_media_quality_size">O menor tamaño</string>
<string name="settings_media_quality_quality">A mellor calidade</string>
<string name="settings_media_quality">Calidade do multimedia</string>
<string name="settings_general">Xeral</string>
<string name="search_no_results">Non se atopan resultados</string>
<string name="search_welcome">Escribe os termos a buscar e preme enter para buscar na túa colección</string>
<string name="search_placeholder">Busca artistas, álbumes e cancións</string>
<string name="title_oss_licences">Licenzas libres</string>
<string name="title_settings">Axustes</string>
<string name="title_downloads">Descargas</string>
<string name="toolbar_search">Buscar</string>
<string name="login_error_userinfo">Non se puido obter info da usuaria</string>
<string name="login_error_hostname_https">O servidor Funkwhale debería ser seguro a través de HTTPS</string>
<string name="login_error_hostname">Escribe un URL válido</string>
<string name="login_logging_in">Accedendo…</string>
<string name="login_submit">Acceder</string>
<string name="login_password">Contrasinal</string>
<string name="login_username">Nome de usuaria</string>
<string name="login_anonymous">Autentificación anónima</string>
<string name="login_cleartext">Permitir tráfico sen cifrar (HTTP)</string>
<string name="login_hostname">Servidor</string>
<string name="login_welcome">Escribe os datos da instancia Funkwhale para acceder ó seu contido</string>
<string name="radios">Radios</string>
<plurals name="downloads_description">
<item quantity="one">Descargando %1$d pista…</item>
<item quantity="other">Descargando %1$d pistas…</item>
</plurals>
<string name="filters_followed">Contido seguido</string>
<string name="filters_my_music">A miña música</string>
<string name="fiters_all">Toda a música</string>
<string name="filters">Filtros</string>
<string name="playlist_added_to">Engadida á lista \"%s\"</string>
<string name="playlist_add_to_create">Crear lista de reprodución</string>
<string name="playlist_add_to_new">Nova lista…</string>
<string name="playlist_add_to">Engadir á lista</string>
<plurals name="playlist_description">
<item quantity="one">%1$d pista • %2$s</item>
<item quantity="other">%1$d pistas • %2$s</item>
</plurals>
<string name="logout_content">Queres saír da sesión en Funkwhale\?</string>
<string name="logout_title">Pechar sesión</string>
<string name="radio_less_listened_description">Escoita pistas que non adoitas escoitar.</string>
<string name="radio_less_listened_title">Menos reproducido</string>
<string name="radio_random_description">Escollas ao chou, igoal descubres algo novo\?</string>
<string name="radio_random_title">Ó chou</string>
<string name="radio_favorites_description">Reproduce os teus favoritos nun bucle de felicidade infinita.</string>
<string name="radio_your_content_description">Escollas das túas propias bibliotecas</string>
<string name="radio_your_content_title">O teu contido</string>
<string name="radio_user_radios">Radios da usuaria</string>
<string name="radio_instance_radios">Radios da instancia</string>
<string name="radio_playback_error">Non se puido reproducir a radio</string>
<string name="track_info_details_track_instance">Instancia Funkwhale</string>
<string name="track_info_details_track_bitrate">Taxa de bits</string>
<string name="track_info_details_track_position">Posición no álbume</string>
<string name="track_info_details_track_duration">Duración</string>
<string name="track_info_details_track_license">Licenza</string>
<string name="track_info_details_track_copyright">Copyright</string>
<string name="track_info_details_track_title">Título da pista</string>
<string name="track_info_details_album">Álbume</string>
<string name="track_info_details_artist">Artista</string>
<string name="track_info_details_title">Detalles da psita</string>
<string name="track_info_details">Info</string>
<string name="track_info_album">Ir ó álbume</string>
<string name="track_info_artist">Ir á artista</string>
<string name="alt_track_info">Info da pista</string>
<string name="alt_more_options">Máis opcións</string>
<string name="alt_album_cover">Cuberta do álbume</string>
<string name="alt_artist_art">Arte da artista</string>
<string name="alt_app_logo">Logo da app</string>
<plurals name="album_count">
<item quantity="one">%d álbume</item>
<item quantity="other">%d álbumes</item>
</plurals>
<string name="error_playback">Non se puido reproducir a pista</string>
<string name="control_next">Pista seguinte</string>
<string name="control_previous">Pista anterior</string>
<string name="control_toggle">Activar reprodución</string>
<string name="manage_add_to_favorites">Engadir a favoritas</string>
<string name="playback_queue_save">Gardar</string>
<string name="playback_queue_clear">Baleirar</string>
<string name="playback_queue_download">Descargar</string>
<string name="playback_queue_play_next">Reproducir a continuación</string>
<string name="playback_queue_add_item">Engadir á cola</string>
<string name="playback_queue_remove_item">Eliminar</string>
<string name="playback_queue_empty">A cola está baleira</string>
<string name="playback_queue">Cola</string>
<string name="playback_shuffle">Barallar</string>
<string name="playback_play">Reproducir</string>
<string name="playback_media_controls_description">Control da reprodución multimedia</string>
<string name="playback_media_controls">Control da reprodución</string>
<string name="favorites">Favoritas</string>
<string name="playlists">Listaxes</string>
<string name="tracks">Pistas</string>
<string name="albums">Álbumes</string>
<string name="artists">Artistas</string>
<string name="settings_logout">Pechar sesión</string>
<string name="settings_crash_report_copied">Copiouse ao portapapeis o último informe de fallos</string>
<string name="settings_crash_report_description">Só se recollen os rexistros de Otter dos 5 minutos anteriores ao fallo</string>
<string name="settings_crash_report_title">Copiar rexistro de fallos</string>
<string name="settings_information_license_description">Licenza MIT</string>
<string name="settings_information_license_title">Licenza</string>
<string name="settings_version_title">Versión</string>
<string name="settings_information_repository_description">Otter por Antonie POPINEAU (apognu)</string>
<string name="settings_information_repository_title">Repositorio</string>
<string name="settings_information">Info</string>
<string name="settings_experiments_restart_content">Pecha e reinicia a app para que se aplique o cambio</string>
<string name="settings_experiments_restart_title">Require reiniciar a app</string>
<string name="settings_experiments_description">Usa baixo a túa responsabilidade. Podería facer fallar a app.</string>
</resources>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="downloads_description">
<item quantity="one">Preuzimanje pjesme %1$d</item>
<item quantity="few">Preuzimanje pjesama %1$d</item>
<item quantity="other">Preuzimanje pjesama %1$d</item>
</plurals>
<plurals name="playlist_description">
<item quantity="one">%1$d pjesma • %2$s</item>
<item quantity="few">%1$d pjesme • %2$s</item>
<item quantity="other">%1$d pjesme • %2$s</item>
</plurals>
<plurals name="album_count">
<item quantity="one">%d album</item>
<item quantity="few">%d albuma</item>
<item quantity="other">%d albuma</item>
</plurals>
<string name="filters_followed">Praćeni sadržaj</string>
<string name="filters_my_music">Moja glazba</string>
<string name="fiters_all">Sva glazba</string>
<string name="filters">Filteri</string>
<string name="playlist_added_to">Dodanu u listu %s</string>
<string name="playlist_add_to_create">Stvorite listu</string>
<string name="playlist_add_to_new">Nova lista…</string>
<string name="playlist_add_to">Dodaj u listu</string>
<string name="logout_content">Jeste li sigurni da se želite odjaviti iz ove Funkwhale instance\?</string>
<string name="logout_title">Odjava</string>
<string name="radio_less_listened_description">Slušajte trake koje inače ne slušate. Vrijeme je da ostvarite ravnotežu.</string>
<string name="radio_less_listened_title">Manje slušano</string>
<string name="radio_random_description">Potpuno nasumični odabiri, možda otkrijete nove stvari\?</string>
<string name="radio_random_title">Nasumično</string>
<string name="radio_favorites_description">Igrajte vaše najdraže pjesme u neprekidnom krugu sreće.</string>
<string name="radio_your_content_description">Odabiri iz vaše biblioteke</string>
<string name="radio_your_content_title">Tvoj sadržaj</string>
<string name="radio_user_radios">Radiji korisnika</string>
<string name="radio_instance_radios">Radiji instance</string>
<string name="radio_playback_error">Pogreška pri pokušaju reprodukcije radija</string>
<string name="track_info_details_track_instance">Funkwhale instanca</string>
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_position">Pozicija albuma</string>
<string name="track_info_details_track_duration">Trajanje</string>
<string name="track_info_details_track_license">Licenca</string>
<string name="track_info_details_track_copyright">Autorska prava</string>
<string name="track_info_details_track_title">Naziv</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_artist">Umjetnik</string>
<string name="track_info_details_title">Detalji</string>
<string name="track_info_details">Informacije</string>
<string name="track_info_album">Idi na album</string>
<string name="track_info_artist">Idi na umjetnika</string>
<string name="alt_track_info">Informacije o traci</string>
<string name="alt_more_options">Više opcija</string>
<string name="alt_album_cover">Naslovna albuma</string>
<string name="alt_artist_art">Naslovna umjetnika</string>
<string name="alt_app_logo">Logo aplikacije</string>
<string name="error_playback">Ova traka se nije mogla reproducirati</string>
<string name="control_next">Sljedeće</string>
<string name="control_previous">Prijašnje</string>
<string name="control_toggle">Igraj / Pauziraj</string>
<string name="manage_add_to_favorites">Dodaj u favorite</string>
<string name="playback_queue_save">Spremi</string>
<string name="playback_queue_clear">Očisti</string>
<string name="playback_queue_download">Preuzmi</string>
<string name="playback_queue_play_next">Igraj sljedeće</string>
<string name="playback_queue_add_item">Dodaj u poredak</string>
<string name="playback_queue_remove_item">Ukloni</string>
<string name="playback_queue_empty">Poredak je prazan</string>
<string name="playback_queue">Poredak</string>
<string name="playback_shuffle">Izmješaj</string>
<string name="playback_play">Igraj</string>
<string name="playback_media_controls_description">Kontrola reprodukcije medija</string>
<string name="playback_media_controls">Kontrola medija</string>
<string name="favorites">Favoriti</string>
<string name="radios">Radiji</string>
<string name="playlists">Liste</string>
<string name="tracks">Pjesme</string>
<string name="albums">Albumi</string>
<string name="artists">Umjetnici</string>
<string name="settings_logout">Odjava</string>
<string name="settings_crash_report_copied">Zadnje izvješće o rušenju je kopirano na međuspremnik</string>
<string name="settings_crash_report_description">Samo Otter-ova izvješća zadnjih 5 minuta prije pada će biti spremljena</string>
<string name="settings_crash_report_title">Kopiraj izvješća rušenja</string>
<string name="settings_information_license_description">MIT licenca</string>
<string name="settings_information_license_title">Licenca</string>
<string name="settings_version_title">Verzija</string>
<string name="settings_information_repository_description">Otter od Antoine POPINEAU (apognu)</string>
<string name="settings_information_repository_title">Softverski repozitorij</string>
<string name="settings_information">Informacije</string>
<string name="settings_experiments_restart_content">Molimo ugasite i ponovno pokrenite aplikaciju kako bi se promjene primjenile</string>
<string name="settings_experiments_restart_title">Potrebno ponovno pokretanje</string>
<string name="settings_experiments_description">Koristiti na svoju odgovornost, može zamrznuti ili srušiti aplikaciju</string>
<string name="settings_experiments">Omogući eksperimentalne funkcije</string>
<string name="settings_night_mode_system_summary">Tamni izgled će se prilagoditi postavkama sistema</string>
<string name="settings_night_mode_system">Prilagodi postavkama sistema</string>
<string name="settings_night_mode_off">Uvijek isključen (svjetli izgled)</string>
<string name="settings_night_mode_off_summary">Svjetli izgled će uvijek biti isključen</string>
<string name="settings_night_mode_on_summary">Tamni izgled će uvijek biti uključen</string>
<string name="settings_night_mode_on">Uvijek uključen (tamni izgled)</string>
<string name="settings_night_mode">Tamni izgled</string>
<string name="settings_other">Ostalo</string>
<string name="settings_play_order_in_order_summary">Preferirate igrati albume po redu</string>
<string name="settings_play_order_in_order">Igraj albume po redu</string>
<string name="settings_play_order_shuffle_summary">Preferirate mješanje albumnih traka</string>
<string name="settings_play_order_shuffle">Izmješaj albume</string>
<string name="settings_play_order">Preferirani poredak reprodukcije</string>
<string name="settings_media_cache_size_summary">%d GB će se koristiti za pohranjivanje datoteka u svrhu izvanmrežne reprodukcije</string>
<string name="settings_media_cache_size">Veličina medijske predmemorije (cache)</string>
<string name="settings_media_quality_summary_size">Igrat će se najmanja dostupna datoteka</string>
<string name="settings_media_quality_summary_quality">Igrat će se najbolja dostupna verzija</string>
<string name="settings_media_quality_size">Najmanja veličina</string>
<string name="settings_media_quality_quality">Najbolja kvaliteta</string>
<string name="settings_media_quality">Kvaliteta medija</string>
<string name="settings_general">Opće</string>
<string name="search_no_results">Nema rezultata za vaš upit</string>
<string name="search_welcome">Iznad upišite svoj pojam/ve i stisnite Enter za pretraživanje kolekcije</string>
<string name="search_placeholder">Pretraži umjetnike, albume i pjesme</string>
<string name="title_oss_licences">Open source licence</string>
<string name="title_settings">Postavke</string>
<string name="title_downloads">Preuzimanja</string>
<string name="toolbar_search">Traži</string>
<string name="login_error_userinfo">Nismo mogli preuzeti informacije o vašem korisniku</string>
<string name="login_error_hostname_https">Funkwhale operator bi trebao biti siguran sa HTTPS-om</string>
<string name="login_error_hostname">Ovo se nije moglo prepoznati kao validan URL</string>
<string name="login_logging_in">Prijavljivanje</string>
<string name="login_submit">Prijavi se</string>
<string name="login_password">Lozinka</string>
<string name="login_username">Korisničko ime</string>
<string name="login_anonymous">Anonimna autentikacija</string>
<string name="login_cleartext">Dozvoli cleartext promet (HTTP)</string>
<string name="login_hostname">Ime operatera</string>
<string name="login_welcome">Molimo unesite detalje vaše Funkwhale \'instance\' kako bi pristupili njenom sadržaju</string>
</resources>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="login_submit">Masuk</string>
<string name="settings_other">Lainnya</string>
<string name="settings_night_mode">Mode gelap</string>
<string name="settings_night_mode_system">Ikuti sistem</string>
<string name="settings_information">Informasi</string>
<string name="settings_version_title">Versi</string>
<string name="settings_information_license_title">Lisensi</string>
<string name="settings_logout">Keluar</string>
<string name="artists">Artis</string>
<string name="albums">Album</string>
<string name="login_hostname">Hostname</string>
<string name="login_anonymous">Autentikasi anonim</string>
<string name="login_username">Nama pengguna</string>
<string name="login_password">Sandi</string>
<string name="login_logging_in">Sedang masuk…</string>
<string name="login_error_userinfo">Tidak dapat memuat informasi pengguna</string>
<string name="toolbar_search">Telusuri</string>
<string name="title_downloads">Unduhan</string>
<string name="title_settings">Pengaturan</string>
<string name="title_oss_licences">Lisensi libre</string>
<string name="search_no_results">Tak ada hasil</string>
<string name="settings_general">Umum</string>
<string name="settings_media_quality">Kualitas media</string>
<string name="login_error_hostname">Masukkan URL yang sah terlebih dahulu</string>
<string name="search_welcome">Masukkan pencarian Anda di atas dan tekan enter untuk menelusuri koleksi Anda</string>
<string name="settings_media_quality_quality">Kualitas terbaik</string>
<string name="settings_media_quality_size">Ukuran terkecil</string>
<string name="settings_media_quality_summary_quality">Versi terbaik yang akan diputar</string>
<string name="settings_media_cache_size">Ukuran cache untuk media</string>
<string name="settings_play_order_in_order">Putar album sesuai urutan</string>
<string name="settings_play_order_in_order_summary">Anda memilih untuk memutar album sesuai urutan</string>
<string name="settings_night_mode_on">Selalu aktif (mode gelap)</string>
<string name="settings_night_mode_on_summary">Mode gelap akan selalu aktif</string>
<string name="settings_night_mode_off">Selalu nonaktif (mode cerah)</string>
<string name="settings_night_mode_off_summary">Mode cerah akan selalu aktif</string>
<string name="settings_night_mode_system_summary">Mode gelap akan mengikuti sistem</string>
<string name="settings_experiments">Fitur eksperimental</string>
<string name="settings_experiments_restart_title">Aplikasi harus dimulai ulang</string>
<string name="settings_experiments_restart_content">Tutup kemudian mulai kembali aplikasi untuk menggunakan pengaturan baru</string>
<string name="settings_information_repository_title">Repositori</string>
<string name="settings_information_repository_description">Otter oleh Antoine POPINEAU (apognu)</string>
<string name="settings_information_license_description">Lisensi MIT</string>
</resources>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="album_count">
<item quantity="one">%d album</item>
<item quantity="other">%d album</item>
</plurals>
<string name="playback_shuffle">Shuffle</string>
<string name="playback_play">Riproduci</string>
<string name="alt_artist_art">Immagine dell\'artista</string>
<string name="playback_queue_clear">Pulisci</string>
<plurals name="downloads_description">
<item quantity="one">Scaricando %1$d brano</item>
<item quantity="other">Scaricando %1$d brani</item>
</plurals>
<string name="filters_followed">Contenuto seguito</string>
<string name="filters_my_music">La mia musica</string>
<string name="fiters_all">Tutta la musica</string>
<string name="filters">Filtri</string>
<string name="playlist_added_to">Aggiunto alla playlist %s</string>
<string name="playlist_add_to_create">Crea playlist</string>
<string name="playlist_add_to_new">Nuova playlist…</string>
<string name="playlist_add_to">Aggiungi alla playlist</string>
<plurals name="playlist_description">
<item quantity="one">%1$d brano • %2$s</item>
<item quantity="other">%1$d brani • %2$s</item>
</plurals>
<string name="logout_content">Sei sicuro di voler uscire da questa istanza di Funkwhale\?</string>
<string name="logout_title">Disconnetti</string>
<string name="radio_less_listened_description">Ascolta i brani che di solito non fai. È ora di ristabilire un po\' di equilibrio.</string>
<string name="radio_less_listened_title">Meno ascoltato</string>
<string name="radio_random_description">Scelte totalmente casuali, forse scoprirai cose nuove\?</string>
<string name="radio_random_title">Casuale</string>
<string name="radio_favorites_description">Riproduci i tuoi brani preferiti in un ciclo di felicità senza fine.</string>
<string name="radio_your_content_description">Scegli dalle tue librerie</string>
<string name="radio_your_content_title">Il tuo contenuto</string>
<string name="radio_user_radios">Radio dell\'utente</string>
<string name="radio_instance_radios">Radio dell\'istanza</string>
<string name="radio_playback_error">Si è verificato un errore durante il tentativo di riprodurre questa radio</string>
<string name="track_info_details_track_instance">Istanza di Funkwhale</string>
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_position">Positione dell\'album</string>
<string name="track_info_details_track_duration">Durata</string>
<string name="track_info_details_track_license">Licenza</string>
<string name="track_info_details_track_copyright">Copyright</string>
<string name="track_info_details_track_title">Titolo del brano</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_artist">Artista</string>
<string name="track_info_details_title">Dettagli del brano</string>
<string name="track_info_details">Informazione</string>
<string name="track_info_album">Vai all\'album</string>
<string name="track_info_artist">Vai all\'artista</string>
<string name="alt_track_info">Informazioni sul brano</string>
<string name="alt_more_options">Più opzioni</string>
<string name="alt_album_cover">Cover dell\'album</string>
<string name="alt_app_logo">Logo dell\'applicazione</string>
<string name="error_playback">Impossibile riprodurre questo brano</string>
<string name="control_next">Traccia successiva</string>
<string name="control_previous">Brano precedente</string>
<string name="control_toggle">Attiva/disattiva la riproduzione</string>
<string name="manage_add_to_favorites">Aggiungi ai preferiti</string>
<string name="playback_queue_save">Salva</string>
<string name="playback_queue_download">Scarica</string>
<string name="playback_queue_play_next">Riproduci successivamente</string>
<string name="playback_queue_add_item">Aggiungi alla coda</string>
<string name="playback_queue_remove_item">Rimuovi</string>
<string name="playback_queue_empty">La tua coda è vuota</string>
<string name="playback_queue">Coda</string>
<string name="playback_media_controls_description">Controlla la riproduzione multimediale</string>
<string name="playback_media_controls">Controlli multimediali</string>
<string name="favorites">Preferiti</string>
<string name="radios">Radio</string>
<string name="playlists">Playlist</string>
<string name="tracks">Brani</string>
<string name="albums">Album</string>
<string name="artists">Artisti</string>
<string name="settings_logout">Disconnetti</string>
<string name="settings_crash_report_copied">L\'ultimo rapporto sull\'arresto anomalo è stato copiato negli appunti</string>
<string name="settings_crash_report_description">Verranno raccolti solo i registri di Otter dagli ultimi 5 minuti fino all\'arresto</string>
<string name="settings_crash_report_title">Copia i registri degli arresti anomali</string>
<string name="settings_information_license_description">Licenza MIT</string>
<string name="settings_information_license_title">Licenza</string>
<string name="settings_version_title">Versione</string>
<string name="settings_information_repository_description">Otter di Antoine POPINEAU (apognu)</string>
<string name="settings_information_repository_title">Repository</string>
<string name="settings_information">Informazione</string>
<string name="settings_experiments_restart_content">Si prega di chiudere e riavviare l\'app affinché questa modifica abbia effetto</string>
<string name="settings_experiments_restart_title">Riavvio richiesto</string>
<string name="settings_experiments_description">Utilizzare a proprio rischio, potrebbe bloccare o mandare in crash l\'app</string>
<string name="settings_experiments">Abilita funzionalità sperimentali</string>
<string name="settings_night_mode_system_summary">La modalità notturna seguirà le impostazioni di sistema</string>
<string name="settings_night_mode_system">Segui le impostazioni di sistema</string>
<string name="settings_night_mode_off_summary">La modalità luce sarà sempre attiva</string>
<string name="settings_night_mode_off">Sempre spento (modalità luce)</string>
<string name="settings_night_mode_on_summary">La modalità oscura sarà sempre attiva</string>
<string name="settings_night_mode_on">Sempre attivo (modalità oscura)</string>
<string name="settings_night_mode">Modalità oscura</string>
<string name="settings_other">Altro</string>
<string name="settings_play_order_in_order_summary">Preferisci riprodurre gli album in ordine</string>
<string name="settings_play_order_in_order">Riproduci gli album in ordine</string>
<string name="settings_play_order_shuffle_summary">Preferisci mescolare le tracce degli album</string>
<string name="settings_play_order_shuffle">Album casuali</string>
<string name="settings_play_order">Ordine di riproduzione preferito</string>
<string name="settings_media_cache_size_summary">%d GB verranno utilizzati per memorizzare le tracce per la riproduzione offline</string>
<string name="settings_media_cache_size">Dimensioni della cache multimediale</string>
<string name="settings_media_quality_summary_size">Verrà riprodotta la traccia più piccola disponibile</string>
<string name="settings_media_quality_summary_quality">Verrà riprodotta la migliore versione disponibile</string>
<string name="settings_media_quality_size">Dimensioni minime</string>
<string name="settings_media_quality_quality">Migliore qualità</string>
<string name="settings_media_quality">Qualità dei media</string>
<string name="settings_general">Generale</string>
<string name="search_no_results">Nessun risultato trovato per la tua ricerca</string>
<string name="search_welcome">Inserisci i termini di ricerca sopra e premi invio per cercare nella tua raccolta</string>
<string name="search_placeholder">Cerca artisti, album e brani</string>
<string name="title_oss_licences">Licenze open source</string>
<string name="title_settings">Impostazioni</string>
<string name="title_downloads">Downloads</string>
<string name="toolbar_search">Cerca</string>
<string name="login_error_userinfo">Non siamo riusciti a recuperare le informazioni sul tuo utente</string>
<string name="login_error_hostname_https">Il nome dell\'host di Funkwhale dovrebbe essere protetto tramite HTTPS</string>
<string name="login_error_hostname">Questo non può essere interpretato come un URL valido</string>
<string name="login_logging_in">Entrando</string>
<string name="login_submit">Accedi</string>
<string name="login_password">Password</string>
<string name="login_username">Nome utente</string>
<string name="login_anonymous">Autenticazione anonima</string>
<string name="login_cleartext">Consenti traffico in chiaro (HTTP)</string>
<string name="login_hostname">Nome dell\'host</string>
<string name="login_welcome">Inserisci i dettagli della tua istanza di Funkwhale per accedere al suo contenuto</string>
</resources>

View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="settings_experiments">実験的な機能を有効にする</string>
<string name="title_settings">設定</string>
<string name="title_oss_licences">オープンソースライセンス</string>
<string name="settings_general">一般設定</string>
<string name="settings_media_cache_size">メディアのキャッシュサイズ</string>
<string name="settings_media_cache_size_summary">オフライン再生向けの曲の保存のために、%d GBが使われます</string>
<string name="settings_night_mode">ダークモード</string>
<string name="settings_other">その他の設定</string>
<string name="settings_experiments_restart_title">アプリの再起動が必要です</string>
<string name="settings_experiments_restart_content">変更を適用するためにアプリを終了し、再起動して下さい</string>
<string name="settings_information">情報</string>
<string name="settings_information_repository_title">Gitリポジトリ</string>
<string name="settings_information_repository_description">Otter by Antoine POPINEAU apognu</string>
<string name="settings_version_title">バージョン</string>
<string name="settings_information_license_title">ライセンス</string>
<string name="settings_information_license_description">MIT License</string>
<string name="settings_crash_report_title">クラッシュログのコピー</string>
<string name="login_username">ユーザーネーム</string>
<string name="login_password">パスワード</string>
<string name="login_submit">ログイン</string>
<string name="login_logging_in">ログインしています</string>
<string name="toolbar_search">検索</string>
<string name="title_downloads">ダウンロード一覧</string>
<string name="search_placeholder">アーティスト、アルバム、曲を探す</string>
<string name="settings_experiments_description">あなた自身のリスクで使用してください。アプリがフリーズしたり、クラッシュするかもしれません</string>
<string name="settings_crash_report_copied">クリップボードに前回のクラッシュレポートがコピーされました</string>
<string name="track_info_details">曲情報</string>
<string name="track_info_details_title">この曲について</string>
<string name="track_info_details_artist">アーティスト</string>
<string name="track_info_album">アルバムのページへ</string>
<string name="playback_queue_add_item">キューに追加する</string>
<string name="playback_queue_play_next">次の曲を再生</string>
<string name="playback_queue_clear">クリア</string>
<string name="playback_queue_save">保存</string>
<string name="manage_add_to_favorites">お気に入りに追加</string>
<string name="control_previous">前の曲</string>
<string name="control_next">次の曲</string>
<plurals name="album_count">
<item quantity="other">%dアルバム</item>
</plurals>
<string name="alt_album_cover">アルバムカバー</string>
<string name="alt_artist_art">アーティストアート</string>
<string name="alt_app_logo">アプリロゴ</string>
<string name="alt_track_info">曲についての情報</string>
<string name="radio_random_title">ランダム</string>
<string name="logout_title">サインアウト</string>
<string name="track_info_details_track_title">曲名</string>
<string name="track_info_details_track_copyright">コピーライト</string>
<string name="track_info_details_track_license">ライセンス</string>
<string name="track_info_details_track_duration">再生時間</string>
<string name="track_info_details_track_bitrate">ビットレート</string>
<string name="track_info_details_track_instance">Funkwhaleインスタンス</string>
<string name="radio_instance_radios">インスタンスのラジオ</string>
<string name="radio_user_radios">ユーザーのラジオ</string>
<plurals name="playlist_description">
<item quantity="other">%1$d曲 • %2$s分</item>
</plurals>
<string name="playlist_add_to">プレイリストに追加</string>
<string name="playlist_add_to_create">プレイリストを作成</string>
<string name="logout_content">本当にこのFunkwhaleポッドからサインアウトしますか</string>
<plurals name="downloads_description">
<item quantity="other">%1$d曲をダウンロードしています</item>
</plurals>
<string name="playback_queue_empty">キューに何もありません</string>
<string name="settings_logout">サインアウト</string>
<string name="artists">アーティスト</string>
<string name="albums">アルバム</string>
<string name="tracks"></string>
<string name="playlists">プレイリスト</string>
<string name="radios">ラジオ</string>
<string name="favorites">お気に入り</string>
<string name="settings_night_mode_on">常にオン(ダークモード)</string>
<string name="settings_night_mode_on_summary">常にダークモードになっています</string>
<string name="settings_night_mode_off">常にオフ(ライトモード)</string>
<string name="settings_night_mode_off_summary">常にライトモードになっています</string>
<string name="settings_night_mode_system">システムの設定に従う</string>
<string name="settings_media_quality_quality">最高品質</string>
<string name="settings_media_quality_size">最小サイズ</string>
<string name="settings_media_quality">メディアのクオリティー</string>
<string name="settings_media_quality_summary_quality">最高品質のメディアが再生されます</string>
<string name="settings_media_quality_summary_size">サイズが小さな曲が再生されます</string>
<string name="settings_play_order">好きな再生方法</string>
<string name="settings_play_order_shuffle">アルバムの曲をシャッフル再生</string>
<string name="settings_play_order_shuffle_summary">アルバムの曲をシャッフル再生します</string>
<string name="settings_play_order_in_order_summary">アルバムの曲を順番に再生します</string>
<string name="settings_play_order_in_order">アルバムの曲を上から順に再生</string>
<string name="settings_night_mode_system_summary">システムの設定に従います</string>
<string name="settings_crash_report_description">Otterのログは、クラッシュするまでの5分間だけ収集されます</string>
<string name="playback_media_controls">メディアコントロール</string>
<string name="playback_media_controls_description">メディアの再生について管理する</string>
<string name="playback_play">再生</string>
<string name="playback_shuffle">シャッフル</string>
<string name="playback_queue_remove_item">除く</string>
<string name="login_anonymous">匿名での認証</string>
<string name="login_hostname">ホストネーム</string>
<string name="login_error_hostname">有効なURLとして認識されませんでした</string>
<string name="login_error_hostname_https">FunkwhaleのホストネームはHTTPSから始まる安全なものにすべきです</string>
<string name="login_error_userinfo">あなたのユーザー情報を取得できませんでした</string>
<string name="playback_queue">キュー</string>
<string name="playback_queue_download">ダウンロード</string>
<string name="track_info_details_album">アルバム</string>
<string name="track_info_artist">アーティストのページへ</string>
<string name="filters">フィルター</string>
<string name="login_welcome">Funkwhaleポッドのコンテンツにアクセスするため、ポッドについての情報を入力してください</string>
<string name="login_cleartext">平文での通信HTTPを許可する</string>
<string name="error_playback">この曲は再生できませんでした</string>
<string name="radio_playback_error">ラジオの再生を試みてる間にエラーがありました</string>
</resources>

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="login_welcome">Skriv inn detaljene til en Funkwhale-instans for å få tilgang til dens innhold</string>
<string name="login_anonymous">Anonym identitetsbekreftelse</string>
<string name="login_username">Brukernavn</string>
<string name="login_password">Passord</string>
<string name="login_submit">Logg inn</string>
<string name="login_logging_in">Logger inn …</string>
<string name="login_error_hostname">Skriv inn en gyldig nettadresse først</string>
<string name="login_error_hostname_https">Funkwhale-vertsnavnet burde sikres med HTTPS</string>
<string name="login_error_userinfo">Kunne ikke hente brukerinfo</string>
<string name="toolbar_search">Søk</string>
<string name="title_downloads">Nedlastninger</string>
<string name="title_settings">Innstillinger</string>
<string name="title_oss_licences">Friprog-lisenser</string>
<string name="search_welcome">Skriv inn søket din ovenfor og trykk enter for å søke i samlingen din</string>
<string name="search_no_results">Resultatløst</string>
<string name="settings_general">Generelt</string>
<string name="settings_media_quality">Mediakvalitet</string>
<string name="settings_media_quality_size">Minste størrelse</string>
<string name="settings_media_quality_summary_quality">Beste tilgjengelige versjon vil bli avspilt</string>
<string name="settings_media_quality_summary_size">Minste tilgjengelige versjon vil bli avspilt</string>
<string name="settings_media_cache_size">Media-hurtiglagringsstørrelse</string>
<string name="settings_media_cache_size_summary">%d GB vil brukes til å lagre spor for frakoblet avspilling</string>
<string name="search_placeholder">Søk etter artister, album og spor</string>
<string name="settings_media_quality_quality">Beste kvalitet</string>
<string name="error_playback">Kunne ikke spille av sporet</string>
<plurals name="downloads_description">
<item quantity="one">Laster ned %1$d spor…</item>
<item quantity="other">Laster ned %1$d spor…</item>
</plurals>
<string name="login_hostname">Vertsnavn</string>
<string name="login_cleartext">Tillat klarteksttrafikk (HTTP)</string>
<string name="settings_play_order">Foretrukket avspillingsrekkefølge</string>
<string name="settings_play_order_in_order">Spill album i rekkefølge</string>
<string name="settings_play_order_in_order_summary">Du foretrekker avspilling av album i rekkefølge</string>
<string name="settings_other">Annet</string>
<string name="settings_night_mode">Mørk drakt</string>
<string name="settings_night_mode_on">Alltid på (mørk drakt)</string>
<string name="settings_play_order_shuffle_summary">Du foretrekker omstokking av album</string>
<string name="settings_play_order_shuffle">Omstokking av album</string>
<string name="settings_night_mode_on_summary">Mørk drakt vil alltid iføres</string>
<string name="settings_night_mode_off">Alltid av (lys drakt)</string>
<string name="settings_night_mode_off_summary">Lys drakt vil alltid iføres</string>
<string name="settings_experiments_restart_content">Avslutt og start programmet igjen for å bruke endringene</string>
<string name="settings_experiments_restart_title">Programomstart kreves</string>
<string name="settings_experiments_description">Bruk på egen risiko. Kan fryse eller krasje programmet.</string>
<string name="settings_night_mode_system">Følg system</string>
<string name="settings_experiments">Eksperimentelle funksjoner</string>
<string name="settings_night_mode_system_summary">Nattmodus vil følge systemet</string>
<string name="settings_information_repository_description">Otter av Antoine POPINEAU (apognu)</string>
<string name="settings_version_title">Versjon</string>
<string name="settings_information_license_title">Lisens</string>
<string name="settings_information_license_description">MIT-lisens</string>
<string name="settings_crash_report_title">Kopier krasjloggføring</string>
<string name="settings_information_repository_title">Pakkebrønn</string>
<string name="settings_information">Info</string>
<string name="settings_crash_report_description">Kun Otter sine logger 5 minutter før krasjet vil samles inn</string>
<string name="settings_crash_report_copied">Siste krasjrapport kopiert til utklippstavlen</string>
<string name="settings_logout">Logg ut</string>
<string name="artists">Artister</string>
<string name="albums">Album</string>
<string name="tracks">Spor</string>
<string name="playlists">Spillelister</string>
<string name="radios">Radiostasjoner</string>
<string name="favorites">Favoritter</string>
<string name="playback_media_controls">Mediakontroller</string>
<string name="playback_media_controls_description">Kontroller mediaavspilling</string>
<string name="playback_play">Spill</string>
<string name="playback_shuffle">Omstokking</string>
<string name="playback_queue_remove_item">Fjern</string>
<string name="playback_queue_add_item">Legg til i avspillingskø</string>
<string name="playback_queue">Avspillingskø</string>
<string name="playback_queue_empty">Avspillingskøen din er tom</string>
<string name="playback_queue_play_next">Spill av neste</string>
<string name="playback_queue_download">Last ned</string>
<string name="playback_queue_clear">Tøm</string>
<string name="playback_queue_save">Lagre</string>
<string name="manage_add_to_favorites">Legg til som favoritt</string>
<string name="control_toggle">Veksle avspilling</string>
<string name="control_previous">Forrige spor</string>
<string name="control_next">Neste spor</string>
<plurals name="album_count">
<item quantity="one">%d album</item>
<item quantity="other">%d album</item>
</plurals>
<string name="alt_artist_art">Artistkunst</string>
<string name="alt_album_cover">Albumsomslag</string>
<string name="alt_more_options">Flere innstillinger</string>
<string name="track_info_artist">Gå til artist</string>
<string name="track_info_album">Gå til album</string>
<string name="track_info_details_title">Spordetaljer</string>
<string name="track_info_details_artist">Artist</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_track_title">Sportittel</string>
<string name="track_info_details_track_copyright">Opphavsrett</string>
<string name="track_info_details_track_license">Lisens</string>
<string name="track_info_details_track_duration">Varighet</string>
<string name="track_info_details_track_position">Albumposisjon</string>
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_instance">Funkwhate-instans</string>
<string name="radio_instance_radios">Instanse-radiostasjoner</string>
<string name="radio_user_radios">Bruker-radiostasjoner</string>
<string name="radio_your_content_title">Ditt innhold</string>
<string name="logout_title">Logg ut</string>
<plurals name="playlist_description">
<item quantity="one">%1$d spor • %2$s</item>
<item quantity="other">%1$d spor • %2$s</item>
</plurals>
<string name="radio_your_content_description">Fra dine egne bibliotek</string>
<string name="radio_random_title">Tilfeldig</string>
<string name="logout_content">Logg ut av denne Funkwhale-instansen\?</string>
<string name="radio_less_listened_description">Lytt til spor du vanligvis ikke spiller.</string>
<string name="radio_less_listened_title">Sjeldnere spilt</string>
<string name="radio_favorites_description">Spill dine favoritter i en uendelig gledesløkke.</string>
<string name="radio_playback_error">Kunne ikke spille denne radiostasjonen</string>
<string name="track_info_details">Info</string>
<string name="alt_track_info">Sporinfo</string>
<string name="alt_app_logo">Programlogo</string>
<string name="radio_random_description">Helt tilfeldig plukket. Kanskje du vil oppdage noe nytt\?</string>
<string name="playlist_add_to">Legg til i spilleliste</string>
<string name="playlist_add_to_new">Ny spilleliste …</string>
<string name="playlist_add_to_create">Opprett spilleliste</string>
<string name="filters">Filtre</string>
<string name="fiters_all">All musikk</string>
<string name="filters_my_music">Min musikk</string>
<string name="filters_followed">Fulgt innhold</string>
<string name="playlist_added_to">Lag til i «%s»-spillelisten</string>
</resources>

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="surface">#121212</color>
<color name="elevatedSurface">#191919</color>
<color name="colorPrimary">#283f4e</color>
<color name="colorAccent">#f1b44f</color>
<color name="colorAccent">#3282b8</color>
<color name="colorSelected">#525252</color>
<color name="colorFavorite">#eba999</color>

View File

@ -39,9 +39,9 @@
<string name="login_submit">Log in</string>
<string name="login_password">Wachtwoord</string>
<string name="login_username">Gebruikersnaam</string>
<string name="login_anonymous">Anoniem aanmelden</string>
<string name="login_anonymous">Anoniem inloggen</string>
<string name="login_hostname">Servernaam</string>
<string name="login_welcome">Voer de gegevens van je Funkwhale-server in om toegang te krijgen tot je media</string>
<string name="login_welcome">Voer de gegevens van een Funkwhale-server in om toegang te krijgen tot de media</string>
<string name="track_info_details_track_duration">Duur</string>
<string name="track_info_details_track_title">Nummer-titel</string>
<string name="track_info_details_album">Album</string>
@ -65,10 +65,10 @@
<string name="control_toggle">Afspelen / Pauze</string>
<string name="manage_add_to_favorites">Aan favorieten toevoegen</string>
<string name="playback_queue_play_next">Volgende afspelen</string>
<string name="playback_queue_add_item">Toevoegen aan wachtrij</string>
<string name="playback_queue_add_item">Toevoegen aan afspeellijst</string>
<string name="playback_queue_remove_item">Verwijder</string>
<string name="playback_queue_empty">De wachtrij is leeg</string>
<string name="playback_queue">Wachtrij</string>
<string name="playback_queue_empty">De afspeellijst is leeg</string>
<string name="playback_queue">Afspeellijst</string>
<string name="playback_shuffle">Shuffle</string>
<string name="favorites">Favorieten</string>
<string name="radios">Radio\'s</string>
@ -84,4 +84,5 @@
<string name="settings_experiments_restart_content">Beëindig en herstart de app om nieuwe instelling toe te passen</string>
<string name="settings_experiments_restart_title">Opnieuw opstarten vereist</string>
<string name="settings_experiments_description">Gebruik op eigen risico; kan de app bevriezen of crashen</string>
<string name="login_cleartext">Onversleutelde verbindingen toestaan (http)</string>
</resources>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="downloads_description">
<item quantity="one">Pobieranie %1$d nagrania</item>
<item quantity="few">Pobieranie %1$d nagrań</item>
<item quantity="many">Pobieranie %1$d nagrań</item>
</plurals>
<plurals name="playlist_description">
<item quantity="one">%1$d nagranie • %2$s</item>
<item quantity="few">%1$d nagrania • %2$s</item>
<item quantity="many">%1$d nagrań • %2$s</item>
</plurals>
<plurals name="album_count">
<item quantity="one">%d album</item>
<item quantity="few">%d albumy</item>
<item quantity="many">%d albumów</item>
</plurals>
<string name="filters_followed">Obserwowane</string>
<string name="filters_my_music">Moje utwory</string>
<string name="fiters_all">Wszystkie utwory</string>
<string name="playlist_added_to">Dodano do grajlisty %s</string>
<string name="playlist_add_to_create">Stwórz grajlistę</string>
<string name="playlist_add_to_new">Nowa grajlista…</string>
<string name="playlist_add_to">Dodaj do grajlisty</string>
<string name="logout_content">Czy na pewno chcesz się wylogować z tego konta Funkwhale\?</string>
<string name="radio_less_listened_description">Posłuchaj tego, na co zwykle nie masz czasu.</string>
<string name="radio_less_listened_title">Rzadziej słuchane</string>
<string name="radio_random_description">Może los przyniesie coś nowego\?</string>
<string name="radio_random_title">Losowe</string>
<string name="radio_favorites_description">Ulubione utwory na okrągło.</string>
<string name="radio_your_content_description">Korzysta z Twoich własnych zasobów</string>
<string name="radio_your_content_title">Twoje utowry</string>
<string name="radio_user_radios">Dodane przez Ciebie</string>
<string name="radio_instance_radios">Radio instancji</string>
<string name="radio_playback_error">Próba odtworzenia radia skończyła się błędem</string>
<string name="track_info_details_track_instance">Źródło</string>
<string name="track_info_details_track_bitrate">Próbkowanie</string>
<string name="track_info_details_track_position">Kolejność w albumie</string>
<string name="track_info_details_title">Dane utworu</string>
<string name="track_info_details">Informacje</string>
<string name="alt_track_info">Informacje o utworze</string>
<string name="alt_artist_art">Ikona artysty</string>
<string name="playback_media_controls_description">Powiadomienie z przyciskami do pauzowania i przełączania utworów</string>
<string name="playback_media_controls">Kontrola odtwarzania</string>
<string name="login_error_hostname_https">Błąd szyfrowanego połączenia (HTTPS)</string>
<string name="playlists">Grajlisty</string>
<string name="settings_play_order_in_order_summary">Wolisz słuchać albumów w domyślnej kolejności</string>
<string name="settings_play_order_shuffle_summary">Wolisz słuchać albumów w losowej kolejności</string>
<string name="search_no_results">Niczego nie znaleziono</string>
<string name="search_welcome">Wpisz wyszukiwaną nazwę i wciśnij enter</string>
<string name="login_error_userinfo">Nie znaleziono takiego użytkownika</string>
<string name="login_hostname">Adres WWW</string>
<string name="login_welcome">Wypełnij dane swojej instancji (serwera) Funkwhale</string>
<string name="filters">Filtry</string>
<string name="logout_title">Wyloguj się</string>
<string name="track_info_details_track_duration">Długość</string>
<string name="track_info_details_track_license">Licencja</string>
<string name="track_info_details_track_copyright">Prawa autorskie</string>
<string name="track_info_details_track_title">Tytuł utworu</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_artist">Artysta</string>
<string name="track_info_album">Przejdź do utworu</string>
<string name="track_info_artist">Przejdź do artysty</string>
<string name="alt_more_options">Ustawienia dodatkowe</string>
<string name="alt_album_cover">Okładka albumu</string>
<string name="playback_queue">Kolejka</string>
<string name="playback_shuffle">Wymieszaj</string>
<string name="playback_play">Odtwórz</string>
<string name="radios">Radio</string>
<string name="settings_crash_report_copied">Szczegóły ostatniej awarii zostały skopiowane do schowka</string>
<string name="settings_crash_report_description">Zostaną skopiowane tylko działania tego programu z ostatnich 5 minut przed awarią</string>
<string name="settings_crash_report_title">Skopiuj szczegóły awarii</string>
<string name="settings_information_repository_description">Otter stworzony przez Antoine\'a POPINEAU (apognu)</string>
<string name="settings_information_repository_title">Repozytorium</string>
<string name="settings_information">Informacje</string>
<string name="settings_experiments_restart_content">Do wprowadzenia zmian należy zrestartować program</string>
<string name="settings_experiments_restart_title">Wymagany restart</string>
<string name="settings_play_order_in_order">Odtwarzaj albumy po kolei</string>
<string name="settings_play_order_shuffle">Losuj albumy</string>
<string name="search_placeholder">Szukaj artystów, albumów i utworów</string>
<string name="title_oss_licences">Licencje bibliotek</string>
<string name="title_downloads">Pobrane</string>
<string name="toolbar_search">Szukaj</string>
<string name="alt_app_logo">Ikona programu</string>
<string name="error_playback">Utwór nie mógł zostać odtworzony</string>
<string name="control_next">Następny utwór</string>
<string name="control_previous">Poprzedni utwór</string>
<string name="control_toggle">Pauza</string>
<string name="manage_add_to_favorites">Dodaj do ulubionych</string>
<string name="playback_queue_save">Zapisz</string>
<string name="playback_queue_clear">Wyczyść</string>
<string name="playback_queue_download">Pobierz</string>
<string name="playback_queue_play_next">Odtwórz następne</string>
<string name="playback_queue_add_item">Dodaj do kolejki</string>
<string name="playback_queue_remove_item">Usuń</string>
<string name="playback_queue_empty">Kolejka jest pusta</string>
<string name="favorites">Ulubione</string>
<string name="tracks">Nagrania</string>
<string name="albums">Albumy</string>
<string name="artists">Artyści</string>
<string name="settings_logout">Wyloguj się</string>
<string name="settings_information_license_description">Licencja MIT</string>
<string name="settings_information_license_title">Licencja</string>
<string name="settings_version_title">Wersja</string>
<string name="settings_experiments_description">Mogą powodować mniejszą stabilność programu</string>
<string name="settings_experiments">Włącz funkcje eksperymentalne</string>
<string name="settings_night_mode_system_summary">Jasność będzie zależna od ustawień telefonu</string>
<string name="settings_night_mode_system">Wedle ustawień systemowych</string>
<string name="settings_night_mode_off_summary">Używasz jasnego trybu</string>
<string name="settings_night_mode_off">Wyłączony</string>
<string name="settings_night_mode_on_summary">Używasz trybu nocnego</string>
<string name="settings_night_mode_on">Włączony</string>
<string name="settings_night_mode">Tryb nocny</string>
<string name="settings_other">Inne</string>
<string name="settings_play_order">Kolejka odtwarzania</string>
<string name="settings_media_cache_size_summary">%d GB zostanie przeznaczone na utwory dostępne offline</string>
<string name="settings_media_cache_size">Wielkość pamięci podręcznej</string>
<string name="settings_media_quality_summary_size">Zostanie ograniczony przesył danych</string>
<string name="settings_media_quality_summary_quality">Usłyszysz dźwięk w najlepszej dostępnej jakości</string>
<string name="settings_media_quality_size">Najmniejszy rozmiar</string>
<string name="settings_media_quality_quality">Najlepsza jakość</string>
<string name="settings_media_quality">Jakość dźwięku</string>
<string name="settings_general">Ogólne</string>
<string name="title_settings">Ustawienia</string>
<string name="login_error_hostname">To nie jest prawidłowy adres URL</string>
<string name="login_logging_in">Trwa logowanie</string>
<string name="login_submit">Zaloguj się</string>
<string name="login_password">Hasło</string>
<string name="login_username">Nazwa użytkownika</string>
<string name="login_anonymous">Dostęp anonimowy</string>
<string name="login_cleartext">Zezwól na nieszyfrowany przesył (HTTP)</string>
</resources>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="login_error_hostname">Введите корректный URL</string>
<plurals name="downloads_description">
<item quantity="one">Загрузка %1$d трека…</item>
<item quantity="few">Загрузка %1$d треков…</item>
<item quantity="many">Загрузка %1$d треков…</item>
<item quantity="other">Загрузка %1$d треков…</item>
</plurals>
<string name="only_my_music">Только моя музыка</string>
<plurals name="playlist_description">
<item quantity="one">%1$d трек • %2$s</item>
<item quantity="few">%1$d трека • %2$s</item>
<item quantity="many">%1$d треков • %2$s</item>
</plurals>
<string name="logout_content">Выйти из этого инстанса Funkwhale\?</string>
<string name="logout_title">Выйти</string>
<string name="radio_less_listened_description">Проигрывает треки которые вы обычно не слушаете.</string>
<string name="radio_less_listened_title">Малопрослушиваемые</string>
<string name="radio_random_description">Полностью случайный выбор, может вы обнаружите для себя новые треки\?</string>
<string name="radio_random_title">Случайный порядок</string>
<string name="radio_favorites_description">Проигрывает ваши любимые мелодии по кругу и никогда не заканчивается.</string>
<string name="radio_your_content_description">Подобрано из вашей библиотеки</string>
<string name="radio_your_content_title">Ваш контент</string>
<string name="radio_user_radios">Пользовательское радио</string>
<string name="radio_instance_radios">Радио инстанса</string>
<string name="radio_playback_error">Не получилось воспроизвести это радио</string>
<string name="track_info_details_track_instance">Инстанс Funkwhale</string>
<string name="track_info_details_track_bitrate">Битрейт</string>
<string name="track_info_details_track_position">Позиция в альбоме</string>
<string name="track_info_details_track_duration">Продолжительность</string>
<string name="track_info_details_track_license">Лицензия</string>
<string name="track_info_details_track_copyright">Авторские права</string>
<string name="track_info_details_track_title">Название трека</string>
<string name="track_info_details_album">Альбом</string>
<string name="track_info_details_artist">Исполнитель</string>
<string name="track_info_details_title">Информация о треке</string>
<string name="track_info_details">Информация</string>
<string name="track_info_album">Перейти к альбому</string>
<string name="track_info_artist">Перейти к исполнителю</string>
<string name="alt_track_info">Информация о треке</string>
<string name="alt_more_options">Больше параметров</string>
<string name="alt_album_cover">Обложка альбома</string>
<string name="alt_artist_art">Изображение исполнителя</string>
<string name="alt_app_logo">Иконка приложения</string>
<plurals name="album_count">
<item quantity="one">%d альбом</item>
<item quantity="few">%d альбома</item>
<item quantity="many">%d альбомов</item>
</plurals>
<string name="error_playback">Не получилось воспроизвести</string>
<string name="control_next">Следующий трек</string>
<string name="control_previous">Прошлый трек</string>
<string name="control_toggle">Переключить воспроизведение</string>
<string name="manage_add_to_favorites">Добавить в избранное</string>
<string name="playback_queue_download">Загрузить</string>
<string name="playback_queue_play_next">Играть следующим</string>
<string name="playback_queue_add_item">Добавить в очередь воспроизведения</string>
<string name="playback_queue_remove_item">Удалить из очереди воспроизведения</string>
<string name="playback_queue_empty">Ваша очередь воспроизведения пуста</string>
<string name="playback_queue">Очередь воспроизведения</string>
<string name="playback_shuffle">Перемешать</string>
<string name="playback_media_controls_description">Управление воспроизведением медиа</string>
<string name="playback_media_controls">Управление медиа</string>
<string name="favorites">Любимые</string>
<string name="radios">Радио</string>
<string name="playlists">Плейлисты</string>
<string name="tracks">Треки</string>
<string name="albums">Альбомы</string>
<string name="artists">Исполнители</string>
<string name="settings_logout">Выйти</string>
<string name="settings_crash_report_copied">Последний отчёт о сбое скопирован в ваш буфер обмена</string>
<string name="settings_crash_report_description">Будут собраны только логи Otter за последние 5 минут до сбоя</string>
<string name="settings_crash_report_title">Скопировать журнал сбоев</string>
<string name="settings_information_license_description">Лицензия MIT</string>
<string name="settings_information_license_title">Лицензия</string>
<string name="settings_version_title">Версия</string>
<string name="settings_information_repository_description">Otter создан Antoine POPINEAU (apognu)</string>
<string name="settings_information_repository_title">Репозиторий</string>
<string name="settings_information">Информация</string>
<string name="settings_experiments_restart_content">Закройте и запустите приложение, чтобы изменения применились</string>
<string name="settings_experiments_restart_title">Требуется перезагрузка приложения</string>
<string name="settings_experiments_description">Используйте на свой страх и риск, приложение может подтормаживать или сбоить.</string>
<string name="settings_experiments">Экспериментальные возможности</string>
<string name="settings_night_mode_system_summary">Тёмный режим будет следовать настройкам системы</string>
<string name="settings_night_mode_system">Следовать настройкам системы</string>
<string name="settings_night_mode_off_summary">Светлый режим всегда будет включен</string>
<string name="settings_night_mode_off">Всегда выключен (светлый режим)</string>
<string name="settings_night_mode_on_summary">Темный режим всегда будет включен</string>
<string name="settings_night_mode_on">Всегда включён (тёмный режим)</string>
<string name="settings_night_mode">Тёмный режим</string>
<string name="settings_other">Другие</string>
<string name="settings_media_cache_size_summary">%d ГБ будет использовано для сохранения треков для оффлайн воспроизведения</string>
<string name="settings_media_cache_size">Размер медиакеша</string>
<string name="settings_media_quality_summary_size">Будет проиграна наихудшая версия</string>
<string name="settings_media_quality_summary_quality">Будет проиграна наилучшая версия</string>
<string name="settings_media_quality_size">Наихудшее качество</string>
<string name="settings_media_quality_quality">Лучшее качество</string>
<string name="settings_media_quality">Качество медиа</string>
<string name="settings_general">Основные</string>
<string name="search_no_results">По вашему запросу ничего не найдено</string>
<string name="search_welcome">Введите ваш поисковый запрос и нажмите Enter для поиска в вашей коллекции</string>
<string name="search_placeholder">Поиск исполнителей, альбомов и треков</string>
<string name="title_oss_licences">Лицензии открытого исходного кода</string>
<string name="title_settings">Настройки</string>
<string name="title_downloads">Загрузки</string>
<string name="toolbar_search">Поиск</string>
<string name="login_error_userinfo">Мы не смогли получить информацию о вашем аккаунте</string>
<string name="login_error_hostname_https">Имя хоста Funkwhale должно быть защищено с помощью HTTPS</string>
<string name="login_logging_in">Происходит вход…</string>
<string name="login_submit">Войти</string>
<string name="login_password">Пароль</string>
<string name="login_username">Имя пользователя</string>
<string name="login_anonymous">Анонимная аутентификация</string>
<string name="login_cleartext">Разрешить незашифрованный тарфик (HTTP)</string>
<string name="login_hostname">Доменное имя</string>
<string name="login_welcome">Введите данные вашего инстанса Funkwhale для доступа к контенту</string>
<string name="playlist_add_to_new">Новый плейлист…</string>
<string name="filters_followed">Подписки на контент</string>
<string name="filters_my_music">Моя музыка</string>
<string name="fiters_all">Вся музыка</string>
<string name="filters">Фильтры</string>
<string name="playlist_added_to">Добавлено в плейлист “%s“</string>
<string name="playlist_add_to_create">Создать плейлист</string>
<string name="playlist_add_to">Добавить в плейлист</string>
<string name="playback_queue_save">Сохранить</string>
<string name="playback_queue_clear">Очистить</string>
<string name="playback_play">Играть</string>
<string name="settings_play_order_in_order_summary">Предпочитаемый порядок проигрывания альбомов</string>
<string name="settings_play_order_in_order">Проигрывать альбомы в порядке</string>
<string name="settings_play_order_shuffle_summary">Перемешивать трэки в альбомах</string>
<string name="settings_play_order_shuffle">Перемешать альбомы</string>
<string name="settings_play_order">Предпочтительный порядок проигрывания</string>
</resources>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="filters">පෙරහන්</string>
<string name="title_oss_licences">විවෘත මූලාශ්‍ර බලපත්‍ර</string>
<string name="title_settings">සැකසුම්</string>
<string name="title_downloads">බාගැනීම්</string>
<string name="toolbar_search">සොයන්න</string>
<string name="login_logging_in">පිවිසෙමින්</string>
<string name="login_submit">පිවිසෙන්න</string>
<string name="login_password">මුර පදය</string>
<string name="login_username">පරිශීලක නාමය</string>
</resources>

View File

@ -1,21 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="downloads_description">
<item quantity="one">下载 %1$d 曲目</item>
<item quantity="other"></item>
<item quantity="other">下载 %1$d 曲目…</item>
</plurals>
<string name="only_my_music">只有我的音乐</string>
<plurals name="playlist_description">
<item quantity="one">%1$d 曲目 • %2$s</item>
<item quantity="other"></item>
<item quantity="other"/>
</plurals>
<string name="logout_content">是否确实要退出此 Funkwhale 实例?</string>
<string name="logout_content">退出此 Funkwhale 实例?</string>
<string name="logout_title">登出</string>
<string name="radio_less_listened_description">听一些你通常不听的曲目。是时候恢复一些平衡了。</string>
<string name="radio_less_listened_title">少听</string>
<string name="radio_less_listened_description">听一些你通常不听的曲目。</string>
<string name="radio_less_listened_title">听的最少的</string>
<string name="radio_random_description">完全随机挑选,也许你会发现新的东西?</string>
<string name="radio_random_title">随机</string>
<string name="radio_playback_error">尝试播放此收音机时出错</string>
<string name="radio_playback_error">无法播放此电台</string>
<string name="track_info_details_track_instance">Funkwhale 实例</string>
<string name="track_info_details_track_bitrate">比特率</string>
<string name="track_info_details_track_position">专辑位置</string>
@ -27,14 +26,14 @@
<string name="track_info_details">信息</string>
<string name="track_info_album">进入专辑</string>
<string name="track_info_artist">转到艺术家</string>
<string name="alt_track_info">有关音乐信息</string>
<string name="alt_track_info">音乐信息</string>
<string name="alt_more_options">更多操作</string>
<string name="alt_album_cover">专辑封面</string>
<string name="alt_artist_art">艺术家</string>
<string name="alt_app_logo">应用程序徽标</string>
<plurals name="album_count">
<item quantity="one">%d专辑</item>
<item quantity="other"></item>
<item quantity="other"/>
</plurals>
<string name="error_playback">无法播放此曲目</string>
<string name="control_next">下一曲</string>
@ -58,7 +57,7 @@
<string name="artists">艺术家</string>
<string name="settings_logout">登出</string>
<string name="settings_crash_report_copied">上次崩溃报告已复制到剪贴板</string>
<string name="settings_crash_report_description">从崩溃前的最后 5 分钟开始,仅包含 Otter 的日志</string>
<string name="settings_crash_report_description">收集从崩溃前的最后 5 分钟开始的 Otter日志</string>
<string name="settings_crash_report_title">复制崩溃日志</string>
<string name="settings_information_license_description">MIT许可证</string>
<string name="settings_information_license_title">许可证</string>
@ -66,10 +65,10 @@
<string name="settings_information_repository_description">Otter来自Antoine POPINEAU (apognu)</string>
<string name="settings_information_repository_title">代码库</string>
<string name="settings_information">信息</string>
<string name="settings_experiments_restart_content">终止并重新启动应用程序,以便此更改生效</string>
<string name="settings_experiments_restart_title">需要重新启动</string>
<string name="settings_experiments_description">自行承担使用风险,可能会冻结或崩溃应用</string>
<string name="settings_experiments">启用实验功能</string>
<string name="settings_experiments_restart_content">关闭并重新启动应用程序,以应用新设置</string>
<string name="settings_experiments_restart_title">应用需要重新启动</string>
<string name="settings_experiments_description">自行承担使用风险,可能会冻结或崩溃应用</string>
<string name="settings_experiments">实验功能</string>
<string name="settings_night_mode_system_summary">夜间模式将遵循系统设置</string>
<string name="settings_night_mode_system">遵循系统设置</string>
<string name="settings_night_mode_off_summary">明亮模式将始终打开</string>
@ -87,19 +86,44 @@
<string name="settings_media_quality">媒体质量</string>
<string name="settings_general">常规</string>
<string name="search_no_results">未找到查询结果</string>
<string name="search_welcome">在上面输入您的搜索词然后按Enter搜索您的收藏</string>
<string name="search_welcome">在上面输入您的搜索词,然后按 Enter 搜索您的收藏</string>
<string name="search_placeholder">搜索艺术家、专辑、曲目</string>
<string name="title_oss_licences">开放源码许可证</string>
<string name="title_oss_licences">自由授权条款</string>
<string name="title_settings">设置</string>
<string name="title_downloads">下载</string>
<string name="toolbar_search">搜索</string>
<string name="login_error_hostname_https">应通过HTTPS保护Funkwhale主机名</string>
<string name="login_error_hostname">这不能理解为有效的 URL</string>
<string name="login_logging_in">登录中</string>
<string name="login_error_hostname">请先输入一个有效的 URL</string>
<string name="login_logging_in">登录中</string>
<string name="login_submit">登录</string>
<string name="login_password">密码</string>
<string name="login_username">用户名</string>
<string name="login_anonymous">匿名身份验证</string>
<string name="login_hostname">主机名称</string>
<string name="login_welcome">请输入您的Funkwhale实例的详细信息以访问其内容</string>
<string name="radio_favorites_description">在永无止境的幸福循环中播放你最喜欢的曲子。</string>
<string name="radio_your_content_description">从您自己的图书馆中挑选</string>
<string name="radio_your_content_title">您的内容</string>
<string name="radio_user_radios">用户电台</string>
<string name="radio_instance_radios">实例电台</string>
<string name="track_info_details_track_license">许可证</string>
<string name="track_info_details_track_copyright">版权所有</string>
<string name="login_error_userinfo">我们无法获取用户信息</string>
<string name="login_cleartext">允许明文流量HTTP</string>
<string name="filters_followed">跟随内容</string>
<string name="filters_my_music">我的音乐</string>
<string name="fiters_all">所有的音乐</string>
<string name="filters">过滤</string>
<string name="playback_queue_clear">清除</string>
<string name="playback_play">播放</string>
<string name="settings_play_order_in_order_summary">你喜欢按顺序播放专辑</string>
<string name="settings_play_order_in_order">按顺序播放专辑</string>
<string name="settings_play_order_shuffle_summary">你喜欢随机播放专辑曲目吗</string>
<string name="settings_play_order_shuffle">专辑重新排序</string>
<string name="settings_play_order">首选播放顺序</string>
<string name="playlist_added_to">添加到播放列表 %s</string>
<string name="playlist_add_to_create">创建播放列表</string>
<string name="playlist_add_to_new">新播放列表…</string>
<string name="playlist_add_to">加入播放列表</string>
<string name="playback_queue_save">保存</string>
</resources>

View File

@ -10,6 +10,16 @@
<item>size</item>
</array>
<array name="play_orders">
<item>@string/settings_play_order_shuffle</item>
<item>@string/settings_play_order_in_order</item>
</array>
<array name="play_orders_values">
<item>shuffle</item>
<item>in_order</item>
</array>
<array name="night_mode">
<item>@string/settings_night_mode_on</item>
<item>@string/settings_night_mode_off</item>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="surface">@android:color/background_light</color>
<color name="elevatedSurface">@android:color/background_light</color>
<color name="colorPrimary">#327eae</color>
<color name="colorPrimaryDark">#3d3e40</color>

View File

@ -1,113 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Otter</string>
<string name="login_welcome">Please enter the details of your Funkwhale instance to access its content</string>
<string name="login_hostname">Host name</string>
<string name="login_cleartext">Allow cleartext traffic (HTTP)</string>
<string name="login_anonymous">Anonymous authentication</string>
<string name="login_username">Username</string>
<string name="login_password">Password</string>
<string name="login_submit">Log in</string>
<string name="login_logging_in">Logging in</string>
<string name="login_error_hostname">This could not be understood as a valid URL</string>
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
<string name="login_error_userinfo">We could not retrieve information about your user</string>
<string name="toolbar_search">Search</string>
<string name="title_downloads">Downloads</string>
<string name="title_settings">Settings</string>
<string name="title_oss_licences">Open source licences</string>
<string name="search_placeholder">Search artists, albums and tracks</string>
<string name="search_welcome">Enter your search terms above and hit enter to search your collection</string>
<string name="search_no_results">No results were found for your query</string>
<string name="settings_general">General</string>
<string name="settings_media_quality">Media quality</string>
<string name="settings_media_quality_quality">Best quality</string>
<string name="settings_media_quality_size">Smallest size</string>
<string name="settings_media_quality_summary_quality">Best available version will be played</string>
<string name="settings_media_quality_summary_size">Smallest available track will be played</string>
<string name="settings_media_cache_size">Media cache size</string>
<string name="settings_media_cache_size_summary">%d GB will be used to store tracks for offline playback</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Dark mode</string>
<string name="settings_night_mode_on">Always on (dark mode)</string>
<string name="settings_night_mode_on_summary">Dark mode will always be on</string>
<string name="settings_night_mode_off">Always off (light mode)</string>
<string name="settings_night_mode_off_summary">Light mode will always be on</string>
<string name="settings_night_mode_system">Follow system settings</string>
<string name="settings_night_mode_system_summary">Night mode will follow system settings</string>
<string name="settings_experiments">Enable experimental features</string>
<string name="settings_experiments_description">Use at your own risks, may freeze or crash the app</string>
<string name="settings_experiments_restart_title">Restart required</string>
<string name="settings_experiments_restart_content">Please kill and restart the app in order for this change to take effect</string>
<string name="settings_information">Information</string>
<string name="settings_information_repository_title">Repository</string>
<string name="settings_information_repository_description">Otter by Antoine POPINEAU (apognu)</string>
<string name="settings_version_title">Version</string>
<string name="settings_information_license_title">License</string>
<string name="settings_information_license_description">MIT license</string>
<string name="settings_crash_report_title">Copy crash logs</string>
<string name="settings_crash_report_description">Only Otter\'s logs from the last 5 minutes up until the crash will be collected</string>
<string name="settings_crash_report_copied">Last crash report was copied to your clipboard</string>
<string name="settings_logout">Sign out</string>
<string name="artists">Artists</string>
<string name="albums">Albums</string>
<string name="tracks">Tracks</string>
<string name="playlists">Playlists</string>
<string name="radios">Radios</string>
<string name="favorites">Favorites</string>
<string name="playback_media_controls">Media controls</string>
<string name="playback_media_controls_description">Control media playback</string>
<string name="playback_shuffle">Shuffle</string>
<string name="playback_queue">Queue</string>
<string name="playback_queue_empty">Your queue is empty</string>
<string name="playback_queue_remove_item">Remove</string>
<string name="playback_queue_add_item">Add to queue</string>
<string name="playback_queue_play_next">Play next</string>
<string name="playback_queue_download">Download</string>
<string name="manage_add_to_favorites">Add to favorites</string>
<string name="control_toggle">Toggle playback</string>
<string name="control_previous">Previous track</string>
<string name="control_next">Next track</string>
<string name="error_playback">This track could not be played</string>
<plurals name="album_count">
<item quantity="one">%d album</item>
<item quantity="other">%d albums</item>
</plurals>
<string name="alt_app_logo">Application logo</string>
<string name="alt_artist_art">Artist art</string>
<string name="alt_album_cover">Album cover</string>
<string name="alt_more_options">More options</string>
<string name="alt_track_info">Information about track</string>
<string name="track_info_artist">Go to artist</string>
<string name="track_info_album">Go to album</string>
<string name="track_info_details">Information</string>
<string name="track_info_details_title">Track details</string>
<string name="track_info_details_artist">Artist</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_track_title">Track title</string>
<string name="track_info_details_track_duration">Duration</string>
<string name="track_info_details_track_position">Album position</string>
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_instance">Funkwhale instance</string>
<string name="radio_playback_error">There was an error while trying to play this radio</string>
<string name="radio_instance_radios">Instance radios</string>
<string name="radio_user_radios">User radios</string>
<string name="radio_your_content_title">Your content</string>
<string name="radio_your_content_description">Picks from your own libraries</string>
<string name="radio_favorites_description"> Play your favorites tunes in a never-ending happiness loop.</string>
<string name="radio_random_title">Random</string>
<string name="radio_random_description">Totally random picks, maybe you\'ll discover new things?</string>
<string name="radio_less_listened_title">Less listened</string>
<string name="radio_less_listened_description">Listen to tracks you usually don\'t. It\'s time to restore some balance.</string>
<string name="logout_title">Sign out</string>
<string name="logout_content">Are you sure you want to sign out of this Funkwhale instance\?</string>
<plurals name="playlist_description">
<item quantity="one">%1$d track • %2$s"</item>
<item quantity="other">%1$d tracks • %2$s"</item>
</plurals>
<string name="only_my_music">Only my music</string>
<plurals name="downloads_description">
<item quantity="one">Downloading %1$d track</item>
<item quantity="other">Downloading %1$d tracks</item>
</plurals>
</resources>
<string name="app_name" translatable="false">Otter</string>
<string name="login_welcome">Please enter details for a Funkwhale instance to access its content</string>
<string name="login_hostname">Hostname</string>
<string name="login_cleartext">Allow cleartext traffic (HTTP)</string>
<string name="login_anonymous">Anonymous authentication</string>
<string name="login_username">Username</string>
<string name="login_password">Password</string>
<string name="login_submit">Log in</string>
<string name="login_logging_in">Logging in…</string>
<string name="login_error_hostname">Enter a valid URL first</string>
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
<string name="login_error_userinfo">Could not fetch user info</string>
<string name="toolbar_search">Search</string>
<string name="title_downloads">Downloads</string>
<string name="title_settings">Settings</string>
<string name="title_oss_licences">Libre licences</string>
<string name="search_placeholder">Search artists, albums and tracks</string>
<string name="search_welcome">Enter your search above and hit enter to search in your collection</string>
<string name="search_no_results">No results</string>
<string name="settings_general">General</string>
<string name="settings_media_quality">Media quality</string>
<string name="settings_media_quality_quality">Best quality</string>
<string name="settings_media_quality_size">Smallest size</string>
<string name="settings_media_quality_summary_quality">Best available version will be played</string>
<string name="settings_media_quality_summary_size">Smallest available track will be played</string>
<string name="settings_media_cache_size">Cache size for media</string>
<string name="settings_media_cache_size_summary">%d GB will be used to store tracks for offline playback</string>
<string name="settings_play_order">Preferred playback order</string>
<string name="settings_play_order_shuffle">Shuffle albums</string>
<string name="settings_play_order_shuffle_summary">You prefer shuffling album tracks</string>
<string name="settings_play_order_in_order">Play albums in order</string>
<string name="settings_play_order_in_order_summary">You prefer playing albums in order</string>
<string name="settings_other">Other</string>
<string name="settings_night_mode">Dark mode</string>
<string name="settings_night_mode_on">Always on (dark mode)</string>
<string name="settings_night_mode_on_summary">Dark mode will always be on</string>
<string name="settings_night_mode_off">Always off (light mode)</string>
<string name="settings_night_mode_off_summary">Light mode will always be on</string>
<string name="settings_night_mode_system">Follow system</string>
<string name="settings_night_mode_system_summary">Night mode will follow the system</string>
<string name="settings_experiments">Experimental features</string>
<string name="settings_experiments_description">Use at your own risks. It may freeze or crash the app.</string>
<string name="settings_experiments_restart_title">App restart required</string>
<string name="settings_experiments_restart_content">Close and restart the app to use the new settings</string>
<string name="settings_information">Info</string>
<string name="settings_information_repository_title">Repository</string>
<string name="settings_information_repository_description">Otter by Antoine POPINEAU (apognu)</string>
<string name="settings_version_title">Version</string>
<string name="settings_information_license_title">License</string>
<string name="settings_information_license_description">MIT license</string>
<string name="settings_crash_report_title">Copy crash logs</string>
<string name="settings_crash_report_description">Only Otter logs from 5 minutes before crash will be collected</string>
<string name="settings_crash_report_copied">Last crash report copied to your clipboard</string>
<string name="settings_logout">Sign out</string>
<string name="artists">Artists</string>
<string name="albums">Albums</string>
<string name="tracks">Tracks</string>
<string name="playlists">Playlists</string>
<string name="radios">Radios</string>
<string name="favorites">Favorites</string>
<string name="playback_media_controls">Media controls</string>
<string name="playback_media_controls_description">Control media playback</string>
<string name="playback_play">Play</string>
<string name="playback_shuffle">Shuffle</string>
<string name="playback_queue">Queue</string>
<string name="playback_queue_empty">Your queue is empty</string>
<string name="playback_queue_remove_item">Remove</string>
<string name="playback_queue_add_item">Add to queue</string>
<string name="playback_queue_play_next">Play next</string>
<string name="playback_queue_download">Download</string>
<string name="playback_queue_clear">Clear</string>
<string name="playback_queue_save">Save</string>
<string name="manage_add_to_favorites">Add to favorites</string>
<string name="control_toggle">Toggle playback</string>
<string name="control_previous">Previous track</string>
<string name="control_next">Next track</string>
<string name="error_playback">Could not play this track</string>
<plurals name="album_count">
<item quantity="one">%d album</item>
<item quantity="other">%d albums</item>
</plurals>
<string name="alt_app_logo">App logo</string>
<string name="alt_artist_art">Artist art</string>
<string name="alt_album_cover">Album cover</string>
<string name="alt_more_options">More options</string>
<string name="alt_track_info">Track info</string>
<string name="track_info_artist">Go to artist</string>
<string name="track_info_album">Go to album</string>
<string name="track_info_details">Info</string>
<string name="track_info_details_title">Track details</string>
<string name="track_info_details_artist">Artist</string>
<string name="track_info_details_album">Album</string>
<string name="track_info_details_track_title">Track title</string>
<string name="track_info_details_track_copyright">Copyright</string>
<string name="track_info_details_track_license">License</string>
<string name="track_info_details_track_duration">Duration</string>
<string name="track_info_details_track_position">Album position</string>
<string name="track_info_details_track_bitrate">Bitrate</string>
<string name="track_info_details_track_instance">Funkwhale instance</string>
<string name="radio_playback_error">Could not play this radio station</string>
<string name="radio_instance_radios">Instance radios</string>
<string name="radio_user_radios">User radios</string>
<string name="radio_your_content_title">Your content</string>
<string name="radio_your_content_description">Picks from your own libraries</string>
<string name="radio_favorites_description">Play your favorites in a never-ending happiness loop.</string>
<string name="radio_random_title">Random</string>
<string name="radio_random_description">Totally random picks. Maybe you\'ll discover new things\?</string>
<string name="radio_less_listened_title">Less played</string>
<string name="radio_less_listened_description">Listen to tracks you usually don\'t.</string>
<string name="logout_title">Sign out</string>
<string name="logout_content">Sign out of this Funkwhale instance\?</string>
<plurals name="playlist_description">
<item quantity="one">%1$d track • %2$s"</item>
<item quantity="other">%1$d tracks • %2$s"</item>
</plurals>
<string name="playlist_add_to">Add to playlist</string>
<string name="playlist_add_to_new">New playlist…</string>
<string name="playlist_add_to_create">Create playlist</string>
<string name="playlist_added_to">Added to “%s” playlist</string>
<string name="filters">Filters</string>
<string name="fiters_all">All music</string>
<string name="filters_my_music">My music</string>
<string name="filters_followed">Followed content</string>
<plurals name="downloads_description">
<item quantity="one">Downloading %1$d track…</item>
<item quantity="other">Downloading %1$d tracks…</item>
</plurals>
</resources>

View File

@ -69,7 +69,13 @@
<item name="android:drawableTint" tools:targetApi="m">@android:color/white</item>
<item name="android:tint">@android:color/white</item>
<item name="android:popupTheme">@style/ThemeOverlay.AppCompat.DayNight</item>
<item name="actionBarPopupTheme">@style/AppTheme.PopupMenu</item>
<item name="popupTheme">@style/AppTheme.PopupMenu</item>
</style>
<style name="AppTheme.PopupMenu" parent="ThemeOverlay.MaterialComponents.Toolbar.Primary">
<item name="android:drawableTint" tools:targetApi="m">@color/blackWhileLight</item>
<item name="android:tint">@color/blackWhileLight</item>
</style>
<style name="AppTheme.FloatingBottomSheet" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
@ -99,4 +105,14 @@
<item name="android:textColor">@android:color/white</item>
</style>
<style name="AppTheme.IconButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="iconPadding">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:paddingLeft">12dp</item>
<item name="android:paddingRight">12dp</item>
<item name="android:minWidth">43dp</item>
<item name="android:minHeight">43dp</item>
<item name="android:background">@android:color/transparent</item>
</style>
</resources>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates
src="user"
tools:ignore="AcceptsUserCertificates" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@ -22,6 +22,14 @@
app:showSeekBarValue="true"
app:updatesContinuously="true" />
<ListPreference
android:defaultValue="shuffle"
android:entries="@array/play_orders"
android:entryValues="@array/play_orders_values"
android:icon="@drawable/play"
android:key="play_order"
android:title="@string/settings_play_order" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_other">

View File

@ -0,0 +1,8 @@
Various UI and performance improvements, some bug fixes and the following new features:
* Initial support for Funkwhale 1.0
* Always more translations (thanks to all contributors)
* Keep downloads when disconnected
* Support for cleartext connections and user-configured CAs
* Changed item ordering to be more friendly
* Enhanced metadata broadcast to work across devices

View File

@ -0,0 +1,8 @@
Diverses améliorations des performances et de l'UI, quelques corrections de bugs et ces nouvelles fonctionnalités :
* Support initial de Funkwhale 1.0
* Ajout et amélioration des traductions (merci à tous les contributeurs)
* Conservation des téléchargements lors des déconnexions
* Support des connexions HTTP et utilisant des CA utilisateurs
* Changement de l'ordre d'affichage des éléments
* Amélioration de la diffusion des métadonnées de lecture pour supporter plus d'appareils

Binary file not shown.