Compare commits

...

163 Commits

Author SHA1 Message Date
Nite
1f9de0be7e Merge branch 'feature/cache-fix' into 'develop'
Fixed cache space cleanup

Closes #1284

See merge request ultrasonic/ultrasonic!1206
2025-04-14 16:47:12 +00:00
Nite
550c486077 Fixed cache space cleanup 2025-04-14 16:47:11 +00:00
Nite
006c554456 Merge branch 'feature/bunch-of-fixes' into 'develop'
Fixed DI to make the widget work

See merge request ultrasonic/ultrasonic!1204
2025-04-11 17:48:08 +00:00
Nite
6443881193 Fixed DI to make the widget work 2025-04-11 17:48:08 +00:00
Nite
a98693bbdb Merge branch 'feature/dependency-update' into 'develop'
Updated libraries to latest version

See merge request ultrasonic/ultrasonic!1202
2025-04-11 08:07:16 +00:00
Nite
03e555eb27
Updated libraries to latest version 2025-04-11 09:58:16 +02:00
Nite
ef73d89491 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request ultrasonic/ultrasonic!1196
2025-04-08 16:05:55 +00:00
josé m
4ad789d20b
Translated using Weblate (Galician)
Currently translated at 100.0% (425 of 425 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-04-04 15:47:10 +02:00
Nite
7c163c237a Merge branch 'feature/min-sdk-26' into 'develop'
Updated Min SDK to 26

Closes #1319

See merge request ultrasonic/ultrasonic!1195
2025-04-04 12:55:13 +00:00
Nite
38432a3cdc Updated Min SDK to 26 2025-04-04 12:55:13 +00:00
Nite
53aee22794 Merge branch 'renovate/jackson-monorepo' into 'develop'
Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.18.3

See merge request ultrasonic/ultrasonic!1183
2025-04-04 10:11:01 +00:00
Nite
9880f15017 Merge branch 'renovate/major-koin' into 'develop'
Update koin to v4 (major)

See merge request ultrasonic/ultrasonic!1194
2025-04-04 09:36:19 +00:00
Renovate Bot
0fc836cf83 Update koin to v4 (major) 2025-04-04 09:36:19 +00:00
Nite
6eb20d1c0b Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.13

See merge request ultrasonic/ultrasonic!1186
2025-04-04 09:16:51 +00:00
Renovate Bot
1f9c9505d7 Update dependency gradle to v8.13 2025-04-04 09:16:50 +00:00
Nite
2487d24371 Merge branch 'renovate/apachecodecs' into 'develop'
Update dependency commons-codec:commons-codec to v1.18.0

See merge request ultrasonic/ultrasonic!1185
2025-04-04 08:48:20 +00:00
Nite
c27b3759b1 Merge branch 'renovate/junit5-monorepo' into 'develop'
Update dependency org.junit.vintage:junit-vintage-engine to v5.12.1

See merge request ultrasonic/ultrasonic!1188
2025-04-04 08:45:58 +00:00
Nite
481f4e0347 Merge branch 'renovate/mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v5.4.0

See merge request ultrasonic/ultrasonic!1189
2025-04-04 08:31:59 +00:00
Renovate Bot
a9b47f5aef Update dependency org.mockito.kotlin:mockito-kotlin to v5.4.0 2025-04-04 08:31:59 +00:00
Renovate Bot
b53bb38631 Update dependency org.junit.vintage:junit-vintage-engine to v5.12.1 2025-04-04 08:31:27 +00:00
Renovate Bot
a2a42e06b0 Update dependency commons-codec:commons-codec to v1.18.0 2025-04-04 08:31:24 +00:00
Nite
adf5b95243 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.16.1

See merge request ultrasonic/ultrasonic!1190
2025-04-04 08:18:46 +00:00
Nite
3d82b3935d Merge branch 'renovate/robolectric' into 'develop'
Update dependency org.robolectric:robolectric to v4.14.1

See merge request ultrasonic/ultrasonic!1191
2025-04-04 08:14:40 +00:00
Nite
7ee66e51e8 Merge branch 'renovate/kotlinx-coroutines-monorepo' into 'develop'
Update kotlinx-coroutines monorepo to v1.10.1

See merge request ultrasonic/ultrasonic!1192
2025-04-04 08:00:49 +00:00
Nite
b126cd594a Merge branch 'renovate/retrofit-monorepo' into 'develop'
Update retrofit monorepo to v2.11.0

See merge request ultrasonic/ultrasonic!1193
2025-04-04 07:52:58 +00:00
Renovate Bot
31f654ee2f Update retrofit monorepo to v2.11.0 2025-04-03 19:31:45 +00:00
Renovate Bot
42c8edc6e7 Update kotlinx-coroutines monorepo to v1.10.1 2025-04-03 19:31:41 +00:00
Renovate Bot
8f7c7c33ff Update dependency org.robolectric:robolectric to v4.14.1 2025-04-03 18:31:59 +00:00
Renovate Bot
c105c0d02d Update dependency org.mockito:mockito-core to v5.16.1 2025-04-03 18:31:51 +00:00
Nite
e74fb3795a Merge branch 'renovate/rxjava' into 'develop'
Update dependency io.reactivex.rxjava3:rxjava to v3.1.10

See merge request ultrasonic/ultrasonic!1182
2025-04-03 17:50:54 +00:00
Nite
80923ca8ac Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.8

See merge request ultrasonic/ultrasonic!1179
2025-04-03 17:44:18 +00:00
Nite
f2fa530047 Merge branch 'renovate/koin' into 'develop'
Update koin to v3.5.6

See merge request ultrasonic/ultrasonic!1172
2025-04-03 17:36:58 +00:00
Óscar García Amor
4cb6ab031b
Fixes translations linting 2025-04-03 09:25:00 +02:00
Óscar García Amor
2d224e5f84
Merge remote-tracking branch 'weblate/develop' into develop 2025-04-03 09:08:13 +02:00
Adolfo Jayme Barrientos
5a653bebb2
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 09:06:40 +02:00
ssantos
5566cb05ab
Translated using Weblate (Portuguese)
Currently translated at 97.8% (413 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 09:02:00 +02:00
Adolfo Jayme Barrientos
7ee7aa23be
Translated using Weblate (Japanese)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ja/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
5a2ae50c74
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
62d7e6bb6d
Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.1% (351 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nb_NO/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
5266fc0b0f
Translated using Weblate (Spanish)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
612925bc7d
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 68.0% (287 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
81499a1b37
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/
2025-04-03 08:58:30 +02:00
Adolfo Jayme Barrientos
27fc8462d1
Translated using Weblate (Russian)
Currently translated at 82.2% (347 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ru/
2025-04-03 08:58:28 +02:00
Adolfo Jayme Barrientos
f3e205a452
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:58:04 +02:00
Adolfo Jayme Barrientos
84374f8c13
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2025-04-03 08:58:04 +02:00
Adolfo Jayme Barrientos
5e8bf249f8
Translated using Weblate (Polish)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pl/
2025-04-03 08:58:04 +02:00
Adolfo Jayme Barrientos
3913e676b7
Translated using Weblate (Dutch)
Currently translated at 95.0% (401 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nl/
2025-04-03 08:58:02 +02:00
Adolfo Jayme Barrientos
a70ee39403
Translated using Weblate (Italian)
Currently translated at 63.0% (266 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/it/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
05bd9f0d2c
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
4d9388f8fe
Translated using Weblate (French)
Currently translated at 96.4% (407 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/fr/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
8acd7da959
Translated using Weblate (German)
Currently translated at 99.0% (418 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/de/
2025-04-03 08:57:21 +02:00
Adolfo Jayme Barrientos
c909848ed5
Translated using Weblate (Czech)
Currently translated at 75.5% (319 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/cs/
2025-04-03 08:57:18 +02:00
Adolfo Jayme Barrientos
e5c8e874e3
Translated using Weblate (English)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/en/
2025-04-03 08:56:27 +02:00
Paulo Schopf
9d5caed73d
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2025-04-03 08:56:27 +02:00
Walton Henry (WaltonH)
292086e9d6
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2025-04-03 08:56:20 +02:00
josé m
5639fcfb8b
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-04-03 08:54:39 +02:00
Paulo Schopf
ce39da4b79
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:54:39 +02:00
ssantos
4e89672d15
Translated using Weblate (Portuguese)
Currently translated at 99.5% (420 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:54:39 +02:00
ssantos
6725ef5a2c
Translated using Weblate (Portuguese)
Currently translated at 97.8% (413 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-04-03 08:54:39 +02:00
Renovate Bot
680cc90871 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.8 2025-04-02 20:31:32 +00:00
Nite
c19dc125a9 Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12.1.1

See merge request ultrasonic/ultrasonic!1166
2025-04-02 19:32:20 +00:00
Renovate Bot
be8fa3c0d1 Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.18.3 2025-04-02 19:31:22 +00:00
Renovate Bot
312a97d664 Update dependency io.reactivex.rxjava3:rxjava to v3.1.10 2025-04-02 19:31:16 +00:00
Renovate Bot
fca954102a Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12.1.1 2025-04-02 19:25:01 +00:00
Nite
9e078b4879 Merge branch 'feature/unified-rating' into 'develop'
Updated rating to be able to use the 5 star and heart rating together

Closes #440, #1231, and #1250

See merge request ultrasonic/ultrasonic!1133
2025-04-02 19:02:33 +00:00
Nite
46e85c27a2 Updated rating to be able to use the 5 star and heart rating together 2025-04-02 19:02:30 +00:00
Adolfo Jayme Barrientos
1124cef382
Translated using Weblate (Japanese)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ja/
2025-03-22 18:44:12 +01:00
Adolfo Jayme Barrientos
01ebf6e1aa
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
b65050ae85
Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.1% (351 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nb_NO/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
ddec1d7bdc
Translated using Weblate (Spanish)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
888d34aeb9
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 68.0% (287 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hant/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
0fb3d8aeca
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/zh_Hans/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
c56913733f
Translated using Weblate (Russian)
Currently translated at 82.2% (347 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/ru/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
76c61e1866
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2025-03-22 18:44:11 +01:00
Adolfo Jayme Barrientos
810bb9ddd1
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
68a2123293
Translated using Weblate (Polish)
Currently translated at 98.8% (417 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pl/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
4221b18e09
Translated using Weblate (Dutch)
Currently translated at 95.0% (401 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/nl/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
e7ed066eed
Translated using Weblate (Italian)
Currently translated at 63.0% (266 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/it/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
1301f7e116
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
63b064b021
Translated using Weblate (French)
Currently translated at 96.4% (407 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/fr/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
a9be77aa75
Translated using Weblate (German)
Currently translated at 99.0% (418 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/de/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
fc7dcc9f77
Translated using Weblate (Czech)
Currently translated at 75.5% (319 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/cs/
2025-03-22 18:44:10 +01:00
Adolfo Jayme Barrientos
48461f82a8
Translated using Weblate (English)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/en/
2025-03-22 18:44:09 +01:00
Paulo Schopf
81ea86fd01
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2024-10-31 15:24:51 +01:00
Walton Henry (WaltonH)
8803f4444e
Translated using Weblate (Hungarian)
Currently translated at 77.9% (329 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/hu/
2024-04-23 21:07:11 +02:00
Renovate Bot
38a97b2b91 Update koin to v3.5.6 2024-04-12 07:32:05 +00:00
josé m
6e7bcc4362
Translated using Weblate (Galician)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2024-02-20 19:02:07 +01:00
Paulo Schopf
c12224e811
Translated using Weblate (Portuguese)
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2024-01-09 13:06:11 +01:00
ssantos
b8ef3cd177
Translated using Weblate (Portuguese)
Currently translated at 99.5% (420 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2024-01-07 17:06:12 +00:00
ssantos
2de773e5de
Translated using Weblate (Portuguese)
Currently translated at 97.8% (413 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt/
2023-12-19 17:09:57 +00:00
birdbird
fe5b63ad1f Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request ultrasonic/ultrasonic!1165
2023-12-06 10:52:51 +00:00
Paulo Schopf
4aff5857fd
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (422 of 422 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/pt_BR/
2023-12-05 22:06:55 +00:00
birdbird
a0d26cb3e7 Merge branch 'renovate/media3' into 'develop'
Update media3 to v1.2.0

See merge request ultrasonic/ultrasonic!1153
2023-12-04 16:40:18 +00:00
tzugen
639ef03bce
Adapt to media 1.2.0 2023-12-04 17:33:18 +01:00
Renovate Bot
b855e4bbe7
Update media3 to v1.2.0 2023-12-04 17:32:37 +01:00
birdbird
b83e349f5c Merge branch 'refactor2' into 'develop'
Update copyright in recently edited files

See merge request ultrasonic/ultrasonic!1164
2023-12-04 15:19:09 +00:00
birdbird
abad0438e3 Update copyright in recently edited files 2023-12-04 15:19:09 +00:00
birdbird
30f02c7eac Merge branch 'room2' into 'develop'
Adapt to room changes

See merge request ultrasonic/ultrasonic!1163
2023-12-04 10:01:42 +00:00
tzugen
64f1c3e172
Adapt to room changes 2023-12-04 10:49:35 +01:00
birdbird
f7f1f40668 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.8.0

See merge request ultrasonic/ultrasonic!1161
2023-12-03 19:45:41 +00:00
birdbird
6519945c7b Merge branch 'renovate/major-ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12

See merge request ultrasonic/ultrasonic!1162
2023-12-03 19:45:20 +00:00
Renovate Bot
94979aeaab Update dependency org.jlleitschuh.gradle:ktlint-gradle to v12 2023-12-03 19:45:20 +00:00
Renovate Bot
37e43b73a7 Update dependency org.mockito:mockito-core to v5.8.0 2023-12-03 14:31:46 +00:00
birdbird
2cf2cf31c4 Merge branch 'exep' into 'develop'
Avoid two exceptions

See merge request ultrasonic/ultrasonic!1159
2023-12-03 14:12:11 +00:00
birdbird
9736ae451a Avoid two exceptions 2023-12-03 14:12:10 +00:00
birdbird
26331c1a07 Merge branch 'renovate/ksp' into 'develop'
Update dependency com.google.devtools.ksp to v1.9.21-1.0.15

See merge request ultrasonic/ultrasonic!1146
2023-12-03 14:06:06 +00:00
birdbird
35ffe9ef10 Merge branch 'renovate/room' into 'develop'
Update room to v2.6.1

See merge request ultrasonic/ultrasonic!1157
2023-12-03 14:06:01 +00:00
birdbird
976400d0e1 Merge branch 'renovate/mockitokotlin' into 'develop'
Update dependency org.mockito.kotlin:mockito-kotlin to v5.2.1

See merge request ultrasonic/ultrasonic!1160
2023-12-03 14:05:41 +00:00
Renovate Bot
d2ef76a2c5 Update dependency org.mockito.kotlin:mockito-kotlin to v5.2.1 2023-12-03 10:32:00 +00:00
Renovate Bot
c0926b1e13 Update room to v2.6.1 2023-12-03 10:31:56 +00:00
Renovate Bot
bf5d41ab30 Update dependency com.google.devtools.ksp to v1.9.21-1.0.15 2023-12-03 10:31:52 +00:00
birdbird
e2716a5965 Merge branch 'renovate/junit5-monorepo' into 'develop'
Update dependency org.junit.vintage:junit-vintage-engine to v5.10.1

See merge request ultrasonic/ultrasonic!1149
2023-12-03 09:31:41 +00:00
birdbird
8351f1dc0a Merge branch 'renovate/gradleplugin' into 'develop'
Update dependency com.android.tools.build:gradle to v8.2.0

See merge request ultrasonic/ultrasonic!1150
2023-12-03 09:31:40 +00:00
birdbird
90997d5f4c Merge branch 'renovate/activity' into 'develop'
Update dependency androidx.activity:activity-ktx to v1.8.1

See merge request ultrasonic/ultrasonic!1151
2023-12-03 09:31:31 +00:00
birdbird
747f071f2f Merge branch 'renovate/jackson' into 'develop'
Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.16.0

See merge request ultrasonic/ultrasonic!1152
2023-12-03 09:31:20 +00:00
birdbird
9e76308cf2 Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request ultrasonic/ultrasonic!1158
2023-12-03 09:30:45 +00:00
josé m
c452030fe1
Translated using Weblate (Galician)
Currently translated at 27.9% (119 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/gl/
2023-12-03 10:30:03 +01:00
birdbird
dbbeac6084 Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.5

See merge request ultrasonic/ultrasonic!1156
2023-12-03 09:29:52 +00:00
birdbird
d56ec198a1 Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.4

See merge request ultrasonic/ultrasonic!1155
2023-12-03 09:29:47 +00:00
birdbird
13238dfdc0 Merge branch 'renovate/kotlin-monorepo' into 'develop'
Update kotlin monorepo to v1.9.21

See merge request ultrasonic/ultrasonic!1154
2023-12-03 09:29:40 +00:00
Renovate Bot
8063814bdc Update dependency com.android.tools.build:gradle to v8.2.0 2023-11-30 18:32:19 +00:00
Renovate Bot
3d8abdc65b Update dependency gradle to v8.5 2023-11-29 14:33:08 +00:00
Renovate Bot
4832876e54 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.4 2023-11-26 12:31:21 +00:00
Renovate Bot
a0b0409930 Update kotlin monorepo to v1.9.21 2023-11-23 12:31:20 +00:00
Renovate Bot
8e6e9d4e8e Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.16.0 2023-11-16 01:31:37 +00:00
Renovate Bot
ee6d03db35 Update dependency androidx.activity:activity-ktx to v1.8.1 2023-11-15 18:31:49 +00:00
Renovate Bot
364270d338 Update dependency org.junit.vintage:junit-vintage-engine to v5.10.1 2023-11-05 17:31:25 +00:00
birdbird
a4dc06fa8a Merge branch 'renovate/detekt' into 'develop'
Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.3

See merge request ultrasonic/ultrasonic!1145
2023-11-03 10:35:44 +00:00
birdbird
5c94d995d4 Merge branch 'renovate/robolectric' into 'develop'
Update dependency org.robolectric:robolectric to v4.11.1

See merge request ultrasonic/ultrasonic!1143
2023-11-03 10:35:41 +00:00
birdbird
366da1c30c Merge branch 'renovate/kotlin-monorepo' into 'develop'
Update kotlin monorepo to v1.9.20

See merge request ultrasonic/ultrasonic!1144
2023-11-03 10:35:40 +00:00
birdbird
e893510e79 Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.7.5

See merge request ultrasonic/ultrasonic!1147
2023-11-03 10:35:34 +00:00
birdbird
67b359999e Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.7.0

See merge request ultrasonic/ultrasonic!1148
2023-11-03 10:35:29 +00:00
Renovate Bot
01569647f7 Update dependency org.mockito:mockito-core to v5.7.0 2023-11-02 20:31:40 +00:00
Renovate Bot
3ee20113ae Update navigation to v2.7.5 2023-11-01 17:31:36 +00:00
Renovate Bot
6edff7e053 Update dependency org.robolectric:robolectric to v4.11.1 2023-10-31 19:35:46 +00:00
Renovate Bot
70cc124818 Update dependency io.gitlab.arturbosch.detekt:detekt-gradle-plugin to v1.23.3 2023-10-31 16:31:16 +00:00
Renovate Bot
98bf943a86 Update kotlin monorepo to v1.9.20 2023-10-30 20:31:18 +00:00
birdbird
58944bb0fd Merge branch 'renovate/room' into 'develop'
Update room to v2.6.0

See merge request ultrasonic/ultrasonic!1138
2023-10-27 14:21:50 +00:00
Renovate Bot
397e1b6ecc Update room to v2.6.0 2023-10-27 14:21:50 +00:00
birdbird
71336b3c9f Merge branch 'weblate-ultrasonic-app' into 'develop'
Translations update from Hosted Weblate

See merge request ultrasonic/ultrasonic!1139
2023-10-27 14:01:51 +00:00
Óscar García Amor
cd47bcf082
Translated using Weblate (Spanish)
Currently translated at 100.0% (426 of 426 strings)

Translation: Ultrasonic/app
Translate-URL: https://hosted.weblate.org/projects/ultrasonic/app/es/
2023-10-27 11:12:57 +00:00
birdbird
4fb5d2a437 Merge branch 'wifiPerf' into 'develop'
Address wifi deprecation

See merge request ultrasonic/ultrasonic!1142
2023-10-27 11:12:46 +00:00
tzugen
e8bf5a38b7
Address wifi deprecation 2023-10-27 13:06:27 +02:00
birdbird
ddfaf520e5 Merge branch 'wifiPerf' into 'develop'
Add a ClearJukebox method

See merge request ultrasonic/ultrasonic!1141
2023-10-27 11:00:01 +00:00
tzugen
4d1c7464b9
Add a ClearJukebox method 2023-10-27 12:32:22 +02:00
birdbird
42c6eac97f Merge branch 'renovate/okhttp-monorepo' into 'develop'
Update okhttp monorepo to v4.12.0

See merge request ultrasonic/ultrasonic!1140
2023-10-20 09:23:43 +00:00
Renovate Bot
2ae0a27588 Update okhttp monorepo to v4.12.0 2023-10-19 23:31:28 +00:00
birdbird
f2e8c0c331 Merge branch 'renovate/ktlintgradle' into 'develop'
Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.1

See merge request ultrasonic/ultrasonic!1131
2023-10-18 11:57:24 +00:00
birdbird
7ce522fd15 Merge branch 'renovate/jackson' into 'develop'
Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.15.3

See merge request ultrasonic/ultrasonic!1132
2023-10-18 11:57:10 +00:00
birdbird
170c61ef84 Merge branch 'renovate/mockito-monorepo' into 'develop'
Update dependency org.mockito:mockito-core to v5.6.0

See merge request ultrasonic/ultrasonic!1129
2023-10-18 11:56:51 +00:00
birdbird
87fdf94e61 Merge branch 'renovate/colorpicker' into 'develop'
Update dependency com.github.skydoves:colorpickerview to v2.3.0

See merge request ultrasonic/ultrasonic!1125
2023-10-18 11:56:00 +00:00
birdbird
fffe245df5 Merge branch 'renovate/materialdesign' into 'develop'
Update dependency com.google.android.material:material to v1.10.0

See merge request ultrasonic/ultrasonic!1128
2023-10-18 11:55:19 +00:00
birdbird
95194bed3e Merge branch 'renovate/gradle-8.x' into 'develop'
Update dependency gradle to v8.4

See merge request ultrasonic/ultrasonic!1127
2023-10-18 11:53:45 +00:00
birdbird
a9467e2fd8 Merge branch 'renovate/navigation' into 'develop'
Update navigation to v2.7.4

See merge request ultrasonic/ultrasonic!1126
2023-10-18 11:51:49 +00:00
birdbird
4b99fdb788 Merge branch 'master' into 'develop'
Merge back from master

See merge request ultrasonic/ultrasonic!1137
2023-10-18 11:51:16 +00:00
birdbird
727e53e096 Merge branch '480' into 'master'
Release 4.8.0

See merge request ultrasonic/ultrasonic!1124
2023-10-18 10:56:51 +00:00
Renovate Bot
c6d26cdd67 Update dependency com.fasterxml.jackson.module:jackson-module-kotlin to v2.15.3 2023-10-13 02:31:21 +00:00
Renovate Bot
458fe5c36e Update dependency org.jlleitschuh.gradle:ktlint-gradle to v11.6.1 2023-10-10 15:31:32 +00:00
Renovate Bot
f388aaf4d8 Update dependency org.mockito:mockito-core to v5.6.0 2023-10-06 15:31:34 +00:00
Renovate Bot
6ddff58afb Update dependency com.google.android.material:material to v1.10.0 2023-10-05 18:31:28 +00:00
Renovate Bot
1e176f995a Update dependency gradle to v8.4 2023-10-04 21:32:34 +00:00
Renovate Bot
22c61258cc Update navigation to v2.7.4 2023-10-04 17:31:13 +00:00
Renovate Bot
288a1ad1c2 Update dependency com.github.skydoves:colorpickerview to v2.3.0 2023-10-02 12:31:29 +00:00
227 changed files with 2502 additions and 1872 deletions

2
.editorconfig Normal file
View File

@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint_code_style = android_studio

1
.gitignore vendored
View File

@ -18,6 +18,7 @@ out/
# Gradle files
.gradle/
.kotlin/
build/
# Local configuration file (sdk path, etc)

View File

@ -1,5 +1,5 @@
default:
image: registry.gitlab.com/ultrasonic/ci-android:1.1.0
image: registry.gitlab.com/ultrasonic/ci-android:1.2.0
cache: &global_cache
key:
files:

2
.idea/compiler.xml generated
View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -13,7 +13,7 @@ buildscript {
google()
mavenCentral()
gradlePluginPortal()
maven { url "https://plugins.gradle.org/m2/" }
maven { url = "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath libs.gradle
@ -43,7 +43,7 @@ allprojects {
// Set Kotlin JVM target to the same for all subprojects
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "17"
jvmTarget = "21"
}
}
@ -55,6 +55,6 @@ allprojects {
}
wrapper {
gradleVersion(libs.versions.gradle.get())
distributionType("all")
gradleVersion = libs.versions.gradle.get()
distributionType = "all"
}

View File

@ -12,5 +12,9 @@ dependencies {
}
android {
namespace 'org.moire.ultrasonic.subsonic.domain'
namespace = 'org.moire.ultrasonic.subsonic.domain'
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}

View File

@ -31,7 +31,7 @@ data class Album(
override var genre: String? = null,
override var starred: Boolean = false,
override var path: String? = null,
override var closeness: Int = 0,
override var closeness: Int = 0
) : MusicDirectory.Child() {
override var isDirectory = true
override var isVideo = false

View File

@ -13,10 +13,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
var name: String? = null
@JvmOverloads
fun getChildren(
includeDirs: Boolean = true,
includeFiles: Boolean = true
): List<Child> {
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
if (includeDirs && includeFiles) {
return toList()
}

View File

@ -4,6 +4,11 @@ plugins {
apply from: bootstrap.kotlinModule
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
api libs.retrofit
api libs.jacksonConverter

View File

@ -8,7 +8,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
* Base class for integration tests for [SubsonicAPIClient] class.
*/
abstract class SubsonicAPIClientTest {
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
@JvmField @Rule
val mockWebServerRule = MockWebServerRule()
protected lateinit var config: SubsonicClientConfiguration
protected lateinit var client: SubsonicAPIClient

View File

@ -11,7 +11,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
* Base class for testing [okhttp3.Interceptor] implementations.
*/
abstract class BaseInterceptorTest {
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
@Rule @JvmField
val mockWebServerRule = MockWebServerRule()
lateinit var client: OkHttpClient

View File

@ -92,7 +92,13 @@ internal class ApiVersionCheckWrapper(
checkVersion(V1_4_0)
checkParamVersion(musicFolderId, V1_12_0)
return api.search2(
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
query,
artistCount,
artistOffset,
albumCount,
albumOffset,
songCount,
musicFolderId
)
}
@ -108,7 +114,13 @@ internal class ApiVersionCheckWrapper(
checkVersion(V1_8_0)
checkParamVersion(musicFolderId, V1_12_0)
return api.search3(
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
query,
artistCount,
artistOffset,
albumCount,
albumOffset,
songCount,
musicFolderId
)
}
@ -228,7 +240,13 @@ internal class ApiVersionCheckWrapper(
checkParamVersion(estimateContentLength, V1_8_0)
checkParamVersion(converted, V1_14_0)
return api.stream(
id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
id,
maxBitRate,
format,
timeOffset,
videoSize,
estimateContentLength,
converted
)
}
@ -335,8 +353,9 @@ internal class ApiVersionCheckWrapper(
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
// If it is true, it is probably the first call with this server
if (!isRealProtocolVersion) return
if (currentApiVersion < expectedVersion)
if (currentApiVersion < expectedVersion) {
throw ApiNotSupportedException(currentApiVersion)
}
}
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {

View File

@ -90,10 +90,7 @@ interface SubsonicAPIDefinition {
): Call<SubsonicResponse>
@GET("setRating.view")
fun setRating(
@Query("id") id: String,
@Query("rating") rating: Int
): Call<SubsonicResponse>
fun setRating(@Query("id") id: String, @Query("rating") rating: Int): Call<SubsonicResponse>
@GET("getArtist.view")
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
@ -158,8 +155,7 @@ interface SubsonicAPIDefinition {
@Query("public") public: Boolean? = null,
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
):
Call<SubsonicResponse>
): Call<SubsonicResponse>
@GET("getPodcasts.view")
fun getPodcasts(
@ -227,10 +223,7 @@ interface SubsonicAPIDefinition {
@Streaming
@GET("getCoverArt.view")
fun getCoverArt(
@Query("id") id: String,
@Query("size") size: Long? = null
): Call<ResponseBody>
fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call<ResponseBody>
@Streaming
@GET("stream.view")

View File

@ -29,10 +29,12 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
V1_13_0("5.3", "1.13.0"),
V1_14_0("6.0", "1.14.0"),
V1_15_0("6.1", "1.15.0"),
V1_16_0("6.1.2", "1.16.0");
V1_16_0("6.1.2", "1.16.0")
;
companion object {
@JvmStatic @Throws(IllegalArgumentException::class)
@JvmStatic
@Throws(IllegalArgumentException::class)
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
val versionComponents = apiVersion.split(".")
@ -41,8 +43,11 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
try {
val majorVersion = versionComponents[0].toInt()
val minorVersion = versionComponents[1].toInt()
val patchVersion = if (versionComponents.size > 2) versionComponents[2].toInt()
else 0
val patchVersion = if (versionComponents.size > 2) {
versionComponents[2].toInt()
} else {
0
}
when (majorVersion) {
1 -> when {

View File

@ -48,7 +48,10 @@ class VersionAwareJacksonConverterFactory(
retrofit: Retrofit
): Converter<*, RequestBody>? {
return jacksonConverterFactory?.requestBodyConverter(
type, parameterAnnotations, methodAnnotations, retrofit
type,
parameterAnnotations,
methodAnnotations,
retrofit
)
}
@ -63,7 +66,7 @@ class VersionAwareJacksonConverterFactory(
}
}
class VersionAwareResponseBodyConverter<T> (
class VersionAwareResponseBodyConverter<T>(
private val notifier: (SubsonicAPIVersions) -> Unit = {},
private val adapter: ObjectReader
) : Converter<ResponseBody, T> {

View File

@ -6,6 +6,7 @@ import okhttp3.Interceptor.Chain
import okhttp3.Response
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
// Allow 20 seconds extra timeout pear MB offset.
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02

View File

@ -23,7 +23,8 @@ enum class AlbumListType(val typeName: String) {
SORTED_BY_ARTIST("alphabeticalByArtist"),
STARRED("starred"),
BY_YEAR("byYear"),
BY_GENRE("byGenre");
BY_GENRE("byGenre")
;
override fun toString(): String {
return typeName

View File

@ -16,7 +16,8 @@ enum class JukeboxAction(val action: String) {
CLEAR("clear"),
REMOVE("remove"),
SHUFFLE("shuffle"),
SET_GAIN("setGain");
SET_GAIN("setGain")
;
override fun toString(): String {
return action

View File

@ -10,7 +10,8 @@ class BookmarksResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("bookmarks") private val bookmarksWrapper = BookmarkWrapper()
@JsonProperty("bookmarks")
private val bookmarksWrapper = BookmarkWrapper()
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
}

View File

@ -10,7 +10,8 @@ class ChatMessagesResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("chatMessages") private val wrapper = ChatMessagesWrapper()
@JsonProperty("chatMessages")
private val wrapper = ChatMessagesWrapper()
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
}

View File

@ -10,7 +10,8 @@ class GenresResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("genres") private val genresWrapper = GenresWrapper()
@JsonProperty("genres")
private val genresWrapper = GenresWrapper()
val genresList: List<Genre> get() = genresWrapper.genresList
}

View File

@ -11,7 +11,8 @@ class GetAlbumList2Response(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("albumList2") private val albumWrapper2 = AlbumWrapper2()
@JsonProperty("albumList2")
private val albumWrapper2 = AlbumWrapper2()
val albumList: List<Album>
get() = albumWrapper2.albumList

View File

@ -10,7 +10,8 @@ class GetAlbumListResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
@JsonProperty("albumList")
private val albumWrapper = AlbumWrapper()
val albumList: List<Album>
get() = albumWrapper.albumList

View File

@ -10,7 +10,8 @@ class GetPodcastsResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("podcasts") private val channelsWrapper = PodcastChannelWrapper()
@JsonProperty("podcasts")
private val channelsWrapper = PodcastChannelWrapper()
val podcastChannels: List<PodcastChannel>
get() = channelsWrapper.channelsList

View File

@ -10,7 +10,8 @@ class GetRandomSongsResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("randomSongs") private val songsWrapper = RandomSongsWrapper()
@JsonProperty("randomSongs")
private val songsWrapper = RandomSongsWrapper()
val songsList
get() = songsWrapper.songsList

View File

@ -10,7 +10,8 @@ class GetSongsByGenreResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("songsByGenre") private val songsByGenreList = SongsByGenreWrapper()
@JsonProperty("songsByGenre")
private val songsByGenreList = SongsByGenreWrapper()
val songsList get() = songsByGenreList.songsList
}

View File

@ -11,11 +11,13 @@ class JukeboxResponse(
error: SubsonicError?,
var jukebox: JukeboxStatus = JukeboxStatus()
) : SubsonicResponse(status, version, error) {
@JsonSetter("jukeboxStatus") fun setJukeboxStatus(jukebox: JukeboxStatus) {
@JsonSetter("jukeboxStatus")
fun setJukeboxStatus(jukebox: JukeboxStatus) {
this.jukebox = jukebox
}
@JsonSetter("jukeboxPlaylist") fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
@JsonSetter("jukeboxPlaylist")
fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
this.jukebox = jukebox
}
}

View File

@ -10,7 +10,8 @@ class MusicFoldersResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("musicFolders") private val wrapper = MusicFoldersWrapper()
@JsonProperty("musicFolders")
private val wrapper = MusicFoldersWrapper()
val musicFolders get() = wrapper.musicFolders
}

View File

@ -10,7 +10,8 @@ class SharesResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
@JsonProperty("shares")
private val wrappedShares = SharesWrapper()
val shares get() = wrappedShares.share
}

View File

@ -20,7 +20,8 @@ open class SubsonicResponse(
) {
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
enum class Status(val jsonValue: String) {
OK("ok"), ERROR("failed");
OK("ok"),
ERROR("failed");
companion object {
fun getStatusFromJson(jsonValue: String) =

View File

@ -10,7 +10,8 @@ class VideosResponse(
version: SubsonicAPIVersions,
error: SubsonicError?
) : SubsonicResponse(status, version, error) {
@JsonProperty("videos") private val videosWrapper = VideosWrapper()
@JsonProperty("videos")
private val videosWrapper = VideosWrapper()
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
}

View File

@ -18,7 +18,9 @@ class ProxyPasswordInterceptorTest {
private val proxyInterceptor = ProxyPasswordInterceptor(
V1_12_0,
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
mockPasswordHexInterceptor,
mockPasswordMd5Interceptor,
false
)
@Test
@ -40,8 +42,10 @@ class ProxyPasswordInterceptorTest {
@Test
fun `Should use hex password if forceHex is true`() {
val interceptor = ProxyPasswordInterceptor(
V1_16_0, mockPasswordHexInterceptor,
mockPasswordMd5Interceptor, true
V1_16_0,
mockPasswordHexInterceptor,
mockPasswordMd5Interceptor,
true
)
interceptor.intercept(mockChain)

View File

@ -1,47 +1,46 @@
[versions]
# You need to run ./gradlew wrapper after updating the version
gradle = "8.1.1"
gradle = "8.13"
navigation = "2.7.3"
gradlePlugin = "8.1.2"
androidxcar = "1.2.0"
androidxcore = "1.12.0"
ktlint = "0.43.2"
ktlintGradle = "11.6.0"
detekt = "1.23.0"
navigation = "2.8.9"
gradlePlugin = "8.9.1"
androidxcar = "1.4.0"
androidxcore = "1.16.0"
ktlint = "1.0.1"
ktlintGradle = "12.2.0"
detekt = "1.23.8"
preferences = "1.2.1"
media3 = "1.1.1"
media3 = "1.6.1"
androidSupport = "1.7.0"
materialDesign = "1.9.0"
constraintLayout = "2.1.4"
activity = "1.8.0"
androidSupport = "1.9.1"
materialDesign = "1.12.0"
constraintLayout = "2.2.1"
activity = "1.10.1"
multidex = "2.0.1"
room = "2.5.2"
kotlin = "1.9.10"
ksp = "1.9.10-1.0.13"
kotlinxCoroutines = "1.7.3"
viewModelKtx = "2.6.2"
room = "2.7.0"
kotlin = "2.1.20"
ksp = "2.1.20-2.0.0"
kotlinxCoroutines = "1.10.2"
viewModelKtx = "2.8.7"
swipeRefresh = "1.1.0"
retrofit = "2.9.0"
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
jackson = "2.13.5"
okhttp = "4.11.0"
koin = "3.5.0"
retrofit = "2.11.0"
jackson = "2.18.3"
okhttp = "4.12.0"
koin = "4.0.4"
picasso = "2.8"
junit4 = "4.13.2"
junit5 = "5.10.0"
mockito = "5.5.0"
mockitoKotlin = "5.1.0"
junit5 = "5.12.2"
mockito = "5.17.0"
mockitoKotlin = "5.4.0"
kluent = "1.73"
apacheCodecs = "1.16.0"
robolectric = "4.10.3"
apacheCodecs = "1.18.0"
robolectric = "4.14.1"
timber = "5.0.1"
fastScroll = "2.0.1"
colorPicker = "2.2.4"
rxJava = "3.1.8"
colorPicker = "2.3.0"
rxJava = "3.1.10"
rxAndroid = "3.0.2"
multiType = "4.3.0"

View File

@ -1,5 +1,5 @@
ext.versions = [
minSdk : 21,
minSdk : 26,
targetSdk : 33,
compileSdk : 34,
compileSdk : 35,
]

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

20
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -145,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@ -153,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@ -202,11 +204,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail

View File

@ -34,10 +34,10 @@ android {
'minify/proguard-kotlin.pro'
}
debug {
minifyEnabled false
multiDexEnabled true
testCoverageEnabled true
applicationIdSuffix '.debug'
minifyEnabled = false
multiDexEnabled = true
testCoverageEnabled = true
applicationIdSuffix = '.debug'
}
}
@ -53,18 +53,18 @@ android {
}
kotlinOptions {
jvmTarget = "17"
jvmTarget = "21"
}
buildFeatures {
viewBinding true
dataBinding true
buildConfig true
viewBinding = true
dataBinding = true
buildConfig = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
ksp {
@ -73,8 +73,8 @@ android {
lint {
baseline = file("lint-baseline.xml")
abortOnError true
warningsAsErrors true
abortOnError = true
warningsAsErrors = true
warning 'ImpliedQuantity'
disable 'IconMissingDensityFolder', 'VectorPath'
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
@ -82,10 +82,10 @@ android {
// We manage dependencies on Gitlab with RenovateBot
disable 'GradleDependency'
disable 'AndroidGradlePluginVersion'
textReport true
checkDependencies true
textReport = true
checkDependencies = true
}
namespace 'org.moire.ultrasonic'
namespace = 'org.moire.ultrasonic'
}

View File

@ -1,6 +1,6 @@
/*
* NavigationActivity.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -13,7 +13,6 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Resources
import android.media.AudioManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.provider.SearchRecentSuggestions
@ -96,6 +95,7 @@ class NavigationActivity : ScopeActivity() {
private var drawerLayout: DrawerLayout? = null
private var host: NavHostFragment? = null
private var selectServerButton: MaterialButton? = null
private var selectServerDropdownImage: ImageView? = null
private var headerBackgroundImage: ImageView? = null
// We store the last search string in this variable.
@ -165,7 +165,7 @@ class NavigationActivity : ScopeActivity() {
drawerLayout
)
setupActionBar(navController, appBarConfiguration)
setupActionBarWithNavController(navController, appBarConfiguration)
setupNavigationMenu(navController)
@ -204,10 +204,11 @@ class NavigationActivity : ScopeActivity() {
}
rxBusSubscription += RxBus.playerStateObservable.subscribe {
if (it.state == STATE_READY)
if (it.state == STATE_READY) {
showNowPlaying()
else
} else {
hideNowPlaying()
}
}
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
@ -226,7 +227,7 @@ class NavigationActivity : ScopeActivity() {
// Setup app shortcuts on supported devices, but not on first start, when the server
// is not configured yet.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
if (!UApp.instance!!.isFirstRun) {
ShortcutUtil.registerShortcuts(this)
}
@ -314,8 +315,11 @@ class NavigationActivity : ScopeActivity() {
// Lifecycle support's constructor registers some event receivers so it should be created early
lifecycleSupport.onCreate()
if (!nowPlayingHidden) showNowPlaying()
else hideNowPlaying()
if (!nowPlayingHidden) {
showNowPlaying()
} else {
hideNowPlaying()
}
}
/*
@ -329,35 +333,31 @@ class NavigationActivity : ScopeActivity() {
}
private fun updateNavigationHeaderForServer() {
// Only show the vector graphic on Android 11 or earlier
val showVectorBackground = (Build.VERSION.SDK_INT < Build.VERSION_CODES.S)
val activeServer = activeServerProvider.getActiveServer()
if (cachedServerCount == 0)
if (cachedServerCount == 0) {
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
else selectServerButton?.text = activeServer.name
} else {
selectServerButton?.text = activeServer.name
}
val foregroundColor =
ServerColor.getForegroundColor(this, activeServer.color, showVectorBackground)
ServerColor.getForegroundColor(this, activeServer.color)
val backgroundColor =
ServerColor.getBackgroundColor(this, activeServer.color)
if (activeServer.index == 0)
if (activeServer.index == 0) {
selectServerButton?.icon =
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
else
} else {
selectServerButton?.icon =
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
}
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
selectServerButton?.setTextColor(foregroundColor)
selectServerDropdownImage?.imageTintList = ColorStateList.valueOf(foregroundColor)
headerBackgroundImage?.setBackgroundColor(backgroundColor)
// Hide the vector graphic on Android 12 or later
if (!showVectorBackground) {
headerBackgroundImage?.setImageDrawable(null)
}
}
private fun setupNavigationMenu(navController: NavController) {
@ -402,26 +402,23 @@ class NavigationActivity : ScopeActivity() {
selectServerButton =
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
val dropDownButton: ImageView? =
selectServerDropdownImage =
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
val onClick: (View) -> Unit = {
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
this.drawerLayout?.closeDrawer(GravityCompat.START)
}
navController.navigate(R.id.serverSelectorFragment)
}
selectServerButton?.setOnClickListener(onClick)
dropDownButton?.setOnClickListener(onClick)
selectServerDropdownImage?.setOnClickListener(onClick)
headerBackgroundImage =
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
}
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
setupActionBarWithNavController(navController, appBarConfig)
}
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
drawerLayout?.closeDrawer(GravityCompat.START)
@ -438,19 +435,30 @@ class NavigationActivity : ScopeActivity() {
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
val navController = findNavController(R.id.nav_host_fragment)
// Check if this item ID exists in the nav graph
val destinationExists = navController.graph.findNode(item.itemId) != null
return if (destinationExists) {
item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
} else {
// Let the fragments handle their own menu items
super.onOptionsItemSelected(item)
}
}
// TODO: Why is this needed? Shouldn't it just work by default?
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
// This override is required by design when using setupActionBarWithNavController()
// with an AppBarConfiguration. It ensures that the Up button behavior is correctly
// delegated — either navigating "up" in the back stack, or opening the drawer if
// we're at a top-level destination.
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration) ||
super.onSupportNavigateUp()
}
override fun onNewIntent(intent: Intent?) {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
when (intent?.action) {
when (intent.action) {
Constants.INTENT_PLAY_RANDOM_SONGS -> {
playRandomSongs()
}
@ -473,7 +481,8 @@ class NavigationActivity : ScopeActivity() {
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
SearchSuggestionProvider.AUTHORITY,
SearchSuggestionProvider.MODE
)
suggestions.saveRecentQuery(query, null)
@ -528,7 +537,6 @@ class NavigationActivity : ScopeActivity() {
private fun showWelcomeDialog() {
if (!UApp.instance!!.setupDialogDisplayed) {
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
InfoDialog.Builder(this)

View File

@ -32,11 +32,11 @@ import org.moire.ultrasonic.util.LayoutType
*/
open class AlbumRowDelegate(
open val onItemClick: (Album) -> Unit,
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
open val onContextMenuClick: (MenuItem, Album) -> Boolean
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
private val starDrawable: Int = R.drawable.ic_star_full
private val starHollowDrawable: Int = R.drawable.ic_star_hollow
private val starDrawable: Int = R.drawable.rating_star_full
private val starHollowDrawable: Int = R.drawable.rating_star_hollow
open var layoutType = LayoutType.LIST
@ -61,8 +61,11 @@ open class AlbumRowDelegate(
val imageLoaderProvider: ImageLoaderProvider by inject()
imageLoaderProvider.executeOn {
it.loadImage(
holder.coverArt, item,
false, 0, R.drawable.unknown_album
holder.coverArt,
item,
false,
0,
R.drawable.unknown_album
)
}
}

View File

@ -23,10 +23,7 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
holder.textView.setText(item.stringId)
}
override fun onCreateViewHolder(
inflater: LayoutInflater,
parent: ViewGroup
): ViewHolder {
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
return ViewHolder(inflater.inflate(layout, parent, false))
}

View File

@ -78,7 +78,10 @@ class FolderSelectorBinder(context: Context) :
val popup = PopupMenu(weakContext.get()!!, layout)
var menuItem = popup.menu.add(
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
MENU_GROUP_MUSIC_FOLDER,
-1,
0,
R.string.select_artist_all_folders
)
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {

View File

@ -46,7 +46,6 @@ class HeaderViewBinder(
}
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
val context = weakContext.get() ?: return
val resources = context.resources
@ -98,7 +97,8 @@ class HeaderViewBinder(
holder.yearView.text = year
val songs = resources.getQuantityString(
R.plurals.n_songs, item.childCount,
R.plurals.n_songs,
item.childCount,
item.childCount
)
holder.songCountView.text = songs

View File

@ -77,7 +77,6 @@ internal class ServerRowAdapter(
*/
@Suppress("LongMethod")
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
var vi: View? = convertView
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)

View File

@ -1,13 +1,18 @@
package org.moire.ultrasonic.adapters
import android.graphics.Color
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.view.MenuInflater
import android.view.View
import android.widget.Checkable
import android.widget.CheckedTextView
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData
@ -31,6 +36,7 @@ import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.service.plusAssign
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.themeColor
const val INDICATOR_THICKNESS_INDEFINITE = 5
const val INDICATOR_THICKNESS_DEFINITE = 10
@ -50,17 +56,12 @@ class TrackViewHolder(val view: View) :
var entry: Track? = null
private set
var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
private var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
var check: CheckedTextView = view.findViewById(R.id.song_check)
var drag: ImageView = view.findViewById(R.id.song_drag)
var observableChecked = MutableLiveData(false)
private var rating: LinearLayout = view.findViewById(R.id.song_rating)
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
private var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
private var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
private var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
private var star: ImageView = view.findViewById(R.id.song_star)
private var track: TextView = view.findViewById(R.id.song_track)
private var title: TextView = view.findViewById(R.id.song_title)
@ -79,15 +80,34 @@ class TrackViewHolder(val view: View) :
private var rxBusSubscription: CompositeDisposable? = null
@Suppress("ComplexMethod")
fun setSong(
song: Track,
checkable: Boolean,
draggable: Boolean,
isSelected: Boolean = false
) {
val useFiveStarRating = Settings.useFiveStarRating
fun setSong(song: Track, checkable: Boolean, draggable: Boolean, isSelected: Boolean = false) {
entry = song
// Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
}
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
if (it.id != song.id) return@subscribe
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateRatingDisplay(song.userRating, it.rating.isHeart)
} else if (it.rating is StarRating) {
updateRatingDisplay(it.rating.starRating.toInt(), song.starred)
}
}
}
val entryDescription = Util.readableEntryDescription(song)
artist.text = entryDescription.artist
@ -108,7 +128,7 @@ class TrackViewHolder(val view: View) :
if (ActiveServerProvider.isOffline()) {
star.isGone = true
} else {
setupStarButtons(song, useFiveStarRating)
setupRating(song)
}
// Instead of blocking the UI thread while looking up the current state,
@ -120,41 +140,12 @@ class TrackViewHolder(val view: View) :
)
}
if (useFiveStarRating) {
updateFiveStars(entry?.userRating ?: 0)
} else {
updateSingleStar(entry!!.starred)
}
updateRatingDisplay(entry!!.userRating, entry!!.starred)
if (song.isVideo) {
artist.isGone = true
progressIndicator.isGone = true
}
// Create new Disposable for the new Subscriptions
rxBusSubscription = CompositeDisposable()
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
}
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
if (it.id != song.id) return@subscribe
updateStatus(it.state, it.progress)
}
// Listen for rating updates
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
launch(Dispatchers.Main) {
// Ignore updates which are not for the current song
if (it.id != song.id) return@launch
if (it.rating is HeartRating) {
updateSingleStar(it.rating.isHeart)
} else if (it.rating is StarRating) {
updateFiveStars(it.rating.starRating.toInt())
}
}
}
}
// This is called when the Holder is recycled and receives a new Song
@ -171,7 +162,10 @@ class TrackViewHolder(val view: View) :
if (isPlaying && !isPlayingCached) {
isPlayingCached = true
title.setCompoundDrawablesWithIntrinsicBounds(
playingIcon, null, null, null
playingIcon,
null,
null,
null
)
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
songLayout.setBackgroundColor(color)
@ -179,62 +173,98 @@ class TrackViewHolder(val view: View) :
} else if (!isPlaying && isPlayingCached) {
isPlayingCached = false
title.setCompoundDrawablesWithIntrinsicBounds(
0, 0, 0, 0
0,
0,
0,
0
)
songLayout.setBackgroundColor(Color.TRANSPARENT)
songLayout.elevation = 0F
}
}
private fun setupStarButtons(track: Track, useFiveStarRating: Boolean) {
if (useFiveStarRating) {
// Hide single star
star.isGone = true
rating.isVisible = true
val rating = if (track.userRating == null) 0 else track.userRating!!
updateFiveStars(rating)
private fun setupRating(track: Track) {
star.isVisible = true
updateRatingDisplay(track.userRating, track.starred)
// Five star rating has no click handler because in the
// track view theres not enough space
} else {
star.isVisible = true
rating.isGone = true
updateSingleStar(track.starred)
star.setOnClickListener {
track.starred = !track.starred
updateSingleStar(track.starred)
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, HeartRating(track.starred))
)
}
}
star.setOnClickListener { toggleHeart(track) }
star.setOnLongClickListener { view -> showRatingPopup(view, track) }
}
private fun toggleHeart(track: Track) {
track.starred = !track.starred
updateRatingDisplay(track.userRating, track.starred)
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, HeartRating(track.starred))
)
}
@Suppress("MagicNumber")
private fun updateFiveStars(rating: Int) {
fiveStar1.setImageResource(
if (rating > 0) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar2.setImageResource(
if (rating > 1) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar3.setImageResource(
if (rating > 2) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar4.setImageResource(
if (rating > 3) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
fiveStar5.setImageResource(
if (rating > 4) R.drawable.ic_star_full else R.drawable.ic_star_hollow
)
private fun showRatingPopup(view: View, track: Track): Boolean {
val popup = PopupMenu(view.context, view)
val inflater: MenuInflater = popup.menuInflater
inflater.inflate(R.menu.rating, popup.menu)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) popup.setForceShowIcon(true)
popup.setOnMenuItemClickListener {
val rating = when (it.itemId) {
R.id.popup_rate_1 -> 1
R.id.popup_rate_2 -> 2
R.id.popup_rate_3 -> 3
R.id.popup_rate_4 -> 4
R.id.popup_rate_5 -> 5
else -> 0
}
track.userRating = rating
updateRatingDisplay(track.userRating, track.starred)
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, StarRating(5, rating.toFloat()))
)
true
}
popup.show()
return true
}
private fun updateSingleStar(starred: Boolean) {
if (starred) {
star.setImageResource(R.drawable.ic_star_full)
} else {
star.setImageResource(R.drawable.ic_star_hollow)
@Suppress("MagicNumber")
private fun updateRatingDisplay(rating: Int?, starred: Boolean) {
val ratingDrawable = when (rating) {
1 -> R.drawable.rating_star_1
2 -> R.drawable.rating_star_2
3 -> R.drawable.rating_star_3
4 -> R.drawable.rating_star_4
5 -> R.drawable.rating_star_5
else -> {
R.drawable.rating_star_0
}
}
val layers = if (starred) {
arrayOf(
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!,
ResourcesCompat.getDrawable(
view.resources,
R.drawable.rating_heart_mini_overlay,
null
)!!
)
} else {
arrayOf(
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!
)
}
val ratingDisplay = LayerDrawable(layers)
ratingDisplay.getDrawable(0).setTint(
view.context.themeColor(com.google.android.material.R.attr.colorOnBackground)
)
if (starred) {
ratingDisplay.getDrawable(1).setTint(
view.context.themeColor(com.google.android.material.R.attr.colorTertiary)
)
}
star.setImageDrawable(ratingDisplay)
}
private fun updateStatus(status: DownloadState, progress: Int?) {
@ -257,7 +287,8 @@ class TrackViewHolder(val view: View) :
showProgress()
}
DownloadState.RETRYING,
DownloadState.QUEUED -> {
DownloadState.QUEUED
-> {
showIndefiniteProgress()
}
else -> {

View File

@ -114,10 +114,8 @@ private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder {
detectLeakedClosableObjects()
detectLeakedRegistrationObjects()
detectFileUriExposure()
detectContentUriWithoutPermission()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
detectContentUriWithoutPermission()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
detectCredentialProtectedWhileLocked()
}

View File

@ -50,7 +50,8 @@ class EqualizerController : CoroutineScope by CoroutineScope(Dispatchers.IO) {
launch {
try {
val settings = deserialize<EqualizerSettings>(
UApp.applicationContext(), "equalizer.dat"
UApp.applicationContext(),
"equalizer.dat"
)
settings?.apply(equalizer!!)
} catch (all: Throwable) {

View File

@ -51,7 +51,8 @@ class ActiveServerProvider(
}
Timber.d(
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
serverId, cachedServer
serverId,
cachedServer
)
}
@ -158,7 +159,7 @@ class ActiveServerProvider(
METADATA_DB + serverId
)
.addMigrations(META_MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
.fallbackToDestructiveMigrationOnDowngrade(true)
.build()
}

View File

@ -1,3 +1,12 @@
/*
* AppDatabase.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@file:Suppress("ktlint:standard:max-line-length")
package org.moire.ultrasonic.data
import androidx.room.Database
@ -31,8 +40,8 @@ val MIGRATION_1_2: Migration = object : Migration(1, 2) {
}
val MIGRATION_2_1: Migration = object : Migration(2, 1) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
id INTEGER NOT NULL PRIMARY KEY,
@ -48,7 +57,7 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO ServerSettingMigration (
id, [index], name, url, userName, password, jukeboxByDefault,
@ -60,10 +69,10 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
FROM ServerSetting
""".trimIndent()
)
database.execSQL(
db.execSQL(
"DROP TABLE ServerSetting"
)
database.execSQL(
db.execSQL(
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
)
}
@ -87,8 +96,8 @@ val MIGRATION_2_3: Migration = object : Migration(2, 3) {
}
val MIGRATION_3_2: Migration = object : Migration(3, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
id INTEGER NOT NULL PRIMARY KEY,
@ -105,7 +114,7 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO ServerSettingMigration (
id, [index], name, url, userName, password, jukeboxByDefault,
@ -117,26 +126,26 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
FROM ServerSetting
""".trimIndent()
)
database.execSQL(
db.execSQL(
"DROP TABLE ServerSetting"
)
database.execSQL(
db.execSQL(
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
)
}
}
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE ServerSetting ADD COLUMN color INTEGER"
)
}
}
val MIGRATION_4_3: Migration = object : Migration(4, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
id INTEGER NOT NULL PRIMARY KEY,
@ -157,7 +166,7 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO ServerSettingMigration (
id, [index], name, url, userName, password, jukeboxByDefault,
@ -171,18 +180,18 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
FROM ServerSetting
""".trimIndent()
)
database.execSQL(
db.execSQL(
"DROP TABLE ServerSetting"
)
database.execSQL(
db.execSQL(
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
)
}
}
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
@ -204,7 +213,7 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO `_new_ServerSetting` (
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
@ -218,14 +227,14 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
FROM `ServerSetting`
""".trimIndent()
)
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
`id` INTEGER PRIMARY KEY NOT NULL,
@ -247,7 +256,7 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
)
""".trimIndent()
)
database.execSQL(
db.execSQL(
"""
INSERT INTO `_new_ServerSetting` (
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
@ -261,25 +270,31 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
FROM `ServerSetting`
""".trimIndent()
)
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
/* ktlint-disable max-line-length */
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)")
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)"
)
db.execSQL(
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
)
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)")
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
database.execSQL("DROP TABLE `ServerSetting`")
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)"
)
db.execSQL(
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
)
db.execSQL("DROP TABLE `ServerSetting`")
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
}
}
/* ktlint-enable max-line-length */

View File

@ -1,3 +1,10 @@
/*
* BasicDaos.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.data
import androidx.room.Dao
@ -91,7 +98,7 @@ interface GenericDao<T> {
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
@JvmSuppressWildcards
fun insertIgnoring(obj: List<T>?): List<Long>
fun insertIgnoring(obj: List<T>): List<Long>
/**
* Update an object from the database.
@ -109,7 +116,7 @@ interface GenericDao<T> {
*/
@Update
@JvmSuppressWildcards
fun update(obj: List<T>?)
fun update(obj: List<T>)
/**
* Delete an object from the database

View File

@ -39,9 +39,7 @@ class CachedDataSource(
)
}
private fun createDataSourceInternal(
upstreamDataSource: DataSource
): CachedDataSource {
private fun createDataSourceInternal(upstreamDataSource: DataSource): CachedDataSource {
return CachedDataSource(
upstreamDataSource
)
@ -93,7 +91,9 @@ class CachedDataSource(
readInternal(buffer, offset, length)
} catch (e: IOException) {
throw HttpDataSourceException.createForIOException(
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
e,
Util.castNonNull(dataSpec),
HttpDataSourceException.TYPE_READ
)
}
} else {

View File

@ -1,10 +1,12 @@
/*
* MetaDatabase.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@file:Suppress("ktlint:standard:max-line-length")
package org.moire.ultrasonic.data
import androidx.room.AutoMigration
@ -37,7 +39,7 @@ import org.moire.ultrasonic.domain.Track
AutoMigration(
from = 1,
to = 2
),
)
],
exportSchema = true,
version = 3
@ -67,19 +69,27 @@ class Converters {
}
}
/* ktlint-disable max-line-length */
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `albums`")
database.execSQL("DROP TABLE `indexes`")
database.execSQL("DROP TABLE `artists`")
database.execSQL("DROP TABLE `tracks`")
database.execSQL("DROP TABLE `music_folders`")
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("DROP TABLE `albums`")
db.execSQL("DROP TABLE `indexes`")
db.execSQL("DROP TABLE `artists`")
db.execSQL("DROP TABLE `tracks`")
db.execSQL("DROP TABLE `music_folders`")
db.execSQL(
"CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))"
)
db.execSQL(
"CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))"
)
}
}
/* ktlint-enable max-line-length */

View File

@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
import androidx.room.Room
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
import org.moire.ultrasonic.data.AppDatabase

View File

@ -1,7 +1,6 @@
package org.moire.ultrasonic.di
import org.koin.dsl.module
import org.moire.ultrasonic.activity.NavigationActivity
import org.moire.ultrasonic.service.ExternalStorageMonitor
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
import org.moire.ultrasonic.service.MediaPlayerManager
@ -20,8 +19,7 @@ val mediaPlayerModule = module {
single { NetworkAndStorageChecker() }
single { ShareHandler() }
scope<NavigationActivity> {
scoped { MediaPlayerManager(get(), get()) }
scoped { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
}
// These MUST be singletons, for the media playback must work headless (without an activity)
single { MediaPlayerManager(get(), get()) }
single { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
}

View File

@ -1,4 +1,5 @@
@file:JvmName("MusicServiceModule")
package org.moire.ultrasonic.di
import kotlin.math.abs

View File

@ -8,6 +8,7 @@
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIAlbumConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Album

View File

@ -8,6 +8,7 @@
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIArtistConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist

View File

@ -1,5 +1,6 @@
// Contains helper functions to convert from api ChatMessage entity to domain entity
@file:JvmName("APIChatMessageConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage

View File

@ -1,5 +1,6 @@
// Collection of functions to convert api Genre entity to domain entity
@file:JvmName("ApiGenreConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre

View File

@ -1,5 +1,6 @@
// Collection of function to convert subsonic api jukebox responses to app entities
@file:JvmName("APIJukeboxConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus

View File

@ -1,6 +1,7 @@
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APILyricsConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics

View File

@ -6,6 +6,7 @@
*/
@file:JvmName("APIMusicDirectoryConverter")
package org.moire.ultrasonic.domain
import java.text.DateFormat
@ -35,10 +36,7 @@ fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId
populateCommonProps(this, this@toAlbumEntity)
}
private fun populateCommonProps(
entry: MusicDirectory.Child,
source: MusicDirectoryChild
) {
private fun populateCommonProps(entry: MusicDirectory.Child, source: MusicDirectoryChild) {
entry.parent = source.parent
entry.isDirectory = source.isDir
entry.title = source.title
@ -63,10 +61,7 @@ private fun populateCommonProps(
}
}
private fun populateTrackProps(
track: Track,
source: MusicDirectoryChild
) {
private fun populateTrackProps(track: Track, source: MusicDirectoryChild) {
track.size = source.size
track.contentType = source.contentType
track.suffix = source.suffix
@ -84,10 +79,11 @@ fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirec
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
forEach {
if (it.isDir)
if (it.isDir) {
newList.add(it.toAlbumEntity(serverId))
else
} else {
newList.add(it.toTrackEntity(serverId))
}
}
return newList

View File

@ -8,6 +8,7 @@
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIMusicFolderConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
@ -18,9 +19,8 @@ fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
name = this.name
)
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
this.map {
val item = it.toDomainEntity(serverId)
item.serverId = serverId
item
}
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> = this.map {
val item = it.toDomainEntity(serverId)
item.serverId = serverId
item
}

View File

@ -31,8 +31,11 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
}
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
this.id, this.name, this.owner,
this.comment, this.songCount.toString(),
this.id,
this.name,
this.owner,
this.comment,
this.songCount.toString(),
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
public
)

View File

@ -1,12 +1,17 @@
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIPodcastConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
this.id, this.title, this.url, this.description, this.status
this.id,
this.title,
this.url,
this.description,
this.status
)
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this

View File

@ -8,6 +8,7 @@
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APISearchConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
@ -15,7 +16,8 @@ import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
emptyList(), emptyList(),
emptyList(),
emptyList(),
this.matchList.map { it.toTrackEntity(serverId) }
)

View File

@ -7,6 +7,7 @@
// Contains helper method to convert subsonic api share to domain model
@file:JvmName("APIShareConverter")
package org.moire.ultrasonic.domain
import java.text.SimpleDateFormat

View File

@ -1,5 +1,6 @@
// Helper functions to convert User entity to domain entity
@file:JvmName("APIUserConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.User

View File

@ -1,6 +1,6 @@
/*
* AboutFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -8,13 +8,13 @@
package org.moire.ultrasonic.fragment
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import java.util.Locale
import org.moire.ultrasonic.R
@ -60,13 +60,13 @@ class AboutFragment : Fragment() {
webPageButton?.setOnClickListener {
startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_webpage_url)))
Intent(Intent.ACTION_VIEW, getString(R.string.about_webpage_url).toUri())
)
}
reportBugButton?.setOnClickListener {
startActivity(
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_report_url)))
Intent(Intent.ACTION_VIEW, getString(R.string.about_report_url).toUri())
)
}
}

View File

@ -1,6 +1,6 @@
/*
* AlbumListFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -65,10 +65,7 @@ class AlbumListFragment(
/**
* The central function to pass a query to the model and return a LiveData object
*/
override fun getLiveData(
refresh: Boolean,
append: Boolean
): LiveData<List<Album>> {
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<Album>> {
fetchAlbums(refresh)
return listModel.list

View File

@ -17,7 +17,6 @@ import org.moire.ultrasonic.NavigationGraphDirections
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ArtistRowBinder
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.model.ArtistListModel
@ -70,7 +69,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
id = item.id,
name = item.name,
parentId = item.id,
isArtist = (item is Artist)
isArtist = false
)
} else {
NavigationGraphDirections.toAlbumList(

View File

@ -1,6 +1,6 @@
/*
* BookmarksFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -68,7 +68,6 @@ class BookmarksFragment : TrackCollectionFragment() {
*/
private fun playNow(songs: List<Track>) {
if (songs.isNotEmpty()) {
mediaPlayerManager.addToPlaylist(
songs = songs,
autoPlay = false,

View File

@ -1,6 +1,6 @@
/*
* EditServerFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -86,7 +86,8 @@ class EditServerFragment : Fragment() {
override fun onAttach(context: Context) {
requireActivity().onBackPressedDispatcher.addCallback(
this, confirmCloseCallback
this,
confirmCloseCallback
)
super.onAttach(context)
}
@ -186,7 +187,7 @@ class EditServerFragment : Fragment() {
}
)
.setNegativeButton(getString(R.string.common_cancel)) {
dialogInterface, _ ->
dialogInterface, _ ->
dialogInterface.dismiss()
}
.setBottomSpace(DIALOG_PADDING)
@ -199,7 +200,8 @@ class EditServerFragment : Fragment() {
}
private val confirmCloseCallback = object : OnBackPressedCallback(
true // default to enabled
// default to enabled
true
) {
override fun handleOnBackPressed() {
finishActivity()
@ -231,35 +233,46 @@ class EditServerFragment : Fragment() {
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
savedInstanceState.putString(
::serverNameEditText.name, serverNameEditText!!.editText?.text.toString()
::serverNameEditText.name,
serverNameEditText!!.editText?.text.toString()
)
savedInstanceState.putString(
::serverAddressEditText.name, serverAddressEditText!!.editText?.text.toString()
::serverAddressEditText.name,
serverAddressEditText!!.editText?.text.toString()
)
savedInstanceState.putString(
::userNameEditText.name, userNameEditText!!.editText?.text.toString()
::userNameEditText.name,
userNameEditText!!.editText?.text.toString()
)
savedInstanceState.putString(
::passwordEditText.name, passwordEditText!!.editText?.text.toString()
::passwordEditText.name,
passwordEditText!!.editText?.text.toString()
)
savedInstanceState.putBoolean(
::selfSignedSwitch.name, selfSignedSwitch!!.isChecked
::selfSignedSwitch.name,
selfSignedSwitch!!.isChecked
)
savedInstanceState.putBoolean(
::plaintextSwitch.name, plaintextSwitch!!.isChecked
::plaintextSwitch.name,
plaintextSwitch!!.isChecked
)
savedInstanceState.putBoolean(
::jukeboxSwitch.name, jukeboxSwitch!!.isChecked
::jukeboxSwitch.name,
jukeboxSwitch!!.isChecked
)
savedInstanceState.putInt(
::serverColorImageView.name, currentColor
::serverColorImageView.name,
currentColor
)
if (selectedColor != null)
if (selectedColor != null) {
savedInstanceState.putInt(
::selectedColor.name, selectedColor!!
::selectedColor.name,
selectedColor!!
)
}
savedInstanceState.putBoolean(
::isInstanceStateSaved.name, true
::isInstanceStateSaved.name,
true
)
super.onSaveInstanceState(savedInstanceState)
@ -286,8 +299,9 @@ class EditServerFragment : Fragment() {
plaintextSwitch!!.isChecked = savedInstanceState.getBoolean(::plaintextSwitch.name)
jukeboxSwitch!!.isChecked = savedInstanceState.getBoolean(::jukeboxSwitch.name)
updateColor(savedInstanceState.getInt(::serverColorImageView.name))
if (savedInstanceState.containsKey(::selectedColor.name))
if (savedInstanceState.containsKey(::selectedColor.name)) {
selectedColor = savedInstanceState.getInt(::selectedColor.name)
}
isInstanceStateSaved = savedInstanceState.getBoolean(::isInstanceStateSaved.name)
}
@ -434,7 +448,7 @@ class EditServerFragment : Fragment() {
serverSetting.shareSupport,
serverSetting.podcastSupport,
serverSetting.videoSupport,
serverSetting.jukeboxSupport,
serverSetting.jukeboxSupport
).any { x -> x == false }
var progressString = String.format(
@ -445,7 +459,7 @@ class EditServerFragment : Fragment() {
|%s - ${resources.getString(R.string.button_bar_podcasts)}
|%s - ${resources.getString(R.string.main_videos)}
|%s - ${resources.getString(R.string.jukebox)}
""".trimMargin(),
""".trimMargin(),
boolToMark(serverSetting.chatSupport),
boolToMark(serverSetting.bookmarkSupport),
boolToMark(serverSetting.shareSupport),
@ -453,15 +467,17 @@ class EditServerFragment : Fragment() {
boolToMark(serverSetting.videoSupport),
boolToMark(serverSetting.jukeboxSupport)
)
if (isAnyDisabled)
if (isAnyDisabled) {
progressString += "\n\n" + resources.getString(R.string.server_editor_disabled_feature)
}
return progressString
}
private fun boolToMark(value: Boolean?): String {
if (value == null)
if (value == null) {
return ""
}
return if (value) "✔️" else ""
}

View File

@ -54,7 +54,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), Koi
id = item.id,
name = item.name,
parentId = item.id,
isArtist = (item is Artist),
isArtist = (item is Artist)
)
findNavController().navigate(action)

View File

@ -92,7 +92,9 @@ class EqualizerFragment : Fragment() {
}
for (preset in 0 until equalizer!!.numberOfPresets) {
val menuItem = menu.add(
MENU_GROUP_PRESET, preset, preset,
MENU_GROUP_PRESET,
preset,
preset,
equalizer!!.getPresetName(
preset.toShort()
)
@ -188,11 +190,7 @@ class EqualizerFragment : Fragment() {
updateLevelText(levelTextView, bandLevel)
bar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(
seekBar: SeekBar,
progress: Int,
fromUser: Boolean
) {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
val level = (progress + minEQLevel).toShort()
if (fromUser) {
try {

View File

@ -1,3 +1,10 @@
/*
* FragmentTitle.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import androidx.appcompat.app.AppCompatActivity

View File

@ -58,7 +58,6 @@ class MainFragment : ScopeFragment(), KoinScopeComponent {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
FragmentTitle.setTitle(this, R.string.music_library_label)
// Load last layout from settings
@ -133,10 +132,7 @@ class MainFragment : ScopeFragment(), KoinScopeComponent {
return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
}
private fun findFragmentAtPosition(
fragmentManager: FragmentManager,
position: Int
): Fragment? {
private fun findFragmentAtPosition(fragmentManager: FragmentManager, position: Int): Fragment? {
// If a fragment was recently created and never shown the fragment manager might not
// hold a reference to it. Fallback on the WeakMap instead.
return fragmentManager.findFragmentByTag("f$position")
@ -172,7 +168,6 @@ class MusicCollectionAdapter(fragment: Fragment, initialType: LayoutType = Layou
}
override fun createFragment(position: Int): Fragment {
Timber.i("Creating new fragment at position: $position")
val action = when (position) {

View File

@ -1,6 +1,6 @@
/*
* MultiListFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -96,9 +96,11 @@ abstract class MultiListFragment<T : Identifiable> : ScopeFragment(), Refreshabl
if (title == null) {
FragmentTitle.setTitle(
this,
if (listModel.isOffline())
if (listModel.isOffline()) {
R.string.music_library_label_offline
else R.string.music_library_label
} else {
R.string.music_library_label
}
)
} else {
FragmentTitle.setTitle(this, title)

View File

@ -1,6 +1,6 @@
/*
* PlayerFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -8,7 +8,6 @@
package org.moire.ultrasonic.fragment
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color.argb
import android.graphics.Point
@ -18,7 +17,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.Menu
@ -31,15 +29,12 @@ import android.view.WindowManager
import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import android.widget.Toast
import android.widget.ViewFlipper
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.MenuHost
@ -105,6 +100,7 @@ import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.themeColor
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.util.toTrack
import org.moire.ultrasonic.view.AutoRepeatButton
@ -125,7 +121,6 @@ class PlayerFragment :
private var swipeDistance = 0
private var swipeVelocity = 0
private var jukeboxAvailable = false
private var useFiveStarRating = false
private var isEqualizerAvailable = false
// Detectors & Callbacks
@ -151,6 +146,7 @@ class PlayerFragment :
private lateinit var fiveStar3ImageView: ImageView
private lateinit var fiveStar4ImageView: ImageView
private lateinit var fiveStar5ImageView: ImageView
private lateinit var heartRatingImageView: ImageView
private lateinit var playlistFlipper: ViewFlipper
private lateinit var emptyTextView: TextView
private lateinit var emptyView: ConstraintLayout
@ -174,12 +170,18 @@ class PlayerFragment :
private lateinit var repeatButton: MaterialButton
private lateinit var progressBar: SeekBar
private lateinit var progressIndicator: CircularProgressIndicator
private val hollowStar = R.drawable.star_hollow_outline
private val fullStar = R.drawable.star_full_outline
private val hollowStar = R.drawable.rating_star_hollow_layered
private val fullStar = R.drawable.rating_star_full_layered
private val hollowHeart = R.drawable.rating_heart_hollow_layered
private val fullHeart = R.drawable.rating_heart_full_layered
private lateinit var hollowStarDrawable: Drawable
private lateinit var fullStarDrawable: Drawable
private lateinit var hollowHeartDrawable: Drawable
private lateinit var fullHeartDrawable: Drawable
private var _binding: CurrentPlayingBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
@ -232,6 +234,7 @@ class PlayerFragment :
fiveStar3ImageView = view.findViewById(R.id.song_five_star_3)
fiveStar4ImageView = view.findViewById(R.id.song_five_star_4)
fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
heartRatingImageView = view.findViewById(R.id.song_rating_heart)
}
@Suppress("LongMethod")
@ -264,7 +267,6 @@ class PlayerFragment :
Lifecycle.State.RESUMED
)
useFiveStarRating = Settings.useFiveStarRating
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100
swipeVelocity = swipeDistance
gestureScanner = GestureDetector(context, this)
@ -276,19 +278,26 @@ class PlayerFragment :
updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled)
updateRepeatButtonState(mediaPlayerManager.repeatMode)
val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating)
if (!useFiveStarRating) ratingLinearLayout.isVisible = false
hollowStarDrawable = ResourcesCompat.getDrawable(resources, hollowStar, null)!!
fullStarDrawable = ResourcesCompat.getDrawable(resources, fullStar, null)!!
setLayerDrawableColors(hollowStarDrawable as LayerDrawable)
setLayerDrawableColors(fullStarDrawable as LayerDrawable)
hollowHeartDrawable = ResourcesCompat.getDrawable(resources, hollowHeart, null)!!
fullHeartDrawable = ResourcesCompat.getDrawable(resources, fullHeart, null)!!
setLayerDrawableColors(hollowHeartDrawable as LayerDrawable)
setLayerDrawableColors(
fullHeartDrawable as LayerDrawable,
RM.attr.colorAccent,
RM.attr.colorSurface
)
fiveStar1ImageView.setOnClickListener { setSongRating(1) }
fiveStar2ImageView.setOnClickListener { setSongRating(2) }
fiveStar3ImageView.setOnClickListener { setSongRating(3) }
fiveStar4ImageView.setOnClickListener { setSongRating(4) }
fiveStar5ImageView.setOnClickListener { setSongRating(5) }
heartRatingImageView.setOnClickListener { setSongHeartRating() }
albumArtImageView.setOnTouchListener { _, me ->
gestureScanner.onTouchEvent(me)
@ -333,8 +342,9 @@ class PlayerFragment :
}
playButton.setOnClickListener {
if (!mediaPlayerManager.isJukeboxEnabled)
if (!mediaPlayerManager.isJukeboxEnabled) {
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
}
launch(CommunicationError.getHandler(context)) {
mediaPlayerManager.play()
@ -407,6 +417,21 @@ class PlayerFragment :
updateButtonStates(it.state)
}
rxBusSubscription += RxBus.ratingPublishedObservable.subscribe { update ->
// Ignore updates which are not for the current song
if (update.id != currentSong?.id) return@subscribe
// Ensure UI thread
launch {
if (update.success == false) {
Toast.makeText(context, "Setting rating failed", Toast.LENGTH_SHORT)
.show()
} else {
updateSongRatingDisplay()
}
}
}
// Query the Jukebox state in an IO Context
ioScope.launch(CommunicationError.getHandler(context)) {
try {
@ -535,38 +560,13 @@ class PlayerFragment :
val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer)
val shareMenuItem = menu.findItem(R.id.menu_item_share)
val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song)
val starMenuItem = menu.findItem(R.id.menu_item_star)
val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set)
val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete)
// Listen to rating changes and update the UI
rxBusSubscription += RxBus.ratingPublishedObservable.subscribe { update ->
// Ignore updates which are not for the current song
if (update.id != currentSong?.id) return@subscribe
// Ensure UI thread
launch {
if (update.success == true && update.rating is HeartRating) {
if (update.rating.isHeart) {
starMenuItem.setIcon(fullStar)
starMenuItem.setTitle(R.string.download_menu_unstar)
} else {
starMenuItem.setIcon(hollowStar)
starMenuItem.setTitle(R.string.download_menu_star)
}
} else if (update.success == false) {
Toast.makeText(context, "Setting rating failed", Toast.LENGTH_SHORT)
.show()
}
}
}
if (isOffline()) {
if (shareMenuItem != null) {
shareMenuItem.isVisible = false
}
starMenuItem.isVisible = false
if (bookmarkMenuItem != null) {
bookmarkMenuItem.isVisible = false
}
@ -585,15 +585,11 @@ class PlayerFragment :
currentSong = track
}
if (useFiveStarRating) starMenuItem.isVisible = false
if (currentSong != null) {
starMenuItem.setIcon(if (currentSong!!.starred) fullStar else hollowStar)
shareSongMenuItem.isVisible = true
goToAlbum.isVisible = true
goToArtist.isVisible = true
} else {
starMenuItem.setIcon(hollowStar)
shareSongMenuItem.isVisible = false
goToAlbum.isVisible = false
goToArtist.isVisible = false
@ -637,10 +633,7 @@ class PlayerFragment :
return popup
}
private fun onContextMenuItemSelected(
menuItem: MenuItem,
item: MusicDirectory.Child
): Boolean {
private fun onContextMenuItemSelected(menuItem: MenuItem, item: MusicDirectory.Child): Boolean {
if (item !is Track) return false
return menuItemSelected(menuItem.itemId, item)
}
@ -707,8 +700,11 @@ class PlayerFragment :
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
toast(
if (jukeboxEnabled) R.string.download_jukebox_on
else R.string.download_jukebox_off,
if (jukeboxEnabled) {
R.string.download_jukebox_on
} else {
R.string.download_jukebox_off
},
false
)
return true
@ -729,16 +725,6 @@ class PlayerFragment :
}
return true
}
R.id.menu_item_star -> {
if (track == null) return true
track.starred = !track.starred
RxBus.ratingSubmitter.onNext(
RatingUpdate(track.id, HeartRating(track.starred))
)
return true
}
R.id.menu_item_bookmark_set -> {
if (track == null) return true
@ -783,7 +769,7 @@ class PlayerFragment :
}
shareHandler.createShare(
this,
tracks = tracks,
tracks = tracks
)
return true
}
@ -792,7 +778,7 @@ class PlayerFragment :
shareHandler.createShare(
this,
listOf(track),
listOf(track)
)
return true
}
@ -876,7 +862,7 @@ class PlayerFragment :
onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id) },
checkable = false,
draggable = true,
lifecycleOwner = viewLifecycleOwner,
lifecycleOwner = viewLifecycleOwner
) { view, track -> onCreateContextMenu(view, track) }.apply {
this.startDrag = { holder ->
dragTouchHelper.startDrag(holder)
@ -898,7 +884,6 @@ class PlayerFragment :
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
@ -951,10 +936,7 @@ class PlayerFragment :
mediaPlayerManager.removeFromPlaylist(pos)
}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ACTION_STATE_DRAG) {
@ -1009,8 +991,10 @@ class PlayerFragment :
if (dX > 0) {
canvas.clipRect(
itemView.left.toFloat(), itemView.top.toFloat(),
dX, itemView.bottom.toFloat()
itemView.left.toFloat(),
itemView.top.toFloat(),
dX,
itemView.bottom.toFloat()
)
canvas.drawColor(backgroundColor)
val left = itemView.left + Util.dpToPx(16, activity!!)
@ -1019,8 +1003,10 @@ class PlayerFragment :
drawable?.draw(canvas)
} else {
canvas.clipRect(
itemView.right.toFloat() + dX, itemView.top.toFloat(),
itemView.right.toFloat(), itemView.bottom.toFloat(),
itemView.right.toFloat() + dX,
itemView.top.toFloat(),
itemView.right.toFloat(),
itemView.bottom.toFloat()
)
canvas.drawColor(backgroundColor)
val left = itemView.right - Util.dpToPx(16, activity!!) - iconSize
@ -1034,7 +1020,13 @@ class PlayerFragment :
viewHolder.itemView.translationX = dX
} else {
super.onChildDraw(
canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive
canvas,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
@ -1070,8 +1062,9 @@ class PlayerFragment :
songTitleTextView.text = currentSong!!.title
artistTextView.text = currentSong!!.artist
albumTextView.text = currentSong!!.album
if (currentSong!!.year != null && Settings.showNowPlayingDetails)
if (currentSong!!.year != null && Settings.showNowPlayingDetails) {
albumTextView.append(String.format(Locale.ROOT, " (%d)", currentSong!!.year))
}
if (Settings.showNowPlayingDetails) {
genreTextView.text = currentSong!!.genre
@ -1079,11 +1072,12 @@ class PlayerFragment :
(currentSong!!.genre != null && currentSong!!.genre!!.isNotBlank())
var bitRate = ""
if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0)
if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0) {
bitRate = String.format(
Util.appContext().getString(R.string.song_details_kbps),
currentSong!!.bitRate
)
}
bitrateFormatTextView.text = String.format(
Locale.ROOT, "%s %s",
bitRate, currentSong!!.suffix
@ -1278,30 +1272,36 @@ class PlayerFragment :
private fun updateSongRatingDisplay() {
val rating = currentSong?.userRating ?: 0
val isHeartSet = currentSong?.starred ?: false
fiveStar1ImageView.setImageDrawable(getStarForRating(rating, 0))
fiveStar2ImageView.setImageDrawable(getStarForRating(rating, 1))
fiveStar3ImageView.setImageDrawable(getStarForRating(rating, 2))
fiveStar4ImageView.setImageDrawable(getStarForRating(rating, 3))
fiveStar5ImageView.setImageDrawable(getStarForRating(rating, 4))
if (isHeartSet) {
heartRatingImageView.setImageDrawable(fullHeartDrawable)
} else {
heartRatingImageView.setImageDrawable(hollowHeartDrawable)
}
}
private fun getStarForRating(rating: Int, position: Int): Drawable {
return if (rating > position) fullStarDrawable else hollowStarDrawable
}
private fun setLayerDrawableColors(drawable: LayerDrawable) {
private fun setLayerDrawableColors(
drawable: LayerDrawable,
innerColor: Int = RM.attr.colorSurface,
borderColor: Int = RM.attr.colorAccent
) {
drawable.apply {
getDrawable(0).setTint(requireContext().themeColor(RM.attr.colorSurface))
getDrawable(1).setTint(requireContext().themeColor(RM.attr.colorAccent))
getDrawable(0).setTint(requireContext().themeColor(innerColor))
getDrawable(1).setTint(requireContext().themeColor(borderColor))
}
}
@ColorInt
fun Context.themeColor(@AttrRes attrRes: Int): Int = TypedValue()
.apply { theme.resolveAttribute(attrRes, this, true) }
.data
private fun setSongRating(rating: Int) {
if (currentSong == null) return
currentSong?.userRating = rating
@ -1315,6 +1315,19 @@ class PlayerFragment :
)
}
private fun setSongHeartRating() {
if (currentSong == null) return
currentSong?.starred = !(currentSong?.starred ?: true)
updateSongRatingDisplay()
RxBus.ratingSubmitter.onNext(
RatingUpdate(
currentSong!!.id,
HeartRating(currentSong?.starred ?: false)
)
)
}
@SuppressLint("InflateParams")
private fun showSavePlaylistDialog() {
val layout = LayoutInflater.from(this.context)

View File

@ -1,6 +1,6 @@
/*
* SearchFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -41,7 +41,6 @@ import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
import org.moire.ultrasonic.util.RefreshableFragment
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.Util.toast
import org.moire.ultrasonic.util.toastingExceptionHandler
@ -143,7 +142,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, Re
val artists = result.artists
if (artists.isNotEmpty()) {
list.add(DividerBinder.Divider(R.string.search_artists))
list.addAll(artists)
if (searchResult!!.artists.size > artists.size) {
@ -283,10 +281,4 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, Re
)
}
}
companion object {
var DEFAULT_ARTISTS = Settings.defaultArtists
var DEFAULT_ALBUMS = Settings.defaultAlbums
var DEFAULT_SONGS = Settings.defaultSongs
}
}

View File

@ -1,14 +1,21 @@
/*
* SettingsFragment.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.net.Uri
import android.os.Bundle
import android.provider.SearchRecentSuggestions
import android.view.View
import androidx.annotation.StringRes
import androidx.core.net.toUri
import androidx.preference.CheckBoxPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -17,7 +24,6 @@ import androidx.preference.PreferenceFragmentCompat
import java.io.File
import kotlin.math.ceil
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
@ -28,7 +34,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileSizes
import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
import org.moire.ultrasonic.provider.SearchSuggestionProvider
import org.moire.ultrasonic.service.MediaPlayerManager
import org.moire.ultrasonic.service.DownloadService
import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.Constants
@ -62,8 +68,6 @@ class SettingsFragment :
private var debugLogToFile: CheckBoxPreference? = null
private var customCacheLocation: CheckBoxPreference? = null
private val mediaPlayerManager: MediaPlayerManager by inject()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.settings, rootKey)
}
@ -190,7 +194,7 @@ class SettingsFragment :
}
cacheLocation?.isVisible = true
val uri = Uri.parse(Settings.cacheLocationUri)
val uri = Settings.cacheLocationUri.toUri()
cacheLocation!!.summary = uri.path
cacheLocation!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
selectCacheLocation()
@ -261,7 +265,8 @@ class SettingsFragment :
val choice = intArrayOf(defaultChoice)
ConfirmationDialog.Builder(requireContext()).setTitle(title)
.setSingleChoiceItems(
R.array.bluetoothDeviceSettingNames, defaultChoice
R.array.bluetoothDeviceSettingNames,
defaultChoice
) { _: DialogInterface?, i: Int -> choice[0] = i }
.setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int ->
dialogInterface.cancel()
@ -337,14 +342,14 @@ class SettingsFragment :
private fun setCacheLocation(path: String) {
if (path != "") {
val uri = Uri.parse(path)
val uri = path.toUri()
cacheLocation!!.summary = uri.path ?: ""
}
Settings.cacheLocationUri = path
// Clear download queue.
mediaPlayerManager.clear()
DownloadService.clearDownloads()
Storage.reset()
Storage.checkForErrorsWithCustomRoot()
}

View File

@ -1,6 +1,6 @@
/*
* TrackCollectionFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -365,9 +365,7 @@ open class TrackCollectionFragment(
)
}
private fun playSelectedOrAllTracks(
insertionMode: MediaPlayerManager.InsertionMode
) {
private fun playSelectedOrAllTracks(insertionMode: MediaPlayerManager.InsertionMode) {
mediaPlayerManager.playTracksAndToast(
fragment = this,
insertionMode = insertionMode,
@ -403,9 +401,7 @@ open class TrackCollectionFragment(
listModel.calculateButtonState(selection, ::updateButtonState)
}
private fun updateButtonState(
show: TrackCollectionModel.Companion.ButtonStates,
) {
private fun updateButtonState(show: TrackCollectionModel.Companion.ButtonStates) {
// We are coming back from unknown context
// and need to ensure Main Thread in order to manipulate the UI
// If view is null, our view was disposed in the meantime
@ -484,10 +480,11 @@ open class TrackCollectionFragment(
internal fun getSelectedTracks(): List<Track> {
// Walk through selected set and get the Entries based on the saved ids.
return viewAdapter.getCurrentList().mapNotNull {
if (it is Track && viewAdapter.isSelected(it.longId))
if (it is Track && viewAdapter.isSelected(it.longId)) {
it
else
} else {
null
}
}
}
@ -586,7 +583,6 @@ open class TrackCollectionFragment(
menuItem: MenuItem,
item: MusicDirectory.Child
): Boolean {
val tracks = getClickedSong(item)
return ContextMenuUtil.handleContextMenuTracks(
@ -600,10 +596,11 @@ open class TrackCollectionFragment(
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
// This can probably be done better
return viewAdapter.getCurrentList().mapNotNull {
if (it is Track && (it.id == item.id))
if (it is Track && (it.id == item.id)) {
it
else
} else {
null
}
}
}

View File

@ -112,9 +112,10 @@ class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
})
messageEditText.setOnEditorActionListener(
OnEditorActionListener {
_: TextView?,
actionId: Int,
event: KeyEvent ->
_: TextView?,
actionId: Int,
event: KeyEvent
->
if (actionId == EditorInfo.IME_ACTION_SEND ||
(actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN)
) {
@ -170,7 +171,8 @@ class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
requireActivity().runOnUiThread { load() }
}
},
refreshInterval.toLong(), refreshInterval.toLong()
refreshInterval.toLong(),
refreshInterval.toLong()
)
}
}

View File

@ -1,6 +1,6 @@
/*
* LyricsFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/

View File

@ -1,6 +1,6 @@
/*
* PlaylistsFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -87,7 +87,7 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
id = id,
playlistId = id,
name = name,
playlistName = name,
playlistName = name
)
findNavController().navigate(action)
}
@ -120,10 +120,14 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
super.onCreateContextMenu(menu, view, menuInfo)
val inflater = requireActivity().menuInflater
if (isOffline()) inflater.inflate(
R.menu.select_playlist_context_offline,
menu
) else inflater.inflate(R.menu.select_playlist_context, menu)
if (isOffline()) {
inflater.inflate(
R.menu.select_playlist_context_offline,
menu
)
} else {
inflater.inflate(R.menu.select_playlist_context, menu)
}
val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
if (downloadMenuItem != null) {
downloadMenuItem.isVisible = !isOffline()
@ -236,13 +240,17 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
Comments: ${playlist.comment}
Song Count: ${playlist.songCount}
""".trimIndent() +
if (playlist.public == null) "" else """
if (playlist.public == null) {
""
} else {
"""
Public: ${playlist.public}
""".trimIndent() + """
""".trimIndent() + """
Creation Date: ${playlist.created.replace('T', ' ')}
""".trimIndent()
""".trimIndent()
}
)
Linkify.addLinks(message, Linkify.WEB_URLS)
textView.text = message

View File

@ -1,6 +1,6 @@
/*
* PodcastFragment.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/

View File

@ -1,3 +1,10 @@
/*
* SelectGenreFragment.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.fragment.legacy
import android.os.Bundle
@ -53,8 +60,8 @@ class SelectGenreFragment : Fragment(), RefreshableFragment {
swipeRefresh?.setOnRefreshListener { load(true) }
genreListView?.setOnItemClickListener {
parent: AdapterView<*>, _: View?,
position: Int, _: Long
parent: AdapterView<*>, _: View?,
position: Int, _: Long
->
val genre = parent.getItemAtPosition(position) as Genre

View File

@ -80,8 +80,9 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
swipeRefresh!!.setOnRefreshListener { load(true) }
emptyTextView = view.findViewById(R.id.select_share_empty)
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
parent, _,
position, _ ->
parent, _,
position, _
->
val share = parent.getItemAtPosition(position) as Share
val action = NavigationGraphDirections.toTrackCollection(
@ -171,7 +172,7 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
id = share.id,
name = share.name,
shuffle = true,
shuffle = true
)
}
R.id.share_menu_delete -> {
@ -235,21 +236,33 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
Visit Count: ${share.visitCount}
""".trimIndent() +
(
if (share.created == null) "" else """
if (share.created == null) {
""
} else {
"""
Creation Date: ${share.created!!.replace('T', ' ')}
""".trimIndent()
""".trimIndent()
}
) +
(
if (share.lastVisited == null) "" else """
if (share.lastVisited == null) {
""
} else {
"""
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
""".trimIndent()
""".trimIndent()
}
) +
if (share.expires == null) "" else """
if (share.expires == null) {
""
} else {
"""
Expiration Date: ${share.expires!!.replace('T', ' ')}
""".trimIndent()
""".trimIndent()
}
)
Linkify.addLinks(message, Linkify.WEB_URLS)
textView.text = message
@ -289,11 +302,7 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
alertDialog.show()
}
private fun updateShareOnServer(
millis: Long,
description: String,
share: Share
) {
private fun updateShareOnServer(millis: Long, description: String, share: Share) {
launchWithToast {
withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService()

View File

@ -1,6 +1,6 @@
/*
* ArtworkBitmapLoader.kt
* Copyright (C) 2009-2022 Ultrasonic developers
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -32,6 +32,11 @@ class ArtworkBitmapLoader : BitmapLoader, KoinComponent {
)
}
override fun supportsMimeType(mimeType: String): Boolean {
// TODO: Implement?
return true
}
override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> {
return executorService.submit<Bitmap> {
decode(

View File

@ -2,7 +2,6 @@ package org.moire.ultrasonic.imageloader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import com.squareup.picasso.Picasso.LoadedFrom.DISK
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
import com.squareup.picasso.Request
@ -65,10 +64,7 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
throw IOException("${response.apiError}")
}
private fun getAlbumArtBitmapFromDisk(
filename: String,
size: Int?
): Bitmap? {
private fun getAlbumArtBitmapFromDisk(filename: String, size: Int?): Bitmap? {
val albumArtFile = FileUtil.getAlbumArtFile(filename)
val bitmap: Bitmap? = null
if (File(albumArtFile).exists()) {
@ -77,11 +73,7 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
return null
}
private fun getBitmapFromDisk(
path: String,
size: Int?,
bitmap: Bitmap?
): Bitmap? {
private fun getBitmapFromDisk(path: String, size: Int?, bitmap: Bitmap?): Bitmap? {
var bitmap1 = bitmap
val opt = BitmapFactory.Options()
if (size != null && size > 0) {
@ -92,12 +84,6 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
BitmapFactory.decodeFile(path, opt)
// Now set the remaining flags
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
opt.inDither = true
opt.inPreferQualityOverSpeed = true
}
opt.inSampleSize = Util.calculateInSampleSize(
opt,
size,

View File

@ -1,3 +1,10 @@
/*
* ImageLoader.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.imageloader
import android.app.ActivityManager
@ -38,7 +45,7 @@ import timber.log.Timber
class ImageLoader(
context: Context,
apiClient: SubsonicAPIClient,
private val config: ImageLoaderConfig,
private val config: ImageLoaderConfig
) : CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
@ -112,7 +119,10 @@ class ImageLoader(
val requestedSize = resolveSize(size, large)
val request = ImageRequest.CoverArt(
id!!, cacheKey!!, null, requestedSize,
id!!,
cacheKey!!,
null,
requestedSize,
placeHolderDrawableRes = defaultResourceId,
errorDrawableRes = defaultResourceId
)
@ -157,7 +167,10 @@ class ImageLoader(
if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
val request = ImageRequest.CoverArt(
id, key, view, requestedSize,
id,
key,
view,
requestedSize,
placeHolderDrawableRes = defaultResourceId,
errorDrawableRes = defaultResourceId
)
@ -170,13 +183,11 @@ class ImageLoader(
/**
* Load the avatar of a given user into an ImageView
*/
fun loadAvatarImage(
view: ImageView,
username: String
) {
fun loadAvatarImage(view: ImageView, username: String) {
if (username.isNotEmpty()) {
val request = ImageRequest.Avatar(
username, view,
username,
view,
placeHolderDrawableRes = R.drawable.ic_contact_picture,
errorDrawableRes = R.drawable.ic_contact_picture
)
@ -284,7 +295,7 @@ sealed class ImageRequest(
imageView: ImageView?,
val size: Int,
placeHolderDrawableRes: Int? = null,
errorDrawableRes: Int? = null,
errorDrawableRes: Int? = null
) : ImageRequest(
placeHolderDrawableRes,
errorDrawableRes,

View File

@ -13,17 +13,15 @@ internal const val QUERY_USERNAME = "username"
* Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which
* we encode the data that we need in the RequestHandler.
*/
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
Uri.Builder()
.scheme(SCHEME)
.appendPath(COVER_ART_PATH)
.appendQueryParameter(QUERY_ID, entityId)
.appendQueryParameter(SIZE, size.toString())
.build()
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = Uri.Builder()
.scheme(SCHEME)
.appendPath(COVER_ART_PATH)
.appendQueryParameter(QUERY_ID, entityId)
.appendQueryParameter(SIZE, size.toString())
.build()
internal fun createLoadAvatarRequest(username: String): Uri =
Uri.Builder()
.scheme(SCHEME)
.appendPath(AVATAR_PATH)
.appendQueryParameter(QUERY_USERNAME, username)
.build()
internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder()
.scheme(SCHEME)
.appendPath(AVATAR_PATH)
.appendQueryParameter(QUERY_USERNAME, username)
.build()

View File

@ -22,11 +22,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
private var lastType: AlbumListType? = null
private var loadedUntil: Int = 0
suspend fun getAlbumsOfArtist(
refresh: Boolean,
id: String,
name: String?
) {
suspend fun getAlbumsOfArtist(refresh: Boolean, id: String, name: String?) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
list.postValue(service.getAlbumsOfArtist(id, name, refresh))

View File

@ -59,36 +59,33 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone
return (this.status === SubsonicResponse.Status.OK)
}
private fun requestFlow(
type: ServerFeature,
api: SubsonicAPIDefinition,
userName: String
) = flow {
when (type) {
ServerFeature.CHAT -> emit(
serverFunctionAvailable(type, api::getChatMessagesSuspend)
)
ServerFeature.BOOKMARK -> emit(
serverFunctionAvailable(type, api::getBookmarksSuspend)
)
ServerFeature.SHARE -> emit(
serverFunctionAvailable(type, api::getSharesSuspend)
)
ServerFeature.PODCAST -> emit(
serverFunctionAvailable(type, api::getPodcastsSuspend)
)
ServerFeature.JUKEBOX -> emit(
serverFunctionAvailable(type) {
val response = api.getUserSuspend(userName)
if (!response.user.jukeboxRole) throw IOException()
response
}
)
ServerFeature.VIDEO -> emit(
serverFunctionAvailable(type, api::getVideosSuspend)
)
private fun requestFlow(type: ServerFeature, api: SubsonicAPIDefinition, userName: String) =
flow {
when (type) {
ServerFeature.CHAT -> emit(
serverFunctionAvailable(type, api::getChatMessagesSuspend)
)
ServerFeature.BOOKMARK -> emit(
serverFunctionAvailable(type, api::getBookmarksSuspend)
)
ServerFeature.SHARE -> emit(
serverFunctionAvailable(type, api::getSharesSuspend)
)
ServerFeature.PODCAST -> emit(
serverFunctionAvailable(type, api::getPodcastsSuspend)
)
ServerFeature.JUKEBOX -> emit(
serverFunctionAvailable(type) {
val response = api.getUserSuspend(userName)
if (!response.user.jukeboxRole) throw IOException()
response
}
)
ServerFeature.VIDEO -> emit(
serverFunctionAvailable(type, api::getVideosSuspend)
)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {

View File

@ -67,10 +67,7 @@ open class GenericListModel(application: Application) :
/**
* Trigger a load() and notify the UI that we are loading
*/
fun backgroundLoadFromServer(
refresh: Boolean,
swipe: SwipeRefreshLayout
) {
fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
viewModelScope.launch {
swipe.isRefreshing = true
loadFromServer(refresh, swipe)
@ -81,10 +78,7 @@ open class GenericListModel(application: Application) :
/**
* Calls the load() function with error handling
*/
private suspend fun loadFromServer(
refresh: Boolean,
swipe: SwipeRefreshLayout
) {
private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
withContext(Dispatchers.IO) {
val musicService = MusicServiceFactory.getMusicService()
val isOffline = ActiveServerProvider.isOffline()

View File

@ -1,3 +1,10 @@
/*
* SearchListModel.kt
* Copyright (C) 2009-2023 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.model
import android.app.Application
@ -6,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.fragment.SearchFragment
import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Settings
@ -31,9 +37,9 @@ class SearchListModel(application: Application) : GenericListModel(application)
fun trimResultLength(
result: SearchResult,
maxArtists: Int = SearchFragment.DEFAULT_ARTISTS,
maxAlbums: Int = SearchFragment.DEFAULT_ALBUMS,
maxSongs: Int = SearchFragment.DEFAULT_SONGS
maxArtists: Int = Settings.defaultArtists,
maxAlbums: Int = Settings.defaultAlbums,
maxSongs: Int = Settings.defaultSongs
): SearchResult {
return SearchResult(
artists = result.artists.take(maxArtists),

View File

@ -30,13 +30,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
private var loadedUntil: Int = 0
/*
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
*/
suspend fun getMusicDirectory(
refresh: Boolean,
id: String,
name: String?
) {
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
*/
suspend fun getMusicDirectory(refresh: Boolean, id: String, name: String?) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getMusicDirectory(id, name, refresh)
@ -46,9 +42,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
suspend fun getAlbum(refresh: Boolean, id: String, name: String?) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory: MusicDirectory = service.getAlbumAsDir(id, name, refresh)
currentListIsSortable = true
@ -74,9 +68,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
suspend fun getStarred() {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory: MusicDirectory
@ -122,7 +114,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
suspend fun getPodcastEpisodes(podcastChannelId: String) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
@ -134,7 +125,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
suspend fun getShare(shareId: String) {
withContext(Dispatchers.IO) {
val service = MusicServiceFactory.getMusicService()
val musicDirectory = MusicDirectory()
@ -174,10 +164,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
}
@Synchronized
fun calculateButtonState(
selection: List<Track>,
onComplete: (ButtonStates) -> Unit
) {
fun calculateButtonState(selection: List<Track>, onComplete: (ButtonStates) -> Unit) {
val enabled = selection.isNotEmpty()
var unpinEnabled = false
var deleteEnabled = false

View File

@ -38,7 +38,9 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
.path(
String.format(
Locale.ROOT,
"%s|%s", track!!.coverArt, FileUtil.getAlbumArtKey(track, true)
"%s|%s",
track!!.coverArt,
FileUtil.getAlbumArtKey(track, true)
)
)
.build()

View File

@ -13,7 +13,6 @@ import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.KeyEvent
@ -160,11 +159,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
/**
* Update Track details in widgets
*/
private fun updateTrack(
context: Context,
views: RemoteViews,
currentSong: Track?
) {
private fun updateTrack(context: Context, views: RemoteViews, currentSong: Track?) {
Timber.d("Updating Widget")
val res = context.resources
val title = currentSong?.title
@ -222,10 +217,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
intent.action = Intent.ACTION_MAIN
intent.addCategory(Intent.CATEGORY_LAUNCHER)
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
// needed starting Android 12 (S = 31)
flags = flags or PendingIntent.FLAG_IMMUTABLE
var pendingIntent =
PendingIntent.getActivity(context, 10, intent, flags)
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent)
@ -239,10 +232,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
)
flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// needed starting Android 12 (S = 31)
flags = PendingIntent.FLAG_IMMUTABLE
}
// needed starting Android 12 (S = 31)
flags = PendingIntent.FLAG_IMMUTABLE
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
intent = Intent(Constants.CMD_PROCESS_KEYCODE)

Some files were not shown because too many files have changed in this diff Show More