Compare commits
247 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
499644d041 | ||
|
|
21ed78d959 | ||
|
|
5ad99b9f27 | ||
|
|
3de5390140 | ||
|
|
d215581e19 | ||
|
|
54612c6b74 | ||
|
|
eaac728a26 | ||
|
|
65d2f8e33f | ||
|
|
baf4e0f0fc | ||
|
|
26c7bee106 | ||
|
|
6e51611867 | ||
|
|
d67e432731 | ||
|
|
8b61396b0f | ||
|
|
0fb6e55b12 | ||
|
|
dd7aa2291b | ||
|
|
ec33c32c89 | ||
|
|
e0ad4e3701 | ||
|
|
253f8033c5 | ||
|
|
c1aed1a4c1 | ||
|
|
23f58439ba | ||
|
|
4c99ced597 | ||
|
|
8d215a7f1c | ||
|
|
38fc4a0936 | ||
|
|
d9949349da | ||
|
|
877d29d285 | ||
|
|
9a17aa8b98 | ||
|
|
fd41395ab8 | ||
|
|
269066e036 | ||
|
|
488460ea9d | ||
|
|
d16a9c234f | ||
|
|
07b507691c | ||
|
|
bde34d3df0 | ||
|
|
e5b7756f96 | ||
|
|
04e692e5e9 | ||
|
|
a23a663d32 | ||
|
|
023bd8071a | ||
|
|
72b1517f61 | ||
|
|
e62ea72c2f | ||
|
|
a24ccf2556 | ||
|
|
49838e2e0f | ||
|
|
8ed1248ee1 | ||
|
|
e9d54957ae | ||
|
|
3cd5843c4b | ||
|
|
75513d3bd4 | ||
|
|
c0c84269ef | ||
|
|
fa2e029f9f | ||
|
|
f1bfb095b7 | ||
|
|
4328415efc | ||
|
|
092ae14ea2 | ||
|
|
26af8a692f | ||
|
|
b870f4c866 | ||
|
|
cf4e78eafc | ||
|
|
83e23c44d9 | ||
|
|
c0959c7ca4 | ||
|
|
e77f3bf9b3 | ||
|
|
55265615e6 | ||
|
|
bd872fc23d | ||
|
|
64a1966ad8 | ||
|
|
5ef5731fe3 | ||
|
|
c5cece8477 | ||
|
|
bae9221070 | ||
|
|
c0dbe01bf9 | ||
|
|
5f550b0df4 | ||
|
|
6100c3e7f1 | ||
|
|
f01ca9fed0 | ||
|
|
d232ebfa6f | ||
|
|
53ca88989f | ||
|
|
a82cf70433 | ||
|
|
89aa18b5f0 | ||
|
|
431014adc4 | ||
|
|
6110a9c8e7 | ||
|
|
993374e56c | ||
|
|
a2801f3168 | ||
|
|
99c31f4318 | ||
|
|
05785979e3 | ||
|
|
586a1a160e | ||
|
|
d04ed8d430 | ||
|
|
193447d07e | ||
|
|
1725b0de2e | ||
|
|
a2401302ed | ||
|
|
f39891dd2c | ||
|
|
8c5390bfef | ||
|
|
10673a49d4 | ||
|
|
3ce34fb874 | ||
|
|
5c94e9122c | ||
|
|
8140e80d61 | ||
|
|
c1b2ec09a4 | ||
|
|
3b3f55c5de | ||
|
|
17020e5192 | ||
|
|
f22aea7b1d | ||
|
|
844b57054b | ||
|
|
8de9aff1f6 | ||
|
|
f59f572e5c | ||
|
|
da2221540e | ||
|
|
9fa29c183a | ||
|
|
d034171d92 | ||
|
|
3a30b3d379 | ||
|
|
2624f396e5 | ||
|
|
8ae32a3a22 | ||
|
|
3c1975f6bf | ||
|
|
43a96faca4 | ||
|
|
bbd6d0864c | ||
|
|
ccea7674bd | ||
|
|
7f332c26ad | ||
|
|
206a7f38ca | ||
|
|
16e0a5e12e | ||
|
|
c6896939e2 | ||
|
|
526253723b | ||
|
|
9350a9cc2e | ||
|
|
e2ec2e4602 | ||
|
|
bca2e8fcae | ||
|
|
43674ea1f9 | ||
|
|
373a1f87a1 | ||
|
|
e14a595fba | ||
|
|
727e137008 | ||
|
|
883d853129 | ||
|
|
0d329aff64 | ||
|
|
94cb6fa279 | ||
|
|
257d80ecac | ||
|
|
d0f77fe0fc | ||
|
|
e95b504dbb | ||
|
|
0b68799507 | ||
|
|
9167be2cf2 | ||
|
|
d426c08cdd | ||
|
|
972c32b9d8 | ||
|
|
a279e20a49 | ||
|
|
fe60fea928 | ||
|
|
c6df43da9c | ||
|
|
475ed3e7c8 | ||
|
|
fb4c762655 | ||
|
|
a110faabe3 | ||
|
|
df2bf43492 | ||
|
|
b46fea6890 | ||
|
|
213a0d5293 | ||
|
|
08b6379601 | ||
|
|
3fbadc2521 | ||
|
|
9e78caeda4 | ||
|
|
e072a49288 | ||
|
|
b89e18eebf | ||
|
|
63607794d6 | ||
|
|
37842fd897 | ||
|
|
a1397a224b | ||
|
|
804d6af6c3 | ||
|
|
e315169005 | ||
|
|
ea76afee09 | ||
|
|
45dda3af9b | ||
|
|
3d70b51244 | ||
|
|
22f196c8c0 | ||
|
|
540aa9ba73 | ||
|
|
1ff0b83a19 | ||
|
|
27f5a47cc9 | ||
|
|
732b6ad09d | ||
|
|
0df7346a14 | ||
|
|
786697109d | ||
|
|
1bfadb0669 | ||
|
|
79dc1cc93b | ||
|
|
38fb2c69f1 | ||
|
|
b34f827bc0 | ||
|
|
97d1b408e1 | ||
|
|
a5065578ca | ||
|
|
aac5c6067d | ||
|
|
cfd7cf314b | ||
|
|
c4b73f6014 | ||
|
|
35d377ce31 | ||
|
|
5e330ac451 | ||
|
|
8188ef169c | ||
|
|
3496918ce6 | ||
|
|
c72f368f6a | ||
|
|
eb089847e0 | ||
|
|
8aaa6b207e | ||
|
|
72d560e4eb | ||
|
|
31219ea754 | ||
|
|
52c411ead0 | ||
|
|
0edbd15d47 | ||
|
|
342241963a | ||
|
|
f5b381eb35 | ||
|
|
be33401b6f | ||
|
|
c415db0cc5 | ||
|
|
35576c3d6f | ||
|
|
a11fbfa829 | ||
|
|
26d1b144e4 | ||
|
|
16b63bf13c | ||
|
|
52434f3aa9 | ||
|
|
7aa325f914 | ||
|
|
5a8a631449 | ||
|
|
e6bbd7b2bf | ||
|
|
3721484dff | ||
|
|
6698052ba5 | ||
|
|
f6f24acfdf | ||
|
|
ca8bcba0d7 | ||
|
|
9a36f8541f | ||
|
|
0026dc287f | ||
|
|
c65077172d | ||
|
|
6e6c261f35 | ||
|
|
130cbbd7dd | ||
|
|
63668f5a8c | ||
|
|
193b551773 | ||
|
|
76a0e12222 | ||
|
|
887d8c85ee | ||
|
|
6a90f06084 | ||
|
|
f7a21cbb52 | ||
|
|
3c6c240b9d | ||
|
|
2553c06a9f | ||
|
|
62a10d142e | ||
|
|
955dc1b015 | ||
|
|
6124ec66f3 | ||
|
|
2c6287405e | ||
|
|
0be309fb22 | ||
|
|
a79543569d | ||
|
|
e3d7120193 | ||
|
|
80a3a54476 | ||
|
|
6448cc598d | ||
|
|
c4bd30d512 | ||
|
|
b2e3596d87 | ||
|
|
ccfe74a6ea | ||
|
|
f4ffdc985e | ||
|
|
33981f9885 | ||
|
|
1bc93cce0e | ||
|
|
ec0eee9d3f | ||
|
|
140546ca4d | ||
|
|
fd075a02c5 | ||
|
|
4d1d953a3a | ||
|
|
9dd509be22 | ||
|
|
efaae35976 | ||
|
|
35784216bc | ||
|
|
8ce0a82506 | ||
|
|
51883cd82b | ||
|
|
829c5d85f6 | ||
|
|
748a19ef44 | ||
|
|
e987226954 | ||
|
|
59e2f4a7fa | ||
|
|
817b53efaa | ||
|
|
12c7ec86a9 | ||
|
|
611b5001be | ||
|
|
923cfd5bc9 | ||
|
|
36d2320e70 | ||
|
|
17713ee400 | ||
|
|
ee3465868e | ||
|
|
9e8141a8d9 | ||
|
|
043e1b39b0 | ||
|
|
938c1de906 | ||
|
|
a0dfe63660 | ||
|
|
42b7441467 | ||
|
|
28c2f87b26 | ||
|
|
8ad35ce83a | ||
|
|
4464b5b34d | ||
|
|
7267c13ee0 |
189
CHANGELOG.md
189
CHANGELOG.md
@@ -1,10 +1,187 @@
|
||||
# Changelog
|
||||
|
||||
## Pending release
|
||||
|
||||
## Pending release..
|
||||
## What's Changed
|
||||
## [4.10.1](https://github.com/eddyizm/tempo/releases/tag/v4.10.1) (2026-02-08)
|
||||
* fix: Addressing some UI/UX quirks by @tiltshiftfocus in https://github.com/eddyizm/tempus/pull/413
|
||||
* fix: keep observer until data is received on continuousPlay bug by @eddyizm in https://github.com/eddyizm/tempus/pull/421
|
||||
* fix: album art now displays on android auto by @trobinson in https://github.com/eddyizm/tempus/pull/414
|
||||
* feat: improve landscape view and increase items per row on landscape view by @tvillega in https://github.com/eddyizm/tempus/pull/411
|
||||
|
||||
## New Contributors
|
||||
* @tiltshiftfocus made their first contribution in https://github.com/eddyizm/tempus/pull/413
|
||||
* @trobinson made their first contribution in https://github.com/eddyizm/tempus/pull/414
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.8...v4.10.1
|
||||
|
||||
## What's Changed
|
||||
## [4.9.8](https://github.com/eddyizm/tempo/releases/tag/v4.9.8) (2026-02-02)
|
||||
* fix: missing Replay Gain metadata from .m4a files by @pgrit in https://github.com/eddyizm/tempus/pull/396
|
||||
* fix: Improve Synced Lyrics by @pgrit in https://github.com/eddyizm/tempus/pull/384
|
||||
* fix: Add selector for playlist visibility by @tvillega in https://github.com/eddyizm/tempus/pull/394
|
||||
* chore(i18n): set links as untranslatable by @tvillega in https://github.com/eddyizm/tempus/pull/400
|
||||
|
||||
## New Contributors
|
||||
* @tvillega made their first contribution in https://github.com/eddyizm/tempus/pull/394
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.5...v4.5.8
|
||||
|
||||
## What's Changed
|
||||
## [4.9.5](https://github.com/eddyizm/tempo/releases/tag/v4.9.5) (2026-01-26)
|
||||
* fix: Avoid crash when server has no songs by @jaime-grj in https://github.com/eddyizm/tempus/pull/389
|
||||
* fix: updated dialog import to address crashing on android 15 by @eddyizm in https://github.com/eddyizm/tempus/pull/392
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.3...v4.9.5
|
||||
|
||||
## What's Changed
|
||||
## [4.9.3](https://github.com/eddyizm/tempo/releases/tag/v4.9.3) (2026-01-25)
|
||||
* fix: Proper raw stream detection by @jaime-grj in https://github.com/eddyizm/tempus/pull/382
|
||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/381
|
||||
* feat: add configurable timeout by @eddyizm in https://github.com/eddyizm/tempus/pull/386
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.9.1...v4.9.3
|
||||
|
||||
## What's Changed
|
||||
## [4.9.1](https://github.com/eddyizm/tempo/releases/tag/v4.9.1) (2026-01-24)
|
||||
* chore: i18n: Add Romanian translation (including locale_config this time!) by @DevMatei in https://github.com/eddyizm/tempus/pull/357
|
||||
* French localization update by @benoit-smith in https://github.com/eddyizm/tempus/pull/356
|
||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/364
|
||||
* docs: updated readme and added known issues for airsonic work around by @eddyizm in https://github.com/eddyizm/tempus/pull/366
|
||||
* fix: toast for made for you click indication by @eddyizm in https://github.com/eddyizm/tempus/pull/365
|
||||
* fix: sort playlist view by @eddyizm in https://github.com/eddyizm/tempus/pull/368
|
||||
* feat: sort preference for playlists by @eddyizm in https://github.com/eddyizm/tempus/pull/370
|
||||
* fix: use existing future when adding tracks, dialed random album tracks off in instant mix by @eddyizm in https://github.com/eddyizm/tempus/pull/373
|
||||
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/374
|
||||
* fix: Check for OpenSubsonic extensions also with password authentication by @pgrit in https://github.com/eddyizm/tempus/pull/375
|
||||
* feat: Implement duration and seeking for transcodes by @drakeerv in https://github.com/eddyizm/tempus/pull/358
|
||||
* feat: Playback speed controls for music by @pgrit in https://github.com/eddyizm/tempus/pull/376
|
||||
|
||||
## New Contributors
|
||||
* @pgrit made their first contribution in https://github.com/eddyizm/tempus/pull/375
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.4...v4.9.1
|
||||
|
||||
## What's Changed
|
||||
## [4.6.4](https://github.com/eddyizm/tempo/releases/tag/v4.6.4) (2026-01-13)
|
||||
* fix: instant mix random songs and broken continuous play by @eddyizm in https://github.com/eddyizm/tempus/pull/354
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.3...v4.6.4
|
||||
|
||||
## What's Changed
|
||||
## [4.6.3](https://github.com/eddyizm/tempo/releases/tag/v4.6.3) (2026-01-10)
|
||||
* fix: give user feedback when trying to add podcast/radio on unsupport… by @eddyizm in https://github.com/eddyizm/tempus/pull/328
|
||||
* docs: Clarify Android Auto enablement by @Forage in https://github.com/eddyizm/tempus/pull/336
|
||||
* fix: instant mix gets a big refactor, with cascading fallbacks to produce a larger queue by @eddyizm in https://github.com/eddyizm/tempus/pull/330
|
||||
* chore(i18n): add missing keys, update Chinese translation and alphabetize by @hongwei1203 in https://github.com/eddyizm/tempus/pull/332
|
||||
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/339
|
||||
* feat: Ability to toggle visibility of artist biography by @kmarius in https://github.com/eddyizm/tempus/pull/338
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.6.0...v4.6.3
|
||||
|
||||
## [4.6.0](https://github.com/eddyizm/tempo/releases/tag/v4.6.0) (2025-12-22)
|
||||
## What's Changed
|
||||
* chore: Update description_empty_title in English and Polish by @tyren234 in https://github.com/eddyizm/tempus/pull/307
|
||||
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/310
|
||||
* fix: checks preference and writes files externally, updates the ui by @eddyizm in https://github.com/eddyizm/tempus/pull/312
|
||||
* chore: Update description_empty_title in Italian by @pochopsp in https://github.com/eddyizm/tempus/pull/314
|
||||
* chore: Update description_empty_title in French and Spanish by @pochopsp in https://github.com/eddyizm/tempus/pull/315
|
||||
* feat: added regular playlist to home view by @eddyizm in https://github.com/eddyizm/tempus/pull/322
|
||||
|
||||
## New Contributors
|
||||
* @tyren234 made their first contribution in https://github.com/eddyizm/tempus/pull/307
|
||||
* @pochopsp made their first contribution in https://github.com/eddyizm/tempus/pull/314
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.5.0...v4.6.0
|
||||
|
||||
## [4.5.0](https://github.com/eddyizm/tempo/releases/tag/v4.5.0) (2025-12-12)
|
||||
## What's Changed
|
||||
* fix: updates starred syncing downloads to user defined directory by @eddyizm in https://github.com/eddyizm/tempus/pull/298
|
||||
* fix: handle empty albums and null mappings by @eddyizm in https://github.com/eddyizm/tempus/pull/301
|
||||
* feat: integrate sort recent searches chronologically by @J4mm3ris in https://github.com/eddyizm/tempus/pull/300
|
||||
* feat: add heart to artist/album pages, fixed artist cover art failing by @eddyizm in https://github.com/eddyizm/tempus/pull/303
|
||||
|
||||
## New Contributors
|
||||
* @J4mm3ris made their first contribution in https://github.com/eddyizm/tempus/pull/300
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.4.0...v4.5.0
|
||||
|
||||
## [4.4.0](https://github.com/eddyizm/tempo/releases/tag/v4.4.0) (2025-11-29)
|
||||
## What's Changed
|
||||
* chore: bringing in media service refactor previously reverted after more testing by @eddyizm in https://github.com/eddyizm/tempus/pull/286
|
||||
* fix: refactor start queue to put the db writing in the background to address instant mix bug by @eddyizm in https://github.com/eddyizm/tempus/pull/287
|
||||
* Feat: playerqueue fab allows playqueue actions -> saving to playlist, download all, load queue, shuffle, clean queue by @eddyizm in https://github.com/eddyizm/tempus/pull/288
|
||||
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/291
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.3.0...v4.4.0
|
||||
|
||||
## [4.3.0](https://github.com/eddyizm/tempo/releases/tag/v4.3.0) (2025-11-23)
|
||||
## What's Changed
|
||||
* chore: Add Obtainium badge to README by @mikaeldui in https://github.com/eddyizm/tempus/pull/280
|
||||
* fix: Revert "refactor MediaService" by @eddyizm in https://github.com/eddyizm/tempus/pull/282
|
||||
* feat: add play functionality to library folder/index items by @antebudimir in https://github.com/eddyizm/tempus/pull/276
|
||||
* fix: start queue blocking UI by @eddyizm in https://github.com/eddyizm/tempus/pull/283
|
||||
|
||||
## New Contributors
|
||||
* @mikaeldui made their first contribution in https://github.com/eddyizm/tempus/pull/280
|
||||
* @antebudimir made their first contribution in https://github.com/eddyizm/tempus/pull/276
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.6...v4.3.0
|
||||
|
||||
## [4.2.6](https://github.com/eddyizm/tempo/releases/tag/v4.2.6) (2025-11-22)
|
||||
## What's Changed
|
||||
* fix: Fix player queue soft-lock by @shrapnelnet in https://github.com/eddyizm/tempus/pull/266
|
||||
* chore: Add Catalan i18n by @marcriera in https://github.com/eddyizm/tempus/pull/268
|
||||
* chore: Refactor MediaService by @pca006132 in https://github.com/eddyizm/tempus/pull/267
|
||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/272
|
||||
* chore(i18n): Update Italian translation by @66Bunz in https://github.com/eddyizm/tempus/pull/278
|
||||
|
||||
## New Contributors
|
||||
* @marcriera made their first contribution in https://github.com/eddyizm/tempus/pull/268
|
||||
* @66Bunz made their first contribution in https://github.com/eddyizm/tempus/pull/278
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.4...v4.2.6
|
||||
|
||||
## [4.2.4](https://github.com/eddyizm/tempo/releases/tag/v4.2.4) (2025-11-15)
|
||||
## What's Changed
|
||||
* chore: Update russian strings.xml by @Sevinfolds in https://github.com/eddyizm/tempus/pull/249
|
||||
* fix: disallow duplicate songs in queue by @eddyizm in https://github.com/eddyizm/tempus/pull/252
|
||||
* fix:github release check by @eddyizm in https://github.com/eddyizm/tempus/pull/253
|
||||
* fix: Fixed crash when viewing share by @drakeerv in https://github.com/eddyizm/tempus/pull/255
|
||||
* chore: Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/257
|
||||
* fix: add podcast/radio channel visible when empty podcasts/radio by @eddyizm in https://github.com/eddyizm/tempus/pull/260
|
||||
|
||||
## New Contributors
|
||||
* @Sevinfolds made their first contribution in https://github.com/eddyizm/tempus/pull/249
|
||||
* @drakeerv made their first contribution in https://github.com/eddyizm/tempus/pull/255
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.2.0...v4.2.4
|
||||
## [4.2.0](https://github.com/eddyizm/tempo/releases/tag/v4.2.0) (2025-11-09)
|
||||
## What's Changed
|
||||
* fix: Equalizer fix in main build variant by @jaime-grj in https://github.com/eddyizm/tempus/pull/239
|
||||
* fix: Images not filling holder by @eddyizm in https://github.com/eddyizm/tempus/pull/244
|
||||
* feat: Make artist and album clickable by @eddyizm in https://github.com/eddyizm/tempus/pull/243
|
||||
* feat: implement scroll to currently playing feature by @shrapnelnet in https://github.com/eddyizm/tempus/pull/247
|
||||
* fix: shuffling genres only queuing 25 songs by @shrapnelnet in https://github.com/eddyizm/tempus/pull/246
|
||||
|
||||
## New Contributors
|
||||
* @shrapnelnet made their first contribution in https://github.com/eddyizm/tempus/pull/247
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.3...v4.2.0
|
||||
|
||||
## [4.1.3](https://github.com/eddyizm/tempo/releases/tag/v4.1.3) (2025-11-06)
|
||||
## What's Changed
|
||||
* [fix: equalizer missing referenced value](https://github.com/eddyizm/tempus/commit/923cfd5bc97ed7db28c90348e3619d0a784fc434)
|
||||
* Fix: Album track list bug by @eddyizm in https://github.com/eddyizm/tempus/pull/237
|
||||
* fix: Add listener to enable equalizer when audioSessionId changes by @jaime-grj in https://github.com/eddyizm/tempus/pull/235
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.1.0...v4.1.3
|
||||
|
||||
## [4.1.0](https://github.com/eddyizm/tempo/releases/tag/v4.1.0) (2025-11-05)
|
||||
## What's Changed
|
||||
* chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205
|
||||
* feat: shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207
|
||||
* chore: Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220
|
||||
* shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207
|
||||
* Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220
|
||||
* feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206
|
||||
* Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210
|
||||
* fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216
|
||||
@@ -12,8 +189,12 @@
|
||||
* fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221
|
||||
* chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217
|
||||
* fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222
|
||||
* fix: skip mapping downloaded item by @pca006132 in https://github.com/eddyizm/tempus/pull/228
|
||||
|
||||
---
|
||||
## New Contributors
|
||||
* @pca006132 made their first contribution in https://github.com/eddyizm/tempus/pull/207
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.7...v4.1.0
|
||||
|
||||
## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28)
|
||||
## What's Changed
|
||||
|
||||
39
README.md
39
README.md
@@ -10,14 +10,21 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
<!-- Reproducible build -->
|
||||
<!-- [<img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status">](https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus) -->
|
||||
|
||||
<a href="https://github.com/eddyizm/tempus/releases/">
|
||||
<img alt="Releases" src="https://img.shields.io/github/downloads/eddyizm/tempus/total.svg?color=4B95DE&style=flat">
|
||||
</a>
|
||||
<!-- Reproducible build -->
|
||||
<a href="https://shields.rbtlog.dev/com.eddyizm.degoogled.tempus"><img src="https://shields.rbtlog.dev/simple/com.eddyizm.degoogled.tempus" alt="RB Status"></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0">
|
||||
<img src="https://img.shields.io/badge/license-GPL%20v3-2B6DBE.svg?style=flat">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/eddyizm/tempus/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
|
||||
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.eddyizm.degoogled.tempus"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
|
||||
<a href="https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.eddyizm.tempus%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Feddyizm%2Ftempus%22%2C%22author%22%3A%22eddyizm%22%2C%22name%22%3A%22Tempus%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Afalse%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22filterReleaseTitlesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22filterReleaseNotesByRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22sortMethodChoice%5C%22%3A%5C%22date%5C%22%2C%5C%22useLatestAssetDateAsReleaseDate%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Atrue%2C%5C%22releaseDateAsVersion%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22tempus%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%2C%5C%22includeZips%5C%22%3Afalse%2C%5C%22zippedApkFilterRegEx%5C%22%3A%5C%22%5C%22%7D%22%2C%22overrideSource%22%3A%22GitHub%22%7D"><img width="200" src="https://github.com/user-attachments/assets/119e7ff4-2636-43cb-ab7f-1b6a58ac3570" /></a>
|
||||
<a href="https://www.openapk.net/tempus/com.eddyizm.degoogled.tempus/"><img src="https://camo.githubusercontent.com/cd56895b28a73ebd781a65b4f567add5419e45797a5cf1485ce408e851c2318e/68747470733a2f2f7777772e6f70656e61706b2e6e65742f696d616765732f6f70656e61706b2d62616467652e706e67" width="200"></a>
|
||||
</p>
|
||||
<!--
|
||||
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
|
||||
@@ -26,10 +33,14 @@
|
||||
|
||||
**Tempus** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
|
||||
|
||||
Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience.
|
||||
Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Listenbrainz.org and Last.fm to personalize your music experience (These must be supported by your backend).
|
||||
|
||||
The project is a fork of [Tempo](#credits).
|
||||
|
||||
[Changelog](CHANGELOG.md)
|
||||
[Wiki](USAGE.md)
|
||||
[Donate](https://github.com/eddyizm/tempus#donate)
|
||||
|
||||
**If you find Tempus useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
|
||||
|
||||
**Use the Github version of the app for full Android Auto and Chromecast support.**
|
||||
@@ -45,11 +56,6 @@ Please note the two variants in the release assets include release/debug and 32/
|
||||
|
||||
`app-degoogled*` <- The izzyOnDroid release that goes without any of the google stuff. It is now available on izzyOnDroid (64bit) I am releasing the both 32/64bit apk's here on github for those who need a 32bit version.
|
||||
|
||||
[CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
## Usage
|
||||
|
||||
[Documentation](USAGE.md) (work in progress)
|
||||
|
||||
## Features
|
||||
- **Subsonic Integration**: Tempus seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
|
||||
@@ -58,16 +64,19 @@ Please note the two variants in the release assets include release/debug and 32/
|
||||
- **Streaming and Offline Mode**: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers.
|
||||
- **Playlist Management**: Create, edit, and manage playlists to curate your perfect music collection.
|
||||
- **Gapless Playback**: Experience uninterrupted playback with gapless listening mode.
|
||||
- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.
|
||||
- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.*
|
||||
- **Scrobbling Integration**: Optionally integrate Tempus with Last.fm or Listenbrainz.org to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
|
||||
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempus, expanding your audio entertainment options.
|
||||
- **Instant Mix**: Full refactor of instant mix function which leverages subsonics similarSongs2 by artist/album and similarSongs endpoints to server a larger play queue more reliably.
|
||||
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
|
||||
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
|
||||
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.*
|
||||
- **Multiple Libraries**: Tempus handles multi-library setups gracefully. They are displayed as Library folders.
|
||||
- **Equalizer**: Option to use in app equalizer.
|
||||
- **Widget**: New widget to keeping the basic controls on your screen at all times.
|
||||
- **Available in 11 languages**: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish
|
||||
|
||||
**Github version only*
|
||||
|
||||
## Screenshot
|
||||
|
||||
<p align="center">
|
||||
@@ -111,7 +120,10 @@ Currently there are no tests but I would love to start on some unit tests.
|
||||
|
||||
Not a hard requirement but any new feature/change should ideally include an update to the nacent documention.
|
||||
|
||||
## Support
|
||||
*Special Thanks*
|
||||
All the amazing [contributors](https://github.com/eddyizm/tempus/graphs/contributors)❤️
|
||||
|
||||
## Donate
|
||||
|
||||
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
|
||||
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
|
||||
@@ -120,7 +132,8 @@ bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
|
||||
|
||||
Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.
|
||||
|
||||
|
||||
## Credits
|
||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
|
||||
|
||||
[Opensvg.org](https://opensvg.org) for the new turntable logo.
|
||||
[Opensvg.org](https://opensvg.org) for the new turntable logo.
|
||||
|
||||
61
USAGE.md
61
USAGE.md
@@ -12,7 +12,7 @@
|
||||
- [Playlist Management](#playlist-management)
|
||||
- [Android Auto](#android-auto)
|
||||
- [Settings](#settings)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Known Issues](#known-issues)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -27,7 +27,9 @@ This app works with any service that implements the Subsonic API, including:
|
||||
- [LMS - Lightweight Music Server](https://github.com/epoupon/lms) - *personal fave and my backend*
|
||||
- [Navidrome](https://www.navidrome.org/)
|
||||
- [Gonic](https://github.com/sentriz/gonic)
|
||||
|
||||
- [Ampache](https://github.com/ampache/ampache)
|
||||
- [NextCloud Music](https://apps.nextcloud.com/apps/music)
|
||||
- [Airsonic Advanced](https://github.com/kagemomiji/airsonic-advanced)
|
||||
|
||||
|
||||
|
||||
@@ -66,6 +68,21 @@ However, if you want to limit or change libraries you could use a workaround, if
|
||||
|
||||
You can create multiple users , one for each library, and save each of them in Tempus app.
|
||||
|
||||
### Folder or index playback
|
||||
|
||||
If your Subsonic-compatible server exposes the folder tree **or** provides an artist index (for example Gonic, Navidrome, or any backend with folder browsing enabled), Tempus lets you play an entire folder from anywhere in the library hierarchy:
|
||||
|
||||
<p align="left">
|
||||
<img src="mockup/usage/music_folders_root.png" width=317 style="margin-right:16px;">
|
||||
<img src="mockup/usage/music_folders_playback.png" width=317>
|
||||
</p>
|
||||
|
||||
- The **Library ▸ Music folders** screen shows each top-level folder with a play icon only after you drill into it. The root entry remains a simple navigator.
|
||||
- When viewing **inner folders** **or artist index entries**, tap the new play button to immediately enqueue every audio track inside that folder/index and all nested subfolders.
|
||||
- Video files are excluded automatically, so only playable audio ends up in the queue.
|
||||
|
||||
No extra config is needed—Tempus adjusts based on the connected backend.
|
||||
|
||||
### Now Playing Screen
|
||||
|
||||
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
|
||||
@@ -84,6 +101,17 @@ On the main player control screen, tapping on the artwork will reveal a small co
|
||||
4. Saves play queue (if the feature is enabled in the settings)
|
||||
* if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right)
|
||||
|
||||
### Podcasts
|
||||
If your server supports it - add a podcast rss feed
|
||||
<p align="left">
|
||||
<img src="mockup/usage/add_podcast_feed.png" width=317>
|
||||
</p>
|
||||
|
||||
### Radio Stations
|
||||
If your server supports it - add a internet radio station feed
|
||||
<p align="left">
|
||||
<img src="mockup/usage/add_radio_station.png" width=326>
|
||||
</p>
|
||||
|
||||
## Navigation
|
||||
|
||||
@@ -131,7 +159,23 @@ On the main player control screen, tapping on the artwork will reveal a small co
|
||||
## Android Auto
|
||||
|
||||
### Enabling on your head unit
|
||||
- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed)
|
||||
To allow the Tempus app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempus isn't installed through Play Store. Note that the Android Auto developer settings are different from the global Android "Developer options".
|
||||
1. Switch to developer mode in the Android Auto settings by tapping ten times on the "Version" item at the bottom, followed by giving your permission.
|
||||
<p align="left">
|
||||
<img width="270" height="600" alt="1a" src="https://github.com/user-attachments/assets/f09f6999-9761-4b05-8ec7-bf221a15dda3" />
|
||||
<img width="270" height="600" alt="1b" src="https://github.com/user-attachments/assets/0795e508-ba01-41c5-96a7-7c03b0156591" />
|
||||
<img width="270" height="600" alt="1c" src="https://github.com/user-attachments/assets/51c15f67-fddb-452e-b5d3-5092edeab390" />
|
||||
</p>
|
||||
|
||||
2. Go to the "Developer settings" by the menu at the top right.
|
||||
<p align="left">
|
||||
<img width="270" height="600" alt="2" src="https://github.com/user-attachments/assets/1ecd1f3e-026d-4d25-87f2-be7f12efbac6" />
|
||||
</p>
|
||||
|
||||
3. Scroll down to the bottom and check "Unknown sources".
|
||||
<p align="left">
|
||||
<img width="270" height="600" alt="3" src="https://github.com/user-attachments/assets/37db88e9-1b76-417f-9c47-da9f3a750fff" />
|
||||
</p>
|
||||
|
||||
|
||||
### Server Settings
|
||||
@@ -148,15 +192,12 @@ On the main player control screen, tapping on the artwork will reveal a small co
|
||||
### Appearance
|
||||
**TODO**
|
||||
|
||||
## Troubleshooting
|
||||
## Known Issues
|
||||
|
||||
### Connection Issues
|
||||
### Airsonic Distorted Playback
|
||||
|
||||
**TODO**
|
||||
|
||||
### Common Issues
|
||||
|
||||
**TODO**
|
||||
First reported in issue [#226](https://github.com/eddyizm/tempus/issues/226)
|
||||
The work around is to disable the cache in the settings, (set to 0), and if needed, cleaning the (Android) cache fixes the problem.
|
||||
|
||||
### Support
|
||||
For additional help:
|
||||
|
||||
@@ -10,8 +10,8 @@ android {
|
||||
minSdkVersion 24
|
||||
targetSdk 35
|
||||
|
||||
versionCode 3
|
||||
versionName '4.1.0'
|
||||
versionCode 19
|
||||
versionName '4.10.1'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
|
||||
1158
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/13.json
Normal file
1158
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/13.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,550 +1,6 @@
|
||||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil
|
||||
import com.cappielloantonio.tempo.util.Constants
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil
|
||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.MappingUtil
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
|
||||
@UnstableApi
|
||||
class MediaService : MediaLibraryService() {
|
||||
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
|
||||
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var shuffleCommands: List<CommandButton>
|
||||
private lateinit var repeatCommands: List<CommandButton>
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
private var customLayout = ImmutableList.of<CommandButton>()
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
return this@MediaService.equalizerManager
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
companion object {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
||||
"android.media3.session.demo.SHUFFLE_ON"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
|
||||
"android.media3.session.demo.SHUFFLE_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
|
||||
"android.media3.session.demo.REPEAT_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
|
||||
"android.media3.session.demo.REPEAT_ONE"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
||||
"android.media3.session.demo.REPEAT_ALL"
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
}
|
||||
|
||||
fun updateMediaItems() {
|
||||
Log.d("MediaService", "update items");
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))}
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post(Runnable {
|
||||
updateMediaItems()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
initializeCustomCommands()
|
||||
initializePlayer()
|
||||
initializeMediaLibrarySession()
|
||||
restorePlayerFromQueue()
|
||||
initializePlayerListener()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
|
||||
setPlayer(player)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// Check if the intent is for our custom equalizer binder
|
||||
if (intent?.action == ACTION_BIND_EQUALIZER) {
|
||||
return binder
|
||||
}
|
||||
// Otherwise, handle it as a normal MediaLibraryService connection
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
|
||||
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val connectionResult = super.onConnect(session, controller)
|
||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||
|
||||
(shuffleCommands + repeatCommands).forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||
}
|
||||
|
||||
customLayout = buildCustomLayout(session.player)
|
||||
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(availableSessionCommands.build())
|
||||
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
|
||||
.setCustomLayout(customLayout)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
|
||||
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
|
||||
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
|
||||
}
|
||||
}
|
||||
|
||||
fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
|
||||
val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0]
|
||||
val repeat = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> repeatCommands[1]
|
||||
Player.REPEAT_MODE_ALL -> repeatCommands[2]
|
||||
else -> repeatCommands[0]
|
||||
}
|
||||
return ImmutableList.of(shuffle, repeat)
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
when (customCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> player.shuffleModeEnabled = true
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> player.shuffleModeEnabled = false
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
||||
val nextMode = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
||||
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE
|
||||
else -> Player.REPEAT_MODE_OFF
|
||||
}
|
||||
player.repeatMode = nextMode
|
||||
}
|
||||
}
|
||||
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
session.setCustomLayout(customLayout)
|
||||
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
override fun onAddMediaItems(
|
||||
mediaSession: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
val updatedMediaItems = mediaItems.map { mediaItem ->
|
||||
val mediaMetadata = mediaItem.mediaMetadata
|
||||
|
||||
val newMetadata = mediaMetadata.buildUpon()
|
||||
.setArtist(
|
||||
if (mediaMetadata.artist != null) mediaMetadata.artist
|
||||
else mediaMetadata.extras?.getString("uri") ?: ""
|
||||
)
|
||||
.build()
|
||||
|
||||
mediaItem.buildUpon()
|
||||
.setUri(mediaItem.requestMetadata.mediaUri)
|
||||
.setMediaMetadata(newMetadata)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.build()
|
||||
}
|
||||
return Futures.immediateFuture(updatedMediaItems)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeCustomCommands() {
|
||||
shuffleCommands = listOf(
|
||||
getShuffleCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
|
||||
),
|
||||
getShuffleCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
|
||||
)
|
||||
)
|
||||
|
||||
repeatCommands = listOf(
|
||||
getRepeatCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)
|
||||
),
|
||||
getRepeatCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)
|
||||
),
|
||||
getRepeatCommandButton(
|
||||
SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)
|
||||
)
|
||||
)
|
||||
|
||||
customLayout = ImmutableList.of(shuffleCommands[0], repeatCommands[0])
|
||||
}
|
||||
|
||||
private fun initializePlayer() {
|
||||
player = ExoPlayer.Builder(this)
|
||||
.setRenderersFactory(getRenderersFactory())
|
||||
.setMediaSourceFactory(getMediaSourceFactory())
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.setLoadControl(initializeLoadControl())
|
||||
.build()
|
||||
|
||||
player.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
|
||||
player.repeatMode = Preferences.getRepeatMode()
|
||||
}
|
||||
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = player.audioSessionId
|
||||
if (equalizerManager.attachToSession(audioSessionId)) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession() {
|
||||
val sessionActivityPendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
|
||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.build()
|
||||
|
||||
if (!customLayout.isEmpty()) {
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
|
||||
updateMediaItems()
|
||||
}
|
||||
|
||||
private fun restorePlayerFromQueue() {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
val queueRepository = QueueRepository()
|
||||
val storedQueue = queueRepository.media
|
||||
if (storedQueue.isNullOrEmpty()) return
|
||||
|
||||
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||
if (mediaItems.isEmpty()) return
|
||||
|
||||
val lastIndex = try {
|
||||
queueRepository.lastPlayedMediaIndex
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}.coerceIn(0, mediaItems.size - 1)
|
||||
|
||||
val lastPosition = try {
|
||||
queueRepository.lastPlayedMediaTimestamp
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}.let { if (it < 0L) 0L else it }
|
||||
|
||||
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||
player.prepare()
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
private fun initializePlayerListener() {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
if (mediaItem == null) return
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
ReplayGainUtil.setReplayGain(player, tracks)
|
||||
val currentMediaItem = player.currentMediaItem
|
||||
if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) {
|
||||
MediaManager.scrobble(currentMediaItem, false)
|
||||
}
|
||||
|
||||
if (player.currentMediaItemIndex + 1 == player.mediaItemCount)
|
||||
MediaManager.continuousPlay(player.currentMediaItem)
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (!isPlaying) {
|
||||
MediaManager.setPlayingPausedTimestamp(
|
||||
player.currentMediaItem,
|
||||
player.currentPosition
|
||||
)
|
||||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (!player.hasNextMediaItem() &&
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||
) {
|
||||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.scrobble(oldPosition.mediaItem, true)
|
||||
MediaManager.saveChronology(oldPosition.mediaItem)
|
||||
}
|
||||
|
||||
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
Preferences.setRepeatMode(repeatMode)
|
||||
customLayout = librarySessionCallback.buildCustomLayout(player)
|
||||
mediaLibrarySession.setCustomLayout(customLayout)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlayer(player: Player) {
|
||||
mediaLibrarySession.player = player
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
|
||||
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
return CommandButton.Builder()
|
||||
.setDisplayName(
|
||||
getString(
|
||||
if (isOn) R.string.exo_controls_shuffle_on_description
|
||||
else R.string.exo_controls_shuffle_off_description
|
||||
)
|
||||
)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
|
||||
.build()
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getRepeatCommandButton(sessionCommand: SessionCommand): CommandButton {
|
||||
val icon = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.drawable.exo_icon_repeat_one
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.drawable.exo_icon_repeat_all
|
||||
else -> R.drawable.exo_icon_repeat_off
|
||||
}
|
||||
val description = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description
|
||||
else -> R.string.exo_controls_repeat_off_description
|
||||
}
|
||||
return CommandButton.Builder()
|
||||
.setDisplayName(getString(description))
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setIconResId(icon)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ignoreFuture(@Suppress("UNUSED_PARAMETER") customLayout: ListenableFuture<SessionResult>) {
|
||||
/* Do nothing. */
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
return DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val extras = mi?.mediaMetadata?.extras
|
||||
val coverId = extras?.getString("coverArtId")
|
||||
val songLink = extras?.getString("assetLinkSong")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||
val albumLink = extras?.getString("assetLinkAlbum")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||
val artistLink = extras?.getString("assetLinkArtist")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration,
|
||||
songLink,
|
||||
albumLink,
|
||||
artistLink
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
class MediaService : BaseMediaService()
|
||||
|
||||
@@ -96,7 +96,12 @@
|
||||
android:resource="@xml/widget_info"/>
|
||||
</receiver>
|
||||
|
||||
|
||||
<provider
|
||||
android:name=".provider.AlbumArtContentProvider"
|
||||
android:authorities="com.cappielloantonio.tempo.provider"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
/>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||
|
||||
@UnstableApi
|
||||
@Database(
|
||||
version = 12,
|
||||
version = 13,
|
||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
||||
autoMigrations = {@AutoMigration(from = 10, to = 11), @AutoMigration(from = 11, to = 12)}
|
||||
)
|
||||
|
||||
@@ -12,9 +12,12 @@ import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface RecentSearchDao {
|
||||
@Query("SELECT * FROM recent_search ORDER BY search DESC")
|
||||
@Query("SELECT search FROM recent_search ORDER BY timestamp DESC")
|
||||
List<String> getRecent();
|
||||
|
||||
@Query("SELECT search FROM recent_search ORDER BY search DESC")
|
||||
List<String> getAlpha();
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
void insert(RecentSearch search);
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ public class UpdateUtil {
|
||||
|
||||
public static boolean showUpdateDialog(LatestRelease release) {
|
||||
if (release.getTagName() == null) return false;
|
||||
String remoteTag = release.getTagName().replaceAll("^\\D+", "");
|
||||
|
||||
try {
|
||||
String[] local = BuildConfig.VERSION_NAME.split("\\.");
|
||||
String[] remote = release.getTagName().split("\\.");
|
||||
String[] remote = remoteTag.split("\\.");
|
||||
|
||||
for (int i = 0; i < local.length; i++) {
|
||||
int localPart = Integer.parseInt(local[i]);
|
||||
|
||||
@@ -27,8 +27,11 @@ public interface ClickCallback {
|
||||
default void onInternetRadioStationClick(Bundle bundle) {}
|
||||
default void onInternetRadioStationLongClick(Bundle bundle) {}
|
||||
default void onMusicFolderClick(Bundle bundle) {}
|
||||
default void onMusicFolderPlay(Bundle bundle) {}
|
||||
default void onMusicDirectoryClick(Bundle bundle) {}
|
||||
default void onMusicDirectoryPlay(Bundle bundle) {}
|
||||
default void onMusicIndexClick(Bundle bundle) {}
|
||||
default void onMusicIndexPlay(Bundle bundle) {}
|
||||
default void onDownloadGroupLongClick(Bundle bundle) {}
|
||||
default void onShareClick(Bundle bundle) {}
|
||||
default void onShareLongClick(Bundle bundle) {}
|
||||
|
||||
@@ -13,5 +13,8 @@ import kotlinx.parcelize.Parcelize
|
||||
data class RecentSearch(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "search")
|
||||
var search: String
|
||||
var search: String,
|
||||
|
||||
@ColumnInfo(name = "timestamp", defaultValue = "0")
|
||||
var timestamp: Long
|
||||
) : Parcelable
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cappielloantonio.tempo.model
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.Keep
|
||||
@@ -13,6 +14,7 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest
|
||||
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child
|
||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
|
||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
|
||||
@@ -197,7 +199,7 @@ class SessionMediaItem() {
|
||||
|
||||
fun getMediaItem(): MediaItem {
|
||||
val uri: Uri = getStreamUri()
|
||||
val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize()))
|
||||
val artworkUri = AlbumArtContentProvider.contentUri(coverArtId)
|
||||
|
||||
val bundle = Bundle()
|
||||
bundle.putString("id", id)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.cappielloantonio.tempo.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AlbumArtContentProvider extends ContentProvider {
|
||||
public static final String AUTHORITY = "com.cappielloantonio.tempo.provider";
|
||||
public static final String ALBUM_ART = "albumArt";
|
||||
private ExecutorService executor;
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(AUTHORITY, "albumArt/*", 1);
|
||||
}
|
||||
|
||||
public static Uri contentUri(String artworkId) {
|
||||
return new Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_CONTENT)
|
||||
.authority(AUTHORITY)
|
||||
.appendPath(ALBUM_ART)
|
||||
.appendPath(artworkId)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
Context context = getContext();
|
||||
String albumId = uri.getLastPathSegment();
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(albumId, Preferences.getImageSize()));
|
||||
|
||||
try {
|
||||
// use pipe to communicate between background thread and caller of openFile()
|
||||
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
|
||||
ParcelFileDescriptor readSide = pipe[0];
|
||||
ParcelFileDescriptor writeSide = pipe[1];
|
||||
|
||||
// perform loading in background thread to avoid blocking UI
|
||||
executor.execute(() -> {
|
||||
try (OutputStream out = new ParcelFileDescriptor.AutoCloseOutputStream(writeSide)) {
|
||||
|
||||
// request artwork from API using Glide
|
||||
File file = Glide.with(context)
|
||||
.asFile()
|
||||
.load(artworkUri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.DATA)
|
||||
.submit()
|
||||
.get();
|
||||
|
||||
// copy artwork down pipe returned by ContentProvider
|
||||
try (InputStream in = new FileInputStream(file)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
writeSide.closeWithError("Failed to load image: " + e.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
writeSide.closeWithError("Failed to load image: " + e.getMessage());
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
});
|
||||
|
||||
return readSide;
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new FileNotFoundException("Could not create pipe: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
executor = Executors.newFixedThreadPool(
|
||||
Math.max(2, Runtime.getRuntime().availableProcessors() / 2)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
if (executor != null) {
|
||||
executor.shutdown();
|
||||
try {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@ package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
|
||||
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
@@ -204,29 +205,12 @@ public class AlbumRepository {
|
||||
return albumInfo;
|
||||
}
|
||||
|
||||
public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getSimilarSongs2(album.getId(), count)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = new ArrayList<>();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
||||
songs.addAll(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
|
||||
}
|
||||
|
||||
callback.onLoadMedia(songs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
callback.onLoadMedia(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
public MutableLiveData<List<Child>> getInstantMix(AlbumID3 album, int count) {
|
||||
// Delegate to the centralized SongRepository
|
||||
return new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count);
|
||||
}
|
||||
|
||||
|
||||
public MutableLiveData<List<Integer>> getDecades() {
|
||||
MutableLiveData<List<Integer>> decades = new MutableLiveData<>();
|
||||
|
||||
@@ -237,7 +221,7 @@ public class AlbumRepository {
|
||||
@Override
|
||||
public void onLoadYear(int last) {
|
||||
if (first != -1 && last != -1) {
|
||||
List<Integer> decadeList = new ArrayList();
|
||||
List<Integer> decadeList = new ArrayList<>();
|
||||
|
||||
int startDecade = first - (first % 10);
|
||||
int lastDecade = last - (last % 10);
|
||||
@@ -298,4 +282,4 @@ public class AlbumRepository {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,14 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import android.util.Log;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -149,7 +151,7 @@ public class ArtistRepository {
|
||||
|
||||
if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) {
|
||||
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
|
||||
if(index != null && index.getArtists() != null) {
|
||||
if(index.getArtists() != null) {
|
||||
artists.addAll(index.getArtists());
|
||||
}
|
||||
}
|
||||
@@ -287,26 +289,8 @@ public class ArtistRepository {
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
|
||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getSimilarSongs2(artist.getId(), count)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
||||
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
return instantMix;
|
||||
// Delegate to the centralized SongRepository
|
||||
return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count);
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getRandomSong(ArtistID3 artist, int count) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
|
||||
@@ -22,6 +23,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.model.Chronology;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.model.SessionMediaItem;
|
||||
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
|
||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
@@ -70,7 +72,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(album.getName())
|
||||
@@ -217,7 +219,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(album.getName())
|
||||
@@ -272,7 +274,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (ArtistID3 artist : artists) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(artist.getName())
|
||||
@@ -397,7 +399,7 @@ public class AutomotiveRepository {
|
||||
List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren();
|
||||
|
||||
for (Child song : children) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(song.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(song.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(song.getTitle())
|
||||
@@ -451,7 +453,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (Child child : directory.getChildren()) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(child.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(child.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(child.getTitle())
|
||||
@@ -550,7 +552,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (PodcastEpisode episode : episodes) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(episode.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(episode.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(episode.getTitle())
|
||||
@@ -687,7 +689,7 @@ public class AutomotiveRepository {
|
||||
List<MediaItem> mediaItems = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(album.getName())
|
||||
@@ -800,7 +802,7 @@ public class AutomotiveRepository {
|
||||
|
||||
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
|
||||
for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(artist.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(artist.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(artist.getName())
|
||||
@@ -822,7 +824,7 @@ public class AutomotiveRepository {
|
||||
|
||||
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
|
||||
for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) {
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(album.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(album.getCoverArtId());
|
||||
|
||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||
.setTitle(album.getName())
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import static android.provider.Settings.System.getString;
|
||||
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -107,13 +104,13 @@ public class PlaylistRepository {
|
||||
return playlistLiveData;
|
||||
}
|
||||
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||
if (songsId.isEmpty()) {
|
||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||
} else{
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
||||
.updatePlaylist(playlistId, null, playlistVisibilityIsPublic, songsId, null)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
@@ -66,88 +66,33 @@ public class PodcastRepository {
|
||||
return liveNewestPodcastEpisodes;
|
||||
}
|
||||
|
||||
public void refreshPodcasts() {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> refreshPodcasts() {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getPodcastClient()
|
||||
.refreshPodcasts()
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.refreshPodcasts();
|
||||
}
|
||||
|
||||
public void createPodcastChannel(String url) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> createPodcastChannel(String url) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getPodcastClient()
|
||||
.createPodcastChannel(url)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.createPodcastChannel(url);
|
||||
}
|
||||
|
||||
public void deletePodcastChannel(String channelId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> deletePodcastChannel(String channelId) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getPodcastClient()
|
||||
.deletePodcastChannel(channelId)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.deletePodcastChannel(channelId);
|
||||
}
|
||||
|
||||
public void deletePodcastEpisode(String episodeId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> deletePodcastEpisode(String episodeId) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getPodcastClient()
|
||||
.deletePodcastEpisode(episodeId)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.deletePodcastEpisode(episodeId);
|
||||
}
|
||||
|
||||
public void downloadPodcastEpisode(String episodeId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> downloadPodcastEpisode(String episodeId) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getPodcastClient()
|
||||
.downloadPodcastEpisode(episodeId)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.downloadPodcastEpisode(episodeId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||
@@ -52,6 +55,8 @@ public class QueueRepository {
|
||||
public MutableLiveData<PlayQueue> getPlayQueue() {
|
||||
MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>();
|
||||
|
||||
Log.d(TAG, "Getting play queue from server...");
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBookmarksClient()
|
||||
.getPlayQueue()
|
||||
@@ -59,12 +64,19 @@ public class QueueRepository {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) {
|
||||
playQueue.setValue(response.body().getSubsonicResponse().getPlayQueue());
|
||||
PlayQueue serverQueue = response.body().getSubsonicResponse().getPlayQueue();
|
||||
Log.d(TAG, "Server returned play queue with " +
|
||||
(serverQueue.getEntries() != null ? serverQueue.getEntries().size() : 0) + " items");
|
||||
playQueue.setValue(serverQueue);
|
||||
} else {
|
||||
Log.d(TAG, "Server returned no play queue");
|
||||
playQueue.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "Failed to get play queue", t);
|
||||
playQueue.setValue(null);
|
||||
}
|
||||
});
|
||||
@@ -73,18 +85,24 @@ public class QueueRepository {
|
||||
}
|
||||
|
||||
public void savePlayQueue(List<String> ids, String current, long position) {
|
||||
Log.d(TAG, "Saving play queue to server - Items: " + ids.size() + ", Current: " + current);
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBookmarksClient()
|
||||
.savePlayQueue(ids, current, position)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "Play queue saved successfully");
|
||||
} else {
|
||||
Log.d(TAG, "Play queue save failed with code: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
Log.e(TAG, "Play queue save failed", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -121,6 +139,14 @@ public class QueueRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMediaInQueue(List<Queue> queue, Child media) {
|
||||
if (queue == null || media == null) return false;
|
||||
return queue.stream().anyMatch(queueItem ->
|
||||
queueItem != null && media.getId() != null &&
|
||||
queueItem.getId().equals(media.getId())
|
||||
);
|
||||
}
|
||||
|
||||
public void insertAll(List<Child> toAdd, boolean reset, int afterIndex) {
|
||||
try {
|
||||
List<Queue> media = new ArrayList<>();
|
||||
@@ -134,8 +160,14 @@ public class QueueRepository {
|
||||
media = getMediaThreadSafe.getMedia();
|
||||
}
|
||||
|
||||
for (int i = 0; i < toAdd.size(); i++) {
|
||||
Queue queueItem = new Queue(toAdd.get(i));
|
||||
List<Child> filteredToAdd = toAdd;
|
||||
final List<Queue> finalMedia = media;
|
||||
filteredToAdd = toAdd.stream()
|
||||
.filter(child -> !isMediaInQueue(finalMedia, child))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (int i = 0; i < filteredToAdd.size(); i++) {
|
||||
Queue queueItem = new Queue(filteredToAdd.get(i));
|
||||
media.add(afterIndex + i, queueItem);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,54 +38,22 @@ public class RadioRepository {
|
||||
return radioStation;
|
||||
}
|
||||
|
||||
public void createInternetRadioStation(String name, String streamURL, String homepageURL) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> createInternetRadioStation(String name, String streamURL, String homepageURL) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getInternetRadioClient()
|
||||
.createInternetRadioStation(streamURL, name, homepageURL)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.createInternetRadioStation(streamURL, name, homepageURL);
|
||||
}
|
||||
|
||||
public void updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getInternetRadioClient()
|
||||
.updateInternetRadioStation(id, streamURL, name, homepageURL)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.updateInternetRadioStation(id, streamURL, name, homepageURL);
|
||||
}
|
||||
|
||||
public void deleteInternetRadioStation(String id) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
public Call<ApiResponse> deleteInternetRadioStation(String id) {
|
||||
return App.getSubsonicClientInstance(false)
|
||||
.getInternetRadioClient()
|
||||
.deleteInternetRadioStation(id)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
.deleteInternetRadioStation(id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -186,7 +187,12 @@ public class SearchingRepository {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
recent = recentSearchDao.getRecent();
|
||||
if(Preferences.isSearchSortingChronologicallyEnabled()){
|
||||
recent = recentSearchDao.getRecent();
|
||||
}
|
||||
else {
|
||||
recent = recentSearchDao.getAlpha();
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getRecent() {
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse;
|
||||
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class SongRepository {
|
||||
|
||||
private static final String TAG = "SongRepository";
|
||||
|
||||
public interface MediaCallbackInternal {
|
||||
void onSongsAvailable(List<Child> songs);
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
|
||||
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
|
||||
|
||||
@@ -42,25 +54,202 @@ public class SongRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
|
||||
return starredSongs;
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
|
||||
/**
|
||||
* Used by ViewModels. Updates the LiveData list incrementally as songs are found.
|
||||
*/
|
||||
public MutableLiveData<List<Child>> getInstantMix(String id, SeedType type, int count) {
|
||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(new ArrayList<>());
|
||||
Set<String> trackIds = new HashSet<>();
|
||||
|
||||
performSmartMix(id, type, count, songs -> {
|
||||
List<Child> current = instantMix.getValue();
|
||||
if (current != null) {
|
||||
for (Child s : songs) {
|
||||
if (!trackIds.contains(s.getId())) {
|
||||
current.add(s);
|
||||
trackIds.add(s.getId());
|
||||
}
|
||||
}
|
||||
|
||||
if (current.size() < count / 2) {
|
||||
fetchSimilarOnly(id, count, remainder -> {
|
||||
for (Child r : remainder) {
|
||||
if (!trackIds.contains(r.getId())) {
|
||||
current.add(r);
|
||||
trackIds.add(r.getId());
|
||||
}
|
||||
}
|
||||
instantMix.postValue(current);
|
||||
});
|
||||
} else {
|
||||
instantMix.postValue(current);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return instantMix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloaded method used by other Repositories
|
||||
*/
|
||||
public void getInstantMix(String id, SeedType type, int count, MediaCallbackInternal callback) {
|
||||
new MediaCallbackAccumulator(callback, count).start(id, type);
|
||||
}
|
||||
|
||||
private class MediaCallbackAccumulator {
|
||||
private final MediaCallbackInternal originalCallback;
|
||||
private final int targetCount;
|
||||
private final List<Child> accumulatedSongs = new ArrayList<>();
|
||||
private final Set<String> trackIds = new HashSet<>();
|
||||
private boolean isComplete = false;
|
||||
|
||||
MediaCallbackAccumulator(MediaCallbackInternal callback, int count) {
|
||||
this.originalCallback = callback;
|
||||
this.targetCount = count;
|
||||
}
|
||||
|
||||
void start(String id, SeedType type) {
|
||||
performSmartMix(id, type, targetCount, this::onBatchReceived);
|
||||
}
|
||||
|
||||
private void onBatchReceived(List<Child> batch) {
|
||||
if (isComplete || batch == null || batch.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int added = 0;
|
||||
for (Child song : batch) {
|
||||
if (!trackIds.contains(song.getId()) && accumulatedSongs.size() < targetCount) {
|
||||
trackIds.add(song.getId());
|
||||
accumulatedSongs.add(song);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
if (accumulatedSongs.size() >= targetCount) {
|
||||
originalCallback.onSongsAvailable(new ArrayList<>(accumulatedSongs));
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void performSmartMix(final String id, final SeedType type, final int count, final MediaCallbackInternal callback) {
|
||||
switch (type) {
|
||||
case ARTIST:
|
||||
fetchSimilarByArtist(id, count, callback);
|
||||
break;
|
||||
case ALBUM:
|
||||
fetchAlbumSongs(id, count, callback);
|
||||
break;
|
||||
case TRACK:
|
||||
fetchSingleTrackThenSimilar(id, count, callback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchAlbumSongs(String albumId, int count, MediaCallbackInternal callback) {
|
||||
App.getSubsonicClientInstance(false).getBrowsingClient().getAlbum(albumId).enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null &&
|
||||
response.body().getSubsonicResponse().getAlbum() != null) {
|
||||
List<Child> albumSongs = response.body().getSubsonicResponse().getAlbum().getSongs();
|
||||
if (albumSongs != null && !albumSongs.isEmpty()) {
|
||||
int fromAlbum = Math.min(count, albumSongs.size());
|
||||
List<Child> limitedAlbumSongs = albumSongs.subList(0, fromAlbum);
|
||||
callback.onSongsAvailable(new ArrayList<>(limitedAlbumSongs));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "fetchAlbumSongsThenSimilar.onFailure()", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchSimilarByArtist(String artistId, final int count, final MediaCallbackInternal callback) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getSimilarSongs2(artistId, count)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> similar = extractSongs(response, "similarSongs2");
|
||||
Log.d(TAG, "fetchSimilarByArtist.onResponse() - similar songs: " + similar.size());
|
||||
|
||||
if (!similar.isEmpty()) {
|
||||
List<Child> limitedSimilar = similar.subList(0, Math.min(count, similar.size()));
|
||||
callback.onSongsAvailable(limitedSimilar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "fetchSimilarByArtist.onFailure()", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchSingleTrackThenSimilar(String trackId, int count, MediaCallbackInternal callback) {
|
||||
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(trackId).enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
Child song = response.body().getSubsonicResponse().getSong();
|
||||
if (song != null) {
|
||||
callback.onSongsAvailable(Collections.singletonList(song));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "fetchSingleTrackThenSimilar.onFailure()", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchSimilarOnly(String id, int count, MediaCallbackInternal callback) {
|
||||
App.getSubsonicClientInstance(false).getBrowsingClient().getSimilarSongs(id, count).enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = extractSongs(response, "similarSongs");
|
||||
if (!songs.isEmpty()) {
|
||||
int limit = Math.min(count, songs.size());
|
||||
callback.onSongsAvailable(songs.subList(0, limit));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.e(TAG, "fetchSimilarOnly.onFailure()", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public MutableLiveData<List<Child>> getContinuousMix(String id, int count) {
|
||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getSimilarSongs2(id, count)
|
||||
.getSimilarSongs(id, count)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
||||
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs() != null) {
|
||||
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs().getSongs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,161 +262,128 @@ public class SongRepository {
|
||||
return instantMix;
|
||||
}
|
||||
|
||||
private List<Child> extractSongs(Response<ApiResponse> response, String type) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
SubsonicResponse res = response.body().getSubsonicResponse();
|
||||
List<Child> list = null;
|
||||
if (type.equals("similarSongs") && res.getSimilarSongs() != null) {
|
||||
list = res.getSimilarSongs().getSongs();
|
||||
} else if (type.equals("similarSongs2") && res.getSimilarSongs2() != null) {
|
||||
list = res.getSimilarSongs2().getSongs();
|
||||
}
|
||||
return (list != null) ? list : new ArrayList<>();
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
|
||||
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getAlbumSongListClient()
|
||||
.getRandomSongs(number, fromYear, toYear)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = new ArrayList<>();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
|
||||
songs.addAll(response.body().getSubsonicResponse().getRandomSongs().getSongs());
|
||||
}
|
||||
|
||||
randomSongsSample.setValue(songs);
|
||||
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = new ArrayList<>();
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||
if (returned != null) {
|
||||
songs.addAll(returned);
|
||||
}
|
||||
}
|
||||
randomSongsSample.setValue(songs);
|
||||
}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
return randomSongsSample;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
|
||||
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = new ArrayList<>();
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||
if (returned != null) {
|
||||
songs.addAll(returned);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
randomSongsSample.setValue(songs);
|
||||
}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
return randomSongsSample;
|
||||
}
|
||||
|
||||
public void scrobble(String id, boolean submission) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getMediaAnnotationClient()
|
||||
.scrobble(id, submission)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
}
|
||||
|
||||
public void setRating(String id, int rating) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getMediaAnnotationClient()
|
||||
.setRating(id, rating)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
|
||||
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getAlbumSongListClient()
|
||||
.getSongsByGenre(id, 100, 100 * page)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
||||
}
|
||||
}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
return songsByGenre;
|
||||
}
|
||||
|
||||
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
|
||||
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
||||
|
||||
for (String id : genresId)
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getAlbumSongListClient()
|
||||
.getSongsByGenre(id, 500, 0)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = new ArrayList<>();
|
||||
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||
songs.addAll(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
||||
}
|
||||
|
||||
songsByGenre.setValue(songs);
|
||||
for (String id : genresId) {
|
||||
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
List<Child> songs = new ArrayList<>();
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||
List<Child> returned = response.body().getSubsonicResponse().getSongsByGenre().getSongs();
|
||||
if (returned != null) {
|
||||
songs.addAll(returned);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
songsByGenre.setValue(songs);
|
||||
}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
}
|
||||
return songsByGenre;
|
||||
}
|
||||
|
||||
public MutableLiveData<Child> getSong(String id) {
|
||||
MutableLiveData<Child> song = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getSong(id)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
song.setValue(response.body().getSubsonicResponse().getSong());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
song.setValue(response.body().getSubsonicResponse().getSong());
|
||||
}
|
||||
}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
return song;
|
||||
}
|
||||
|
||||
public MutableLiveData<String> getSongLyrics(Child song) {
|
||||
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getMediaRetrievalClient()
|
||||
.getLyrics(song.getArtist(), song.getTitle())
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
|
||||
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback<ApiResponse>() {
|
||||
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
|
||||
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
|
||||
}
|
||||
}
|
||||
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||
});
|
||||
return lyrics;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Binder
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||
import androidx.media3.session.*
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.*
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
private const val TAG = "BaseMediaService"
|
||||
|
||||
@UnstableApi
|
||||
open class BaseMediaService : MediaLibraryService() {
|
||||
companion object {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
||||
"android.media3.session.demo.SHUFFLE_ON"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
|
||||
"android.media3.session.demo.SHUFFLE_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
|
||||
"android.media3.session.demo.REPEAT_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
|
||||
"android.media3.session.demo.REPEAT_ONE"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
||||
"android.media3.session.demo.REPEAT_ALL"
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
const val ACTION_EQUALIZER_UPDATED = "com.cappielloantonio.tempo.service.EQUALIZER_UPDATED"
|
||||
}
|
||||
|
||||
protected lateinit var exoplayer: ExoPlayer
|
||||
protected lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var networkCallback: CustomNetworkCallback
|
||||
private lateinit var equalizerManager: EqualizerManager
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
val player = mediaLibrarySession.player
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget(player)
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
open fun playerInitHook() {
|
||||
initializeExoPlayer()
|
||||
initializeMediaLibrarySession(exoplayer)
|
||||
initializePlayerListener(exoplayer)
|
||||
setPlayer(null, exoplayer)
|
||||
}
|
||||
|
||||
open fun getMediaLibrarySessionCallback(): MediaLibrarySession.Callback {
|
||||
return CustomMediaLibrarySessionCallback(baseContext)
|
||||
}
|
||||
|
||||
fun updateMediaItems(player: Player) {
|
||||
Log.d(TAG, "update items")
|
||||
val n = player.mediaItemCount
|
||||
val k = player.currentMediaItemIndex
|
||||
val current = player.currentPosition
|
||||
val items = (0..n - 1).map { MappingUtil.mapMediaItem(player.getMediaItemAt(it)) }
|
||||
player.clearMediaItems()
|
||||
player.setMediaItems(items, k, current)
|
||||
}
|
||||
|
||||
fun restorePlayerFromQueue(player: Player) {
|
||||
if (player.mediaItemCount > 0) return
|
||||
|
||||
val queueRepository = QueueRepository()
|
||||
val storedQueue = queueRepository.media
|
||||
if (storedQueue.isNullOrEmpty()) return
|
||||
|
||||
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
|
||||
if (mediaItems.isEmpty()) return
|
||||
|
||||
val lastIndex = try {
|
||||
queueRepository.lastPlayedMediaIndex
|
||||
} catch (_: Exception) {
|
||||
0
|
||||
}.coerceIn(0, mediaItems.size - 1)
|
||||
|
||||
val lastPosition = try {
|
||||
queueRepository.lastPlayedMediaTimestamp
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}.let { if (it < 0L) 0L else it }
|
||||
|
||||
player.setMediaItems(mediaItems, lastIndex, lastPosition)
|
||||
player.prepare()
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
fun initializePlayerListener(player: Player) {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
Log.d(TAG, "onMediaItemTransition" + player.currentMediaItemIndex)
|
||||
if (mediaItem == null) return
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
Log.d(TAG, "onTracksChanged " + player.currentMediaItemIndex)
|
||||
ReplayGainUtil.setReplayGain(player, tracks)
|
||||
val currentMediaItem = player.currentMediaItem
|
||||
if (currentMediaItem != null) {
|
||||
val item = MappingUtil.mapMediaItem(currentMediaItem)
|
||||
if (item.mediaMetadata.extras != null)
|
||||
MediaManager.scrobble(item, false)
|
||||
|
||||
if (player.nextMediaItemIndex == C.INDEX_UNSET) {
|
||||
val browserFuture = MediaBrowser.Builder(
|
||||
this@BaseMediaService,
|
||||
SessionToken(this@BaseMediaService, ComponentName(this@BaseMediaService, this@BaseMediaService::class.java))
|
||||
).buildAsync()
|
||||
MediaManager.continuousPlay(player.currentMediaItem, browserFuture)
|
||||
}
|
||||
}
|
||||
|
||||
if (player is ExoPlayer) {
|
||||
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
|
||||
if (MediaManager.justStarted.get()) {
|
||||
Log.d(TAG, "update shuffle order")
|
||||
MediaManager.justStarted.set(false)
|
||||
val shuffledList = IntArray(player.mediaItemCount) { i -> i }
|
||||
shuffledList.shuffle()
|
||||
val index = shuffledList.indexOf(player.currentMediaItemIndex)
|
||||
// swap current media index to the first index
|
||||
if (index > -1 && shuffledList.isNotEmpty()) {
|
||||
val tmp = shuffledList[0]
|
||||
shuffledList[0] = shuffledList[index]
|
||||
shuffledList[index] = tmp
|
||||
}
|
||||
player.shuffleOrder =
|
||||
DefaultShuffleOrder(shuffledList, kotlin.random.Random.nextLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||
if (!isPlaying) {
|
||||
MediaManager.setPlayingPausedTimestamp(
|
||||
player.currentMediaItem,
|
||||
player.currentPosition
|
||||
)
|
||||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
Log.d(TAG, "onPlaybackStateChanged")
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
if (!player.hasNextMediaItem() &&
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||
) {
|
||||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget(player)
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
Log.d(TAG, "onPositionDiscontinuity")
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.scrobble(oldPosition.mediaItem, true)
|
||||
MediaManager.saveChronology(oldPosition.mediaItem)
|
||||
}
|
||||
|
||||
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
Preferences.setRepeatMode(repeatMode)
|
||||
}
|
||||
|
||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||
Log.d(TAG, "onAudioSessionIdChanged")
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
|
||||
if (oldPlayer === newPlayer) return
|
||||
if (oldPlayer != null) {
|
||||
val currentQueue = getQueueFromPlayer(oldPlayer)
|
||||
val currentIndex = oldPlayer.currentMediaItemIndex
|
||||
val currentPosition = oldPlayer.currentPosition
|
||||
val isPlaying = oldPlayer.playWhenReady
|
||||
oldPlayer.stop()
|
||||
newPlayer.setMediaItems(currentQueue, currentIndex, currentPosition)
|
||||
newPlayer.playWhenReady = isPlaying
|
||||
newPlayer.prepare()
|
||||
}
|
||||
mediaLibrarySession.player = newPlayer
|
||||
}
|
||||
|
||||
open fun releasePlayers() {
|
||||
exoplayer.release()
|
||||
}
|
||||
|
||||
fun getQueueFromPlayer(player: Player): List<MediaItem> {
|
||||
return (0..player.mediaItemCount - 1).map(player::getMediaItemAt)
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaLibrarySession.player
|
||||
|
||||
if (!player.playWhenReady || player.mediaItemCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
playerInitHook()
|
||||
initializeEqualizerManager()
|
||||
initializeNetworkListener()
|
||||
restorePlayerFromQueue(mediaLibrarySession.player)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
releaseNetworkCallback()
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayers()
|
||||
mediaLibrarySession.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// Check if the intent is for our custom equalizer binder
|
||||
if (intent?.action == ACTION_BIND_EQUALIZER) {
|
||||
return binder
|
||||
}
|
||||
// Otherwise, handle it as a normal MediaLibraryService connection
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
private fun initializeExoPlayer() {
|
||||
exoplayer = ExoPlayer.Builder(this)
|
||||
.setRenderersFactory(getRenderersFactory())
|
||||
.setMediaSourceFactory(getMediaSourceFactory())
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.setLoadControl(initializeLoadControl())
|
||||
.build()
|
||||
|
||||
exoplayer.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
|
||||
exoplayer.repeatMode = Preferences.getRepeatMode()
|
||||
}
|
||||
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = exoplayer.audioSessionId
|
||||
attachEqualizerIfPossible(audioSessionId)
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession(player: Player) {
|
||||
Log.d(TAG, "initializeMediaLibrarySession")
|
||||
val sessionActivityPendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(Intent(baseContext, MainActivity::class.java))
|
||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, getMediaLibrarySessionCallback())
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun initializeNetworkListener() {
|
||||
networkCallback = CustomNetworkCallback()
|
||||
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(
|
||||
networkCallback
|
||||
)
|
||||
updateMediaItems(mediaLibrarySession.player)
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
return DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun releaseNetworkCallback() {
|
||||
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
||||
private fun updateWidget(player: Player) {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val extras = mi?.mediaMetadata?.extras
|
||||
val coverId = extras?.getString("coverArtId")
|
||||
val songLink = extras?.getString("assetLinkSong")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
|
||||
val albumLink = extras?.getString("assetLinkAlbum")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
|
||||
val artistLink = extras?.getString("assetLinkArtist")
|
||||
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration,
|
||||
songLink,
|
||||
albumLink,
|
||||
artistLink
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||
if (attached) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
sendBroadcast(Intent(ACTION_EQUALIZER_UPDATED))
|
||||
}
|
||||
return attached
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
|
||||
|
||||
@UnstableApi
|
||||
private class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
|
||||
private val shuffleCommands: List<CommandButton>
|
||||
private val repeatCommands: List<CommandButton>
|
||||
|
||||
constructor(ctx: Context) {
|
||||
shuffleCommands = listOf(
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON,
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF
|
||||
)
|
||||
.map { getShuffleCommandButton(SessionCommand(it, Bundle.EMPTY), ctx) }
|
||||
repeatCommands = listOf(
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL
|
||||
)
|
||||
.map { getRepeatCommandButton(SessionCommand(it, Bundle.EMPTY), ctx) }
|
||||
}
|
||||
|
||||
override fun onConnect(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
val connectionResult = super.onConnect(session, controller)
|
||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||
|
||||
(shuffleCommands + repeatCommands).forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||
}
|
||||
|
||||
val result = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(availableSessionCommands.build())
|
||||
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
|
||||
.setMediaButtonPreferences(buildCustomLayout(session.player))
|
||||
.build()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
Log.d(TAG, "onCustomCommand")
|
||||
when (customCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
||||
val nextMode = when (session.player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
||||
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE
|
||||
else -> Player.REPEAT_MODE_OFF
|
||||
}
|
||||
session.player.repeatMode = nextMode
|
||||
}
|
||||
}
|
||||
|
||||
session.setMediaButtonPreferences(buildCustomLayout(session.player))
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
override fun onAddMediaItems(
|
||||
mediaSession: MediaSession,
|
||||
controller: ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
Log.d(TAG, "onAddMediaItems")
|
||||
val updatedMediaItems = mediaItems.map { mediaItem ->
|
||||
val mediaMetadata = mediaItem.mediaMetadata
|
||||
val newMetadata = mediaMetadata.buildUpon()
|
||||
.setArtist(
|
||||
if (mediaMetadata.artist != null) mediaMetadata.artist
|
||||
else mediaMetadata.extras?.getString("uri") ?: ""
|
||||
)
|
||||
.build()
|
||||
|
||||
mediaItem.buildUpon()
|
||||
.setUri(mediaItem.requestMetadata.mediaUri)
|
||||
.setMediaMetadata(newMetadata)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.build()
|
||||
}
|
||||
return Futures.immediateFuture(updatedMediaItems)
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getShuffleCommandButton(
|
||||
sessionCommand: SessionCommand,
|
||||
ctx: Context
|
||||
): CommandButton {
|
||||
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
|
||||
return CommandButton.Builder(if (isOn) CommandButton.ICON_SHUFFLE_OFF else CommandButton.ICON_SHUFFLE_ON)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setDisplayName(
|
||||
ctx.getString(
|
||||
if (isOn) R.string.exo_controls_shuffle_on_description
|
||||
else R.string.exo_controls_shuffle_off_description
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateResource")
|
||||
private fun getRepeatCommandButton(
|
||||
sessionCommand: SessionCommand,
|
||||
ctx: Context
|
||||
): CommandButton {
|
||||
val icon = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> CommandButton.ICON_REPEAT_ONE
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> CommandButton.ICON_REPEAT_ALL
|
||||
else -> CommandButton.ICON_REPEAT_OFF
|
||||
}
|
||||
val description = when (sessionCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> R.string.exo_controls_repeat_one_description
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL -> R.string.exo_controls_repeat_all_description
|
||||
else -> R.string.exo_controls_repeat_off_description
|
||||
}
|
||||
return CommandButton.Builder(icon)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setDisplayName(ctx.getString(description))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
|
||||
val shuffle = shuffleCommands[if (player.shuffleModeEnabled) 1 else 0]
|
||||
val repeat = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> repeatCommands[1]
|
||||
Player.REPEAT_MODE_ALL -> repeatCommands[2]
|
||||
else -> repeatCommands[0]
|
||||
}
|
||||
return ImmutableList.of(shuffle, repeat)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() {
|
||||
var wasWifi = false
|
||||
|
||||
init {
|
||||
val manager = getSystemService(ConnectivityManager::class.java)
|
||||
val network = manager.activeNetwork
|
||||
val capabilities = manager.getNetworkCapabilities(network)
|
||||
if (capabilities != null)
|
||||
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
if (isWifi != wasWifi) {
|
||||
wasWifi = isWifi
|
||||
widgetUpdateHandler.post {
|
||||
updateMediaItems(mediaLibrarySession.player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
return equalizerManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.cappielloantonio.tempo.service;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.MediaItem;
|
||||
@@ -25,6 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
@@ -36,10 +38,16 @@ import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class MediaManager {
|
||||
private static final String TAG = "MediaManager";
|
||||
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
||||
public static AtomicBoolean justStarted = new AtomicBoolean(false);
|
||||
|
||||
private static final ExecutorService backgroundExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
public static void registerPlaybackObserver(
|
||||
ListenableFuture<MediaBrowser> browserFuture,
|
||||
@@ -173,33 +181,46 @@ public class MediaManager {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
||||
if (mediaBrowserListenableFuture != null) {
|
||||
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
browser.clearMediaItems();
|
||||
browser.setMediaItems(MappingUtil.mapMediaItems(media));
|
||||
browser.prepare();
|
||||
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
||||
|
||||
new Handler(Looper.getMainLooper()).post(() -> {
|
||||
justStarted.set(true);
|
||||
browser.setMediaItems(items, startIndex, 0);
|
||||
browser.prepare();
|
||||
|
||||
Player.Listener timelineListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
int itemCount = browser.getMediaItemCount();
|
||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||
browser.seekTo(startIndex, 0);
|
||||
browser.play();
|
||||
browser.removeListener(this);
|
||||
Player.Listener timelineListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
|
||||
int itemCount = browser.getMediaItemCount();
|
||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||
browser.seekTo(startIndex, 0);
|
||||
browser.play();
|
||||
browser.removeListener(this);
|
||||
} else {
|
||||
Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
browser.addListener(timelineListener);
|
||||
};
|
||||
|
||||
browser.addListener(timelineListener);
|
||||
});
|
||||
|
||||
enqueueDatabase(media, true, 0);
|
||||
backgroundExecutor.execute(() -> {
|
||||
Log.d(TAG, "Background: enqueuing to database");
|
||||
enqueueDatabase(media, true, 0);
|
||||
});
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
Log.e(TAG, "Error in startQueue: " + e.getMessage(), e);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
@@ -210,10 +231,11 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(media));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItem(MappingUtil.mapMediaItem(media));
|
||||
browser.prepare();
|
||||
browser.play();
|
||||
enqueueDatabase(media, true, 0);
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
@@ -229,7 +251,7 @@ public class MediaManager {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
mediaBrowser.clearMediaItems();
|
||||
justStarted.set(true);
|
||||
mediaBrowser.setMediaItem(mediaItem);
|
||||
mediaBrowser.prepare();
|
||||
mediaBrowser.play();
|
||||
@@ -247,10 +269,11 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
|
||||
browser.prepare();
|
||||
browser.play();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
@@ -264,10 +287,11 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
justStarted.set(true);
|
||||
browser.setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
|
||||
browser.prepare();
|
||||
browser.play();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
@@ -281,9 +305,11 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex());
|
||||
mediaBrowserListenableFuture.get().addMediaItems(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
|
||||
Log.e(TAG, "enqueue");
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
|
||||
browser.addMediaItems(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItems(media));
|
||||
} else {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
|
||||
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media));
|
||||
@@ -301,9 +327,11 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
if (playImmediatelyAfter && mediaBrowserListenableFuture.get().getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getNextMediaItemIndex());
|
||||
mediaBrowserListenableFuture.get().addMediaItem(mediaBrowserListenableFuture.get().getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
|
||||
Log.e(TAG, "enqueue");
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
if (playImmediatelyAfter && browser.getNextMediaItemIndex() != -1) {
|
||||
enqueueDatabase(media, false, browser.getNextMediaItemIndex());
|
||||
browser.addMediaItem(browser.getNextMediaItemIndex(), MappingUtil.mapMediaItem(media));
|
||||
} else {
|
||||
enqueueDatabase(media, false, mediaBrowserListenableFuture.get().getMediaItemCount());
|
||||
mediaBrowserListenableFuture.get().addMediaItem(MappingUtil.mapMediaItem(media));
|
||||
@@ -321,8 +349,10 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().removeMediaItems(startIndex, endIndex + 1);
|
||||
mediaBrowserListenableFuture.get().addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
|
||||
Log.e(TAG, "shuffle");
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
browser.removeMediaItems(startIndex, endIndex + 1);
|
||||
browser.addMediaItems(MappingUtil.mapMediaItems(media).subList(startIndex, endIndex + 1));
|
||||
swapDatabase(media);
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
@@ -337,6 +367,7 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
Log.e(TAG, "swap");
|
||||
mediaBrowserListenableFuture.get().moveMediaItem(from, to);
|
||||
swapDatabase(media);
|
||||
}
|
||||
@@ -352,6 +383,7 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
Log.e(TAG, "remove");
|
||||
if (mediaBrowserListenableFuture.get().getMediaItemCount() > 1 && mediaBrowserListenableFuture.get().getCurrentMediaItemIndex() != toRemove) {
|
||||
mediaBrowserListenableFuture.get().removeMediaItem(toRemove);
|
||||
removeDatabase(media, toRemove);
|
||||
@@ -371,6 +403,7 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
Log.e(TAG, "remove range");
|
||||
mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem);
|
||||
removeRangeDatabase(media, fromItem, toItem);
|
||||
}
|
||||
@@ -411,27 +444,33 @@ public class MediaManager {
|
||||
}
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public static void continuousPlay(MediaItem mediaItem) {
|
||||
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
|
||||
Preferences.setLastInstantMix();
|
||||
|
||||
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
|
||||
instantMix.observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> media) {
|
||||
if (media != null) {
|
||||
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
|
||||
App.getContext(),
|
||||
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))
|
||||
).buildAsync();
|
||||
|
||||
enqueue(mediaBrowserListenableFuture, media, true);
|
||||
}
|
||||
|
||||
instantMix.removeObserver(this);
|
||||
}
|
||||
});
|
||||
public static void continuousPlay(MediaItem mediaItem,
|
||||
ListenableFuture<MediaBrowser> existingBrowserFuture) {
|
||||
if (mediaItem == null
|
||||
|| !Preferences.isContinuousPlayEnabled()
|
||||
|| !Preferences.isInstantMixUsable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Preferences.setLastInstantMix();
|
||||
|
||||
LiveData<List<Child>> instantMix =
|
||||
getSongRepository().getContinuousMix(mediaItem.mediaId, 25);
|
||||
|
||||
instantMix.observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> media) {
|
||||
if (media == null || media.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingBrowserFuture != null) {
|
||||
Log.d(TAG, "Continuous play: adding " + media.size() + " tracks");
|
||||
enqueue(existingBrowserFuture, media, true);
|
||||
}
|
||||
instantMix.removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void saveChronology(MediaItem mediaItem) {
|
||||
|
||||
@@ -34,6 +34,11 @@ public class AlbumSongListClient {
|
||||
return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear);
|
||||
}
|
||||
|
||||
public Call<ApiResponse> getRandomSongs(int size, Integer fromYear, Integer toYear, String genre) {
|
||||
Log.d(TAG, "getRandomSongs()");
|
||||
return albumSongListService.getRandomSongs(subsonic.getParams(), size, fromYear, toYear, genre);
|
||||
}
|
||||
|
||||
public Call<ApiResponse> getSongsByGenre(String genre, int count, int offset) {
|
||||
Log.d(TAG, "getSongsByGenre()");
|
||||
return albumSongListService.getSongsByGenre(subsonic.getParams(), genre, count, offset);
|
||||
|
||||
@@ -19,6 +19,9 @@ public interface AlbumSongListService {
|
||||
@GET("getRandomSongs")
|
||||
Call<ApiResponse> getRandomSongs(@QueryMap Map<String, String> params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear);
|
||||
|
||||
@GET("getRandomSongs")
|
||||
Call<ApiResponse> getRandomSongs(@QueryMap Map<String, String> params, @Query("size") int size, @Query("fromYear") Integer fromYear, @Query("toYear") Integer toYear, @Query("genre") String genre);
|
||||
|
||||
@GET("getSongsByGenre")
|
||||
Call<ApiResponse> getSongsByGenre(@QueryMap Map<String, String> params, @Query("genre") String genre, @Query("count") int count, @Query("offset") int offset);
|
||||
|
||||
|
||||
@@ -24,13 +24,15 @@ public class SystemClient {
|
||||
|
||||
public Call<ApiResponse> ping() {
|
||||
Log.d(TAG, "ping()");
|
||||
int timeoutSeconds = Preferences.getNetworkPingTimeout();
|
||||
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
||||
if (Preferences.isInUseServerAddressLocal()) {
|
||||
pingCall.timeout()
|
||||
.timeout(1, TimeUnit.SECONDS);
|
||||
.timeout(timeoutSeconds, TimeUnit.SECONDS);
|
||||
} else {
|
||||
int finalTimeout = Math.min(timeoutSeconds * 2, 10);
|
||||
pingCall.timeout()
|
||||
.timeout(3, TimeUnit.SECONDS);
|
||||
.timeout(finalTimeout, TimeUnit.SECONDS);
|
||||
}
|
||||
return pingCall;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ open class Playlist(
|
||||
var name: String? = null,
|
||||
@ColumnInfo(name = "duration")
|
||||
var duration: Long = 0,
|
||||
@SerializedName("coverArt")
|
||||
@ColumnInfo(name = "coverArt")
|
||||
var coverArtId: String? = null,
|
||||
) : Parcelable {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.cappielloantonio.tempo.subsonic.models
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Keep
|
||||
class SimilarSongs {
|
||||
@SerializedName("song")
|
||||
var songs: List<Child>? = null
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.cappielloantonio.tempo.ui.activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
@@ -11,6 +13,7 @@ import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
@@ -62,6 +65,7 @@ public class MainActivity extends BaseActivity {
|
||||
private BottomNavigationView bottomNavigationView;
|
||||
public NavController navController;
|
||||
private BottomSheetBehavior bottomSheetBehavior;
|
||||
private boolean isLandscape = false;
|
||||
private AssetLinkNavigator assetLinkNavigator;
|
||||
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||
|
||||
@@ -85,6 +89,8 @@ public class MainActivity extends BaseActivity {
|
||||
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
||||
connectivityStatusReceiverManager(true);
|
||||
|
||||
isLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
|
||||
|
||||
init();
|
||||
checkConnectionType();
|
||||
getOpenSubsonicExtensions();
|
||||
@@ -141,6 +147,15 @@ public class MainActivity extends BaseActivity {
|
||||
} else {
|
||||
goToLogin();
|
||||
}
|
||||
|
||||
// Set bottom navigation height
|
||||
if (isLandscape) {
|
||||
ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams();
|
||||
Rect windowRect = new Rect();
|
||||
bottomNavigationView.getWindowVisibleDisplayFrame(windowRect);
|
||||
layoutParams.width = windowRect.height();
|
||||
bottomNavigationView.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
|
||||
// BOTTOM SHEET/NAVIGATION
|
||||
@@ -215,7 +230,9 @@ public class MainActivity extends BaseActivity {
|
||||
@Override
|
||||
public void onSlide(@NonNull View view, float slideOffset) {
|
||||
animateBottomSheet(slideOffset);
|
||||
animateBottomNavigation(slideOffset, navigationHeight);
|
||||
if (!isLandscape) {
|
||||
animateBottomNavigation(slideOffset, navigationHeight);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -354,7 +371,7 @@ public class MainActivity extends BaseActivity {
|
||||
|
||||
// TODO Enter all settings to be reset
|
||||
Preferences.setOpenSubsonic(false);
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
|
||||
Preferences.setPlaybackSpeed(1.0f);
|
||||
Preferences.setSkipSilenceMode(false);
|
||||
Preferences.setDataSavingMode(false);
|
||||
Preferences.setStarredSyncEnabled(false);
|
||||
@@ -384,7 +401,7 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void pingServer() {
|
||||
if (Preferences.getToken() == null) return;
|
||||
if (Preferences.getToken() == null && Preferences.getPassword() == null) return;
|
||||
|
||||
if (Preferences.isInUseServerAddressLocal()) {
|
||||
mainViewModel.ping().observe(this, subsonicResponse -> {
|
||||
@@ -428,7 +445,7 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void getOpenSubsonicExtensions() {
|
||||
if (Preferences.getToken() != null) {
|
||||
if (Preferences.getToken() != null || Preferences.getPassword() != null) {
|
||||
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
||||
if (openSubsonicExtensions != null) {
|
||||
Preferences.setOpenSubsonicExtensions(openSubsonicExtensions);
|
||||
@@ -438,7 +455,7 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
|
||||
private void checkTempoUpdate() {
|
||||
if (BuildConfig.FLAVOR.equals("tempus") && Preferences.showTempoUpdateDialog()) {
|
||||
if (BuildConfig.FLAVOR.equals("tempus") && Preferences.isGithubUpdateEnabled() && Preferences.showTempusUpdateDialog()) {
|
||||
mainViewModel.checkTempoUpdate().observe(this, latestRelease -> {
|
||||
if (latestRelease != null && UpdateUtil.showUpdateDialog(latestRelease)) {
|
||||
GithubTempoUpdateDialog dialog = new GithubTempoUpdateDialog(latestRelease);
|
||||
|
||||
@@ -105,6 +105,16 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
|
||||
filtering.filter(currentFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filtering;
|
||||
|
||||
@@ -66,6 +66,16 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ItemLibraryArtistBinding item;
|
||||
|
||||
|
||||
@@ -97,6 +97,16 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filtering;
|
||||
|
||||
@@ -113,6 +113,16 @@ public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizont
|
||||
return artists.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ItemHorizontalArtistBinding item;
|
||||
|
||||
|
||||
@@ -60,6 +60,16 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ItemLibrarySimilarArtistBinding item;
|
||||
|
||||
|
||||
@@ -73,6 +73,11 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
|
||||
this.item = item;
|
||||
|
||||
itemView.setOnClickListener(v -> onClick());
|
||||
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
onLongClick();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public void onClick() {
|
||||
@@ -82,6 +87,13 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
|
||||
|
||||
click.onMediaClick(bundle);
|
||||
}
|
||||
|
||||
private boolean onLongClick() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
||||
click.onMediaLongClick(bundle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void startAnimation(ViewHolder holder) {
|
||||
|
||||
@@ -96,6 +96,16 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
|
||||
return shuffling;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
private List<Child> groupSong(List<Child> songs) {
|
||||
switch (view) {
|
||||
case Constants.DOWNLOAD_TYPE_TRACK:
|
||||
|
||||
@@ -53,7 +53,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
||||
.into(holder.item.musicDirectoryCoverImageView);
|
||||
|
||||
holder.item.musicDirectoryMoreButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
|
||||
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.INVISIBLE : View.VISIBLE);
|
||||
holder.item.musicDirectoryPlayButton.setVisibility(child.isDir() ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,6 +80,7 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
||||
itemView.setOnLongClickListener(v -> onLongClick());
|
||||
|
||||
item.musicDirectoryMoreButton.setOnClickListener(v -> onClick());
|
||||
item.musicDirectoryPlayButton.setOnClickListener(v -> onPlayClick());
|
||||
}
|
||||
|
||||
public void onClick() {
|
||||
@@ -107,5 +108,13 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void onPlayClick() {
|
||||
if (children.get(getBindingAdapterPosition()).isDir()) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.MUSIC_DIRECTORY_ID, children.get(getBindingAdapterPosition()).getId());
|
||||
click.onMusicDirectoryPlay(bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
|
||||
|
||||
itemView.setOnClickListener(v -> onClick());
|
||||
item.musicIndexMoreButton.setOnClickListener(v -> onClick());
|
||||
item.musicIndexPlayButton.setOnClickListener(v -> onPlayClick());
|
||||
}
|
||||
|
||||
public void onClick() {
|
||||
@@ -83,5 +84,11 @@ public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.Vi
|
||||
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
|
||||
click.onMusicIndexClick(bundle);
|
||||
}
|
||||
|
||||
public void onPlayClick() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.MUSIC_DIRECTORY_ID, artists.get(getBindingAdapterPosition()).getId());
|
||||
click.onMusicIndexPlay(bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,12 @@ import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
|
||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
@@ -29,7 +32,9 @@ import com.google.common.util.concurrent.MoreExecutors;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
|
||||
private static final String TAG = "PlayerSongQueueAdapter";
|
||||
@@ -37,7 +42,7 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private List<Child> songs;
|
||||
|
||||
private final Map<String, Boolean> downloadStatusCache = new ConcurrentHashMap<>();
|
||||
private String currentPlayingId;
|
||||
private boolean isPlaying;
|
||||
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||
@@ -78,7 +83,6 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
||||
.build()
|
||||
.thumbnail(thumbnail)
|
||||
.into(holder.item.queueSongCoverImageView);
|
||||
|
||||
MediaManager.getCurrentIndex(mediaBrowserListenableFuture, new MediaIndexCallback() {
|
||||
@Override
|
||||
public void onRecovery(int index) {
|
||||
@@ -94,6 +98,23 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
||||
}
|
||||
});
|
||||
|
||||
boolean isDownloaded = false;
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(holder.itemView.getContext());
|
||||
if (downloaderManager != null) {
|
||||
isDownloaded = downloaderManager.isDownloaded(song.getId());
|
||||
}
|
||||
} else {
|
||||
isDownloaded = ExternalAudioReader.getUri(song) != null;
|
||||
}
|
||||
|
||||
if (isDownloaded) {
|
||||
holder.item.downloadIndicatorIcon.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.downloadIndicatorIcon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (Preferences.showItemRating()) {
|
||||
if (song.getStarred() == null && song.getUserRating() == null) {
|
||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
||||
@@ -153,7 +174,7 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
||||
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public List<Child> getItems() {
|
||||
return this.songs;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,16 @@ public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter<Podcast
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filtering;
|
||||
|
||||
@@ -71,6 +71,16 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
ItemHomePodcastEpisodeBinding item;
|
||||
|
||||
|
||||
@@ -173,10 +173,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
)
|
||||
)
|
||||
) {
|
||||
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
|
||||
|
||||
if (songs.get(position).getDiscNumber() != null && !Objects.requireNonNull(songs.get(position).getDiscNumber()).toString().isBlank()) {
|
||||
holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titleless, songs.get(position).getDiscNumber().toString()));
|
||||
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.differentDiskDividerSector.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (album.getDiscTitles() != null) {
|
||||
@@ -252,6 +254,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPlaybackState(String mediaId, boolean playing) {
|
||||
String oldId = this.currentPlayingId;
|
||||
boolean oldPlaying = this.isPlaying;
|
||||
|
||||
@@ -55,7 +55,7 @@ public class GithubTempoUpdateDialog extends DialogFragment {
|
||||
});
|
||||
|
||||
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> {
|
||||
Preferences.setTempoUpdateReminder();
|
||||
Preferences.setTempusUpdateReminder();
|
||||
Objects.requireNonNull(getDialog()).dismiss();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public class PlaybackSpeedDialog extends DialogFragment {
|
||||
private static final String TAG = "PlaybackSpeedDialog";
|
||||
|
||||
public interface PlaybackSpeedListener {
|
||||
void onSpeedSelected(float speed);
|
||||
}
|
||||
|
||||
private PlaybackSpeedListener listener;
|
||||
|
||||
private static final float[] SPEED_VALUES = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
|
||||
private static final String[] SPEED_LABELS = {"0.5x", "0.75x", "1.0x", "1.25x", "1.5x", "1.75x", "2.0x"};
|
||||
|
||||
public void setPlaybackSpeedListener(PlaybackSpeedListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
float currentSpeed = Preferences.getPlaybackSpeed();
|
||||
int selectedIndex = getSelectedIndex(currentSpeed);
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireActivity())
|
||||
.setTitle(R.string.playback_speed_dialog_title)
|
||||
.setSingleChoiceItems(SPEED_LABELS, selectedIndex, (dialog, which) -> {
|
||||
float selectedSpeed = SPEED_VALUES[which];
|
||||
Preferences.setPlaybackSpeed(selectedSpeed);
|
||||
if (listener != null) {
|
||||
listener.onSpeedSelected(selectedSpeed);
|
||||
}
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(R.string.playback_speed_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
||||
.create();
|
||||
}
|
||||
|
||||
private int getSelectedIndex(float currentSpeed) {
|
||||
for (int i = 0; i < SPEED_VALUES.length; i++) {
|
||||
if (Math.abs(SPEED_VALUES[i] - currentSpeed) < 0.01f) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 2; // Default to 1.0x
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
||||
private DialogPlaylistChooserBinding bind;
|
||||
@@ -35,9 +37,21 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||
|
||||
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
||||
|
||||
String[] playlistVisibilityChoice = {
|
||||
getString(R.string.playlist_chooser_dialog_visibility_public),
|
||||
getString(R.string.playlist_chooser_dialog_visibility_private)
|
||||
};
|
||||
|
||||
return new MaterialAlertDialogBuilder(getActivity())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.playlist_chooser_dialog_title)
|
||||
.setSingleChoiceItems(
|
||||
playlistVisibilityChoice,
|
||||
0,
|
||||
(dialog, which) -> {
|
||||
boolean isPublic = (which == 0);
|
||||
playlistChooserViewModel.setIsPlaylistPublic(isPublic);
|
||||
})
|
||||
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
||||
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
||||
.create();
|
||||
|
||||
@@ -3,11 +3,13 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogRadioEditorBinding;
|
||||
import com.cappielloantonio.tempo.interfaces.RadioCallback;
|
||||
@@ -21,7 +23,6 @@ import java.util.Objects;
|
||||
public class RadioEditorDialog extends DialogFragment {
|
||||
private DialogRadioEditorBinding bind;
|
||||
private RadioEditorViewModel radioEditorViewModel;
|
||||
|
||||
private final RadioCallback radioCallback;
|
||||
|
||||
private String radioName;
|
||||
@@ -36,25 +37,26 @@ public class RadioEditorDialog extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
bind = DialogRadioEditorBinding.inflate(getLayoutInflater());
|
||||
|
||||
radioEditorViewModel = new ViewModelProvider(requireActivity()).get(RadioEditorViewModel.class);
|
||||
|
||||
setupObservers();
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.radio_editor_dialog_title)
|
||||
.setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> {
|
||||
if (validateInput()) {
|
||||
if (radioEditorViewModel.getRadioToEdit() == null) {
|
||||
radioEditorViewModel.createRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL);
|
||||
radioEditorViewModel.createRadio(radioName, radioStreamURL,
|
||||
radioHomepageURL.isEmpty() ? null : radioHomepageURL);
|
||||
} else {
|
||||
radioEditorViewModel.updateRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL);
|
||||
radioEditorViewModel.updateRadio(radioName, radioStreamURL,
|
||||
radioHomepageURL.isEmpty() ? null : radioHomepageURL);
|
||||
}
|
||||
dismissDialog();
|
||||
}
|
||||
})
|
||||
.setNeutralButton(R.string.radio_editor_dialog_neutral_button, (dialog, id) -> {
|
||||
radioEditorViewModel.deleteRadio();
|
||||
dismissDialog();
|
||||
})
|
||||
.setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> {
|
||||
dialog.cancel();
|
||||
@@ -62,6 +64,24 @@ public class RadioEditorDialog extends DialogFragment {
|
||||
.create();
|
||||
}
|
||||
|
||||
private void setupObservers() {
|
||||
radioEditorViewModel.getIsSuccess().observe(this, isSuccess -> {
|
||||
if (isSuccess != null && isSuccess) {
|
||||
Toast.makeText(requireContext(),
|
||||
radioEditorViewModel.getRadioToEdit() == null ?
|
||||
App.getContext().getString(R.string.radio_editor_dialog_added) : App.getContext().getString(R.string.radio_editor_dialog_updated),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
dismissDialog();
|
||||
}
|
||||
});
|
||||
radioEditorViewModel.getErrorMessage().observe(this, error -> {
|
||||
if (error != null && !error.isEmpty()) {
|
||||
Toast.makeText(requireContext(), error, Toast.LENGTH_LONG).show();
|
||||
radioEditorViewModel.clearError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
@@ -77,7 +97,6 @@ public class RadioEditorDialog extends DialogFragment {
|
||||
private void setParameterInfo() {
|
||||
if (getArguments() != null && getArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT) != null) {
|
||||
InternetRadioStation toEdit = requireArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT);
|
||||
|
||||
radioEditorViewModel.setRadioToEdit(toEdit);
|
||||
|
||||
bind.internetRadioStationNameTextView.setText(toEdit.getName());
|
||||
@@ -90,22 +109,21 @@ public class RadioEditorDialog extends DialogFragment {
|
||||
radioName = Objects.requireNonNull(bind.internetRadioStationNameTextView.getText()).toString().trim();
|
||||
radioStreamURL = Objects.requireNonNull(bind.internetRadioStationStreamUrlTextView.getText()).toString().trim();
|
||||
radioHomepageURL = Objects.requireNonNull(bind.internetRadioStationHomepageUrlTextView.getText()).toString().trim();
|
||||
|
||||
if (TextUtils.isEmpty(radioName)) {
|
||||
bind.internetRadioStationNameTextView.setError(getString(R.string.error_required));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(radioStreamURL)) {
|
||||
bind.internetRadioStationStreamUrlTextView.setError(getString(R.string.error_required));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void dismissDialog() {
|
||||
radioCallback.onDismiss();
|
||||
if (radioCallback != null) {
|
||||
radioCallback.onDismiss();
|
||||
}
|
||||
Objects.requireNonNull(getDialog()).dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -49,6 +50,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
||||
|
||||
private AlbumCatalogueAdapter albumAdapter;
|
||||
private int spanCount = 2;
|
||||
private String currentSortOrder;
|
||||
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
|
||||
|
||||
@@ -90,6 +92,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
||||
}
|
||||
|
||||
initAppBar();
|
||||
initAlbumCatalogueView();
|
||||
initProgressLoader();
|
||||
@@ -133,8 +139,8 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void initAlbumCatalogueView() {
|
||||
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
||||
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
||||
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
||||
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
|
||||
|
||||
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -12,6 +12,7 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -60,12 +61,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
/** @noinspection deprecation*/
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
/** @noinspection deprecation*/
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
@@ -81,7 +84,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
init(view);
|
||||
initAppBar();
|
||||
initAlbumInfoTextButton();
|
||||
initAlbumNotes();
|
||||
@@ -119,12 +122,13 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
bind = null;
|
||||
}
|
||||
|
||||
/** @noinspection deprecation*/
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_rate_album) {
|
||||
Bundle bundle = new Bundle();
|
||||
AlbumID3 album = albumPageViewModel.getAlbum().getValue();
|
||||
bundle.putParcelable(Constants.ALBUM_OBJECT, (Parcelable) album);
|
||||
bundle.putParcelable(Constants.ALBUM_OBJECT, album);
|
||||
RatingDialog dialog = new RatingDialog();
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
@@ -159,8 +163,21 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
albumPageViewModel.setAlbum(getViewLifecycleOwner(), requireArguments().getParcelable(Constants.ALBUM_OBJECT));
|
||||
private void init(View view) {
|
||||
AlbumID3 albumArg = requireArguments().getParcelable(Constants.ALBUM_OBJECT);
|
||||
assert albumArg != null;
|
||||
albumPageViewModel.setAlbum(getViewLifecycleOwner(), albumArg);
|
||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(albumArg.getStarred() != null);
|
||||
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
albumPageViewModel.setFavorite();
|
||||
});
|
||||
albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> {
|
||||
if (album != null) {
|
||||
favoriteToggle.setChecked(album.getStarred() != null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initAppBar() {
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -50,6 +51,7 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
private ArtistCatalogueViewModel artistCatalogueViewModel;
|
||||
|
||||
private ArtistCatalogueAdapter artistAdapter;
|
||||
private int spanCount = 2;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -66,6 +68,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
||||
}
|
||||
|
||||
initAppBar();
|
||||
initArtistCatalogueView();
|
||||
|
||||
@@ -108,8 +114,8 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void initArtistCatalogueView() {
|
||||
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
||||
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
||||
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
||||
bind.artistCatalogueRecyclerView.setHasFixedSize(true);
|
||||
|
||||
artistAdapter = new ArtistCatalogueAdapter(this);
|
||||
|
||||
@@ -2,15 +2,22 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
@@ -28,18 +35,21 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@UnstableApi
|
||||
public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
@@ -54,6 +64,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private int spanCount = 2;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
activity = (MainActivity) getActivity();
|
||||
@@ -63,7 +75,11 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
||||
}
|
||||
|
||||
init(view);
|
||||
initAppBar();
|
||||
initArtistInfo();
|
||||
initPlayButtons();
|
||||
@@ -100,7 +116,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
bind = null;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
private void init(View view) {
|
||||
artistPageViewModel.setArtist(requireArguments().getParcelable(Constants.ARTIST_OBJECT));
|
||||
|
||||
bind.mostStreamedSongTextViewClickable.setOnClickListener(v -> {
|
||||
@@ -109,6 +125,14 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
bundle.putParcelable(Constants.ARTIST_OBJECT, artistPageViewModel.getArtist());
|
||||
activity.navController.navigate(R.id.action_artistPageFragment_to_songListPageFragment, bundle);
|
||||
});
|
||||
|
||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(artistPageViewModel.getArtist().getStarred() != null);
|
||||
favoriteToggle.setOnClickListener(v -> artistPageViewModel.setFavorite(requireContext()));
|
||||
|
||||
Button bioToggle = view.findViewById(R.id.button_toggle_bio);
|
||||
bioToggle.setOnClickListener(v ->
|
||||
Toast.makeText(getActivity(), R.string.artist_no_artist_info_toast, Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
private void initAppBar() {
|
||||
@@ -126,53 +150,118 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
if (artistInfo == null) {
|
||||
if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE);
|
||||
} else {
|
||||
String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography());
|
||||
if (getContext() != null && bind != null) {
|
||||
ArtistID3 currentArtist = artistPageViewModel.getArtist();
|
||||
String primaryId = currentArtist.getCoverArtId() != null && !currentArtist.getCoverArtId().trim().isEmpty()
|
||||
? currentArtist.getCoverArtId()
|
||||
: currentArtist.getId();
|
||||
|
||||
final String fallbackId = (Objects.requireNonNull(primaryId).equals(currentArtist.getCoverArtId()) &&
|
||||
currentArtist.getId() != null &&
|
||||
!currentArtist.getId().equals(primaryId))
|
||||
? currentArtist.getId()
|
||||
: null;
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), primaryId, CustomGlideRequest.ResourceType.Artist)
|
||||
.build()
|
||||
.listener(new com.bumptech.glide.request.RequestListener<Drawable>() {
|
||||
@Override
|
||||
public boolean onLoadFailed(@Nullable com.bumptech.glide.load.engine.GlideException e,
|
||||
Object model,
|
||||
@NonNull com.bumptech.glide.request.target.Target<Drawable> target,
|
||||
boolean isFirstResource) {
|
||||
if (e != null) {
|
||||
e.getMessage();
|
||||
if (e.getMessage().contains("400") && fallbackId != null) {
|
||||
|
||||
if (bind != null)
|
||||
bind.artistPageBioSector.setVisibility(!normalizedBio.trim().isEmpty() ? View.VISIBLE : View.GONE);
|
||||
if (bind != null)
|
||||
bind.bioMoreTextViewClickable.setVisibility(artistInfo.getLastFmUrl() != null ? View.VISIBLE : View.GONE);
|
||||
Log.d("ArtistCover", "Primary ID failed (400), trying fallback: " + fallbackId);
|
||||
|
||||
if (getContext() != null && bind != null) CustomGlideRequest.Builder
|
||||
.from(requireContext(), artistPageViewModel.getArtist().getId(), CustomGlideRequest.ResourceType.Artist)
|
||||
.build()
|
||||
.into(bind.artistBackdropImageView);
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), fallbackId, CustomGlideRequest.ResourceType.Artist)
|
||||
.build()
|
||||
.into(bind.artistBackdropImageView);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bind != null) bind.bioTextView.setText(normalizedBio);
|
||||
@Override
|
||||
public boolean onResourceReady(@NonNull Drawable resource,
|
||||
@NonNull Object model,
|
||||
com.bumptech.glide.request.target.Target<Drawable> target,
|
||||
@NonNull com.bumptech.glide.load.DataSource dataSource,
|
||||
boolean isFirstResource) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.into(bind.artistBackdropImageView);
|
||||
}
|
||||
|
||||
if (bind != null) bind.bioMoreTextViewClickable.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(artistInfo.getLastFmUrl()));
|
||||
startActivity(intent);
|
||||
});
|
||||
if (bind != null) {
|
||||
String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography()).trim();
|
||||
String lastFmUrl = artistInfo.getLastFmUrl();
|
||||
|
||||
if (bind != null) bind.artistPageBioSector.setVisibility(View.VISIBLE);
|
||||
if (normalizedBio.isEmpty()) {
|
||||
bind.bioTextView.setVisibility(View.GONE);
|
||||
} else {
|
||||
bind.bioTextView.setText(normalizedBio);
|
||||
}
|
||||
|
||||
if (lastFmUrl == null) {
|
||||
bind.bioMoreTextViewClickable.setVisibility(View.GONE);
|
||||
} else {
|
||||
bind.bioMoreTextViewClickable.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(artistInfo.getLastFmUrl()));
|
||||
startActivity(intent);
|
||||
});
|
||||
bind.bioMoreTextViewClickable.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (!normalizedBio.isEmpty() || lastFmUrl != null) {
|
||||
View view = bind.getRoot();
|
||||
|
||||
Button bioToggle = view.findViewById(R.id.button_toggle_bio);
|
||||
bioToggle.setOnClickListener(v -> {
|
||||
if (bind != null) {
|
||||
boolean displayBio = Preferences.getArtistDisplayBiography();
|
||||
Preferences.setArtistDisplayBiography(!displayBio);
|
||||
bind.artistPageBioSector.setVisibility(displayBio ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
boolean displayBio = Preferences.getArtistDisplayBiography();
|
||||
bind.artistPageBioSector.setVisibility(displayBio ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initPlayButtons() {
|
||||
bind.artistPageShuffleButton.setOnClickListener(v -> {
|
||||
artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (!songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
} else {
|
||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bind.artistPageRadioButton.setOnClickListener(v -> {
|
||||
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
||||
bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
} else {
|
||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show();
|
||||
artistPageViewModel.getArtistShuffleList().removeObserver(this);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
artistPageViewModel.getArtistInstantMix().removeObserver(this);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void initTopSongsView() {
|
||||
@@ -195,8 +284,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
|
||||
private void initAlbumsView() {
|
||||
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
||||
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
||||
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
||||
bind.albumsRecyclerView.setHasFixedSize(true);
|
||||
|
||||
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
|
||||
@@ -214,8 +303,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
|
||||
private void initSimilarArtistsView() {
|
||||
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
||||
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
||||
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false));
|
||||
bind.similarArtistsRecyclerView.setHasFixedSize(true);
|
||||
|
||||
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);
|
||||
|
||||
@@ -27,7 +27,13 @@ import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.repository.DirectoryRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Directory;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||
@@ -53,6 +59,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
private MusicDirectoryAdapter musicDirectoryAdapter;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private DirectoryRepository directoryRepository;
|
||||
|
||||
private MenuItem menuItem;
|
||||
|
||||
@@ -77,6 +84,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentDirectoryBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
|
||||
directoryRepository = new DirectoryRepository();
|
||||
|
||||
initAppBar();
|
||||
initDirectoryListView();
|
||||
@@ -197,4 +205,57 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
public void onMusicDirectoryClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicDirectoryPlay(Bundle bundle) {
|
||||
String directoryId = bundle.getString(Constants.MUSIC_DIRECTORY_ID);
|
||||
if (directoryId != null) {
|
||||
Toast.makeText(requireContext(), getString(R.string.folder_play_collecting), Toast.LENGTH_SHORT).show();
|
||||
collectAndPlayDirectorySongs(directoryId);
|
||||
}
|
||||
}
|
||||
|
||||
private void collectAndPlayDirectorySongs(String directoryId) {
|
||||
List<Child> allSongs = new ArrayList<>();
|
||||
AtomicInteger pendingRequests = new AtomicInteger(0);
|
||||
|
||||
collectSongsFromDirectory(directoryId, allSongs, pendingRequests, () -> {
|
||||
if (!allSongs.isEmpty()) {
|
||||
activity.runOnUiThread(() -> {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, allSongs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
Toast.makeText(requireContext(), getString(R.string.folder_play_playing, allSongs.size()), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
} else {
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(requireContext(), getString(R.string.folder_play_no_songs), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void collectSongsFromDirectory(String directoryId, List<Child> allSongs, AtomicInteger pendingRequests, Runnable onComplete) {
|
||||
pendingRequests.incrementAndGet();
|
||||
|
||||
directoryRepository.getMusicDirectory(directoryId).observe(getViewLifecycleOwner(), directory -> {
|
||||
if (directory != null && directory.getChildren() != null) {
|
||||
for (Child child : directory.getChildren()) {
|
||||
if (child.isDir()) {
|
||||
// It's a subdirectory, recurse into it
|
||||
collectSongsFromDirectory(child.getId(), allSongs, pendingRequests, onComplete);
|
||||
} else if (!child.isVideo()) {
|
||||
// It's a song, add it to the list
|
||||
synchronized (allSongs) {
|
||||
allSongs.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement pending requests and check if we're done
|
||||
if (pendingRequests.decrementAndGet() == 0) {
|
||||
onComplete.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package com.cappielloantonio.tempo.ui.fragment
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.ServiceConnection
|
||||
import android.content.BroadcastReceiver
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
@@ -12,10 +14,12 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||
import com.cappielloantonio.tempo.service.BaseMediaService
|
||||
import com.cappielloantonio.tempo.service.MediaService
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
|
||||
@@ -28,10 +32,21 @@ class EqualizerFragment : Fragment() {
|
||||
private lateinit var safeSpace: Space
|
||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||
|
||||
private var receiverRegistered = false
|
||||
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == BaseMediaService.ACTION_EQUALIZER_UPDATED) {
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as MediaService.LocalBinder
|
||||
val binder = service as BaseMediaService.LocalBinder
|
||||
equalizerManager = binder.getEqualizerManager()
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
@@ -46,15 +61,32 @@ class EqualizerFragment : Fragment() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Intent(requireContext(), MediaService::class.java).also { intent ->
|
||||
intent.action = MediaService.ACTION_BIND_EQUALIZER
|
||||
intent.action = BaseMediaService.ACTION_BIND_EQUALIZER
|
||||
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
if (!receiverRegistered) {
|
||||
ContextCompat.registerReceiver(
|
||||
requireContext(),
|
||||
equalizerUpdatedReceiver,
|
||||
IntentFilter(BaseMediaService.ACTION_EQUALIZER_UPDATED),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
receiverRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
requireActivity().unbindService(connection)
|
||||
equalizerManager = null
|
||||
if (receiverRegistered) {
|
||||
try {
|
||||
requireContext().unregisterReceiver(equalizerUpdatedReceiver)
|
||||
} catch (_: Exception) {
|
||||
// ignore if not registered
|
||||
}
|
||||
receiverRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -234,4 +266,4 @@ class EqualizerFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(context: Context): Int =
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -32,6 +33,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@@ -41,6 +43,7 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
||||
private GenreCatalogueViewModel genreCatalogueViewModel;
|
||||
|
||||
private GenreCatalogueAdapter genreCatalogueAdapter;
|
||||
private int spanCount = 2;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -56,6 +59,10 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
||||
View view = bind.getRoot();
|
||||
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
|
||||
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
spanCount = Preferences.getLandscapeItemsPerRow();
|
||||
}
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
initGenreCatalogueView();
|
||||
@@ -97,8 +104,8 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void initGenreCatalogueView() {
|
||||
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
||||
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 16, false));
|
||||
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 16, false));
|
||||
bind.genreCatalogueRecyclerView.setHasFixedSize(true);
|
||||
|
||||
genreCatalogueAdapter = new GenreCatalogueAdapter(this);
|
||||
|
||||
@@ -5,6 +5,8 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -38,10 +40,10 @@ import com.cappielloantonio.tempo.model.HomeSector;
|
||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
||||
@@ -57,6 +59,8 @@ import com.cappielloantonio.tempo.ui.dialog.HomeRearrangementDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
@@ -66,8 +70,6 @@ import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -228,6 +230,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
activity.navController.navigate(R.id.action_homeFragment_to_albumListPageFragment, bundle);
|
||||
});
|
||||
|
||||
bind.playlistCatalogueTextViewClickable.setOnClickListener(v -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.PLAYLIST_ALL, Constants.PLAYLIST_ALL);
|
||||
activity.navController.navigate(R.id.action_homeFragment_to_playlistCatalogueFragment, bundle);
|
||||
});
|
||||
|
||||
bind.recentlyPlayedAlbumsTextViewClickable.setOnClickListener(v -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(Constants.ALBUM_RECENTLY_PLAYED, Constants.ALBUM_RECENTLY_PLAYED);
|
||||
@@ -279,51 +287,113 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
|
||||
private void initSyncStarredView() {
|
||||
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||
if (Preferences.isStarredSyncEnabled()) {
|
||||
homeViewModel.getAllStarredTracks().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
List<String> toSync = new ArrayList<>();
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
int songsToSyncCount = 0;
|
||||
List<String> toSyncSample = new ArrayList<>();
|
||||
|
||||
for (Child song : songs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
toSync.add(song.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
if (!toSync.isEmpty()) {
|
||||
bind.homeSyncStarredCard.setVisibility(View.VISIBLE);
|
||||
bind.homeSyncStarredTracksToSync.setText(String.join(", ", toSync));
|
||||
}
|
||||
}
|
||||
|
||||
homeViewModel.getAllStarredTracks().removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind.homeSyncStarredCancel.setOnClickListener(v -> bind.homeSyncStarredCard.setVisibility(View.GONE));
|
||||
|
||||
bind.homeSyncStarredDownload.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null) {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
|
||||
for (Child song : songs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToSyncCount++;
|
||||
if (toSyncSample.size() < 3) {
|
||||
toSyncSample.add(song.getTitle());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Child song : songs) {
|
||||
if (ExternalAudioReader.getUri(song) == null) {
|
||||
songsToSyncCount++;
|
||||
if (toSyncSample.size() < 3) {
|
||||
toSyncSample.add(song.getTitle());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
homeViewModel.getAllStarredTracks().removeObserver(this);
|
||||
if (songsToSyncCount > 0) {
|
||||
bind.homeSyncStarredCard.setVisibility(View.VISIBLE);
|
||||
|
||||
StringBuilder displayText = new StringBuilder();
|
||||
if (!toSyncSample.isEmpty()) {
|
||||
displayText.append(String.join(", ", toSyncSample));
|
||||
if (songsToSyncCount > 3) {
|
||||
displayText.append("...");
|
||||
}
|
||||
}
|
||||
|
||||
String countText = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_songs_count,
|
||||
songsToSyncCount,
|
||||
songsToSyncCount
|
||||
);
|
||||
|
||||
if (displayText.length() > 0) {
|
||||
bind.homeSyncStarredTracksToSync.setText(displayText.toString() + "\n" + countText);
|
||||
} else {
|
||||
bind.homeSyncStarredTracksToSync.setText(countText);
|
||||
}
|
||||
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
} else {
|
||||
bind.homeSyncStarredCard.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind.homeSyncStarredCancel.setOnClickListener(v -> {
|
||||
bind.homeSyncStarredCard.setVisibility(View.GONE);
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
});
|
||||
|
||||
bind.homeSyncStarredDownload.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
homeViewModel.getAllStarredTracks().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
int downloadedCount = 0;
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
for (Child song : songs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
downloadedCount++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Child song : songs) {
|
||||
if (ExternalAudioReader.getUri(song) == null) {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
downloadedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadedCount > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, downloadedCount, downloadedCount),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
bind.homeSyncStarredCard.setVisibility(View.GONE);
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -331,6 +401,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
|
||||
private void initSyncStarredAlbumsView() {
|
||||
|
||||
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<AlbumID3>>() {
|
||||
@Override
|
||||
@@ -344,6 +415,9 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.homeSyncStarredAlbumsCancel.setOnClickListener(v -> {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
});
|
||||
|
||||
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
||||
@@ -351,24 +425,36 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Child song : allSongs) {
|
||||
if (ExternalAudioReader.getUri(song) == null) {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -379,33 +465,73 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
List<String> albumsNeedingSync = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
boolean albumNeedsSync = false;
|
||||
// Check if any songs from this album need downloading
|
||||
for (Child song : allSongs) {
|
||||
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
albumNeedsSync = true;
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
boolean albumNeedsSync = false;
|
||||
for (Child song : allSongs) {
|
||||
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
albumNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (albumNeedsSync) {
|
||||
albumsNeedingSync.add(album.getName());
|
||||
}
|
||||
}
|
||||
if (albumNeedsSync) {
|
||||
albumsNeedingSync.add(album.getName());
|
||||
} else {
|
||||
for (AlbumID3 album : albums) {
|
||||
boolean albumNeedsSync = false;
|
||||
for (Child song : allSongs) {
|
||||
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
|
||||
ExternalAudioReader.getUri(song) == null) {
|
||||
songsToDownload++;
|
||||
albumNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (albumNeedsSync) {
|
||||
albumsNeedingSync.add(album.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_albums_count,
|
||||
albumsNeedingSync.size(),
|
||||
|
||||
StringBuilder displayText = new StringBuilder();
|
||||
List<String> sampleAlbums = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < Math.min(albumsNeedingSync.size(), 3); i++) {
|
||||
sampleAlbums.add(albumsNeedingSync.get(i));
|
||||
}
|
||||
|
||||
if (!sampleAlbums.isEmpty()) {
|
||||
displayText.append(String.join(", ", sampleAlbums));
|
||||
if (albumsNeedingSync.size() > 3) {
|
||||
displayText.append("...");
|
||||
}
|
||||
}
|
||||
|
||||
String countText = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_albums_count,
|
||||
albumsNeedingSync.size(),
|
||||
albumsNeedingSync.size()
|
||||
);
|
||||
bind.homeSyncStarredAlbumsToSync.setText(message);
|
||||
|
||||
if (displayText.length() > 0) {
|
||||
bind.homeSyncStarredAlbumsToSync.setText(displayText.toString() + "\n" + countText);
|
||||
} else {
|
||||
bind.homeSyncStarredAlbumsToSync.setText(countText);
|
||||
}
|
||||
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
} else {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -428,6 +554,9 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.homeSyncStarredArtistsCancel.setOnClickListener(v -> {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
});
|
||||
|
||||
bind.homeSyncStarredArtistsDownload.setOnClickListener(v -> {
|
||||
@@ -435,24 +564,36 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Child song : allSongs) {
|
||||
if (ExternalAudioReader.getUri(song) == null) {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -463,33 +604,73 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
List<String> artistsNeedingSync = new ArrayList<>();
|
||||
|
||||
for (ArtistID3 artist : artists) {
|
||||
boolean artistNeedsSync = false;
|
||||
// Check if any songs from this artist need downloading
|
||||
for (Child song : allSongs) {
|
||||
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
artistNeedsSync = true;
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
|
||||
for (ArtistID3 artist : artists) {
|
||||
boolean artistNeedsSync = false;
|
||||
for (Child song : allSongs) {
|
||||
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
artistNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (artistNeedsSync) {
|
||||
artistsNeedingSync.add(artist.getName());
|
||||
}
|
||||
}
|
||||
if (artistNeedsSync) {
|
||||
artistsNeedingSync.add(artist.getName());
|
||||
} else {
|
||||
for (ArtistID3 artist : artists) {
|
||||
boolean artistNeedsSync = false;
|
||||
for (Child song : allSongs) {
|
||||
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
|
||||
ExternalAudioReader.getUri(song) == null) {
|
||||
songsToDownload++;
|
||||
artistNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (artistNeedsSync) {
|
||||
artistsNeedingSync.add(artist.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_artists_count,
|
||||
artistsNeedingSync.size(),
|
||||
|
||||
StringBuilder displayText = new StringBuilder();
|
||||
List<String> sampleArtists = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < Math.min(artistsNeedingSync.size(), 3); i++) {
|
||||
sampleArtists.add(artistsNeedingSync.get(i));
|
||||
}
|
||||
|
||||
if (!sampleArtists.isEmpty()) {
|
||||
displayText.append(String.join(", ", sampleArtists));
|
||||
if (artistsNeedingSync.size() > 3) {
|
||||
displayText.append("...");
|
||||
}
|
||||
}
|
||||
|
||||
String countText = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_artists_count,
|
||||
artistsNeedingSync.size(),
|
||||
artistsNeedingSync.size()
|
||||
);
|
||||
bind.homeSyncStarredArtistsToSync.setText(message);
|
||||
|
||||
if (displayText.length() > 0) {
|
||||
bind.homeSyncStarredArtistsToSync.setText(displayText.toString() + "\n" + countText);
|
||||
} else {
|
||||
bind.homeSyncStarredArtistsToSync.setText(countText);
|
||||
}
|
||||
|
||||
if (getActivity() != null) {
|
||||
getActivity().runOnUiThread(() -> reorder());
|
||||
}
|
||||
} else {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -497,7 +678,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void initDiscoverSongSlideView() {
|
||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||
|
||||
@@ -962,6 +1143,18 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
if (bind != null && homeViewModel.getHomeSectorList() != null) {
|
||||
bind.homeLinearLayoutContainer.removeAllViews();
|
||||
|
||||
if (bind.homeSyncStarredCard.getVisibility() == View.VISIBLE) {
|
||||
bind.homeLinearLayoutContainer.addView(bind.homeSyncStarredCard);
|
||||
}
|
||||
|
||||
if (bind.homeSyncStarredAlbumsCard.getVisibility() == View.VISIBLE) {
|
||||
bind.homeLinearLayoutContainer.addView(bind.homeSyncStarredAlbumsCard);
|
||||
}
|
||||
|
||||
if (bind.homeSyncStarredArtistsCard.getVisibility() == View.VISIBLE) {
|
||||
bind.homeLinearLayoutContainer.addView(bind.homeSyncStarredArtistsCard);
|
||||
}
|
||||
|
||||
for (HomeSector sector : homeViewModel.getHomeSectorList()) {
|
||||
if (!sector.isVisible()) continue;
|
||||
|
||||
@@ -1062,20 +1255,25 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaClick(Bundle bundle) {
|
||||
if (bundle.containsKey(Constants.MEDIA_MIX)) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT));
|
||||
Child track = bundle.getParcelable(Constants.TRACK_OBJECT);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
|
||||
if (mediaBrowserListenableFuture != null) {
|
||||
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> {
|
||||
MusicUtil.ratingFilter(songs);
|
||||
final boolean[] playbackStarted = {false};
|
||||
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), track)
|
||||
.observe(getViewLifecycleOwner(), songs -> {
|
||||
if (playbackStarted[0] || songs == null || songs.isEmpty()) return;
|
||||
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
|
||||
}
|
||||
});
|
||||
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||
if (playbackStarted[0]) return;
|
||||
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
playbackStarted[0] = true;
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
} else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) {
|
||||
List<Child> media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT);
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.FragmentIndexBinding;
|
||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.repository.DirectoryRepository;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.MusicIndexAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.IndexUtil;
|
||||
import com.cappielloantonio.tempo.viewmodel.IndexViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@UnstableApi
|
||||
public class IndexFragment extends Fragment implements ClickCallback {
|
||||
@@ -32,6 +45,8 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
private IndexViewModel indexViewModel;
|
||||
|
||||
private MusicIndexAdapter musicIndexAdapter;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private DirectoryRepository directoryRepository;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
@@ -40,6 +55,7 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentIndexBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
indexViewModel = new ViewModelProvider(requireActivity()).get(IndexViewModel.class);
|
||||
directoryRepository = new DirectoryRepository();
|
||||
|
||||
initAppBar();
|
||||
initDirectoryListView();
|
||||
@@ -48,6 +64,18 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
releaseMediaBrowser();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
@@ -107,4 +135,65 @@ public class IndexFragment extends Fragment implements ClickCallback {
|
||||
public void onMusicIndexClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicIndexPlay(Bundle bundle) {
|
||||
String directoryId = bundle.getString(Constants.MUSIC_DIRECTORY_ID);
|
||||
if (directoryId != null) {
|
||||
Toast.makeText(requireContext(), getString(R.string.folder_play_collecting), Toast.LENGTH_SHORT).show();
|
||||
collectAndPlayDirectorySongs(directoryId);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||
}
|
||||
|
||||
private void releaseMediaBrowser() {
|
||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
|
||||
private void collectAndPlayDirectorySongs(String directoryId) {
|
||||
List<Child> allSongs = new ArrayList<>();
|
||||
AtomicInteger pendingRequests = new AtomicInteger(0);
|
||||
|
||||
collectSongsFromDirectory(directoryId, allSongs, pendingRequests, () -> {
|
||||
if (!allSongs.isEmpty()) {
|
||||
activity.runOnUiThread(() -> {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, allSongs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
Toast.makeText(requireContext(), getString(R.string.folder_play_playing, allSongs.size()), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
} else {
|
||||
activity.runOnUiThread(() -> {
|
||||
Toast.makeText(requireContext(), getString(R.string.folder_play_no_songs), Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void collectSongsFromDirectory(String directoryId, List<Child> allSongs, AtomicInteger pendingRequests, Runnable onComplete) {
|
||||
pendingRequests.incrementAndGet();
|
||||
|
||||
directoryRepository.getMusicDirectory(directoryId).observe(getViewLifecycleOwner(), directory -> {
|
||||
if (directory != null && directory.getChildren() != null) {
|
||||
for (Child child : directory.getChildren()) {
|
||||
if (child.isDir()) {
|
||||
// It's a subdirectory, recurse into it
|
||||
collectSongsFromDirectory(child.getId(), allSongs, pendingRequests, onComplete);
|
||||
} else if (!child.isVideo()) {
|
||||
// It's a song, add it to the list
|
||||
synchronized (allSongs) {
|
||||
allSongs.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Decrement pending requests and check if we're done
|
||||
if (pendingRequests.decrementAndGet() == 0) {
|
||||
onComplete.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,11 @@ import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
@@ -31,6 +35,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.LibraryViewModel;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -49,6 +55,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
|
||||
|
||||
private MaterialToolbar materialToolbar;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -79,6 +86,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
activity.setBottomNavigationBarVisibility(true);
|
||||
}
|
||||
|
||||
@@ -292,4 +300,8 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
||||
public void onMusicFolderClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle);
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBindi
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.dialog.PlaybackSpeedDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
||||
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
||||
@@ -413,10 +414,10 @@ public class PlayerControllerFragment extends Fragment {
|
||||
bind.getRoot().setShowNextButton(true);
|
||||
bind.getRoot().setShowFastForwardButton(false);
|
||||
bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL | RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE);
|
||||
bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.GONE);
|
||||
bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.VISIBLE);
|
||||
bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.GONE);
|
||||
bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.VISIBLE);
|
||||
resetPlaybackParameters(mediaBrowser);
|
||||
setPlaybackParameters(mediaBrowser);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -522,33 +523,12 @@ public class PlayerControllerFragment extends Fragment {
|
||||
|
||||
private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) {
|
||||
playbackSpeedButton.setOnClickListener(view -> {
|
||||
float currentSpeed = Preferences.getPlaybackSpeed();
|
||||
|
||||
if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_080) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_100));
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
|
||||
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_100) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_125));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_125));
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_125);
|
||||
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_125) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_150));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_150));
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_150);
|
||||
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_150) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_175));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_175));
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_175);
|
||||
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_175) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_200));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_200));
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_200);
|
||||
} else if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_200) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_080));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_080));
|
||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_080);
|
||||
}
|
||||
PlaybackSpeedDialog dialog = new PlaybackSpeedDialog();
|
||||
dialog.setPlaybackSpeedListener(speed -> {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, speed));
|
||||
});
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
});
|
||||
|
||||
skipSilenceToggleButton.setOnClickListener(view -> {
|
||||
@@ -600,7 +580,7 @@ public class PlayerControllerFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void resetPlaybackParameters(MediaBrowser mediaBrowser) {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(1.0f));
|
||||
// TODO Resettare lo skip del silenzio
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.os.Handler;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextPaint;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -51,6 +53,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
private Runnable syncLyricsRunnable;
|
||||
private String currentLyrics;
|
||||
private LyricsList currentLyricsList;
|
||||
private Integer lastLineIdx;
|
||||
private String currentDescription;
|
||||
|
||||
@Override
|
||||
@@ -109,6 +112,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
currentLyrics = null;
|
||||
currentLyricsList = null;
|
||||
currentDescription = null;
|
||||
lastLineIdx = null;
|
||||
}
|
||||
|
||||
private void initOverlay() {
|
||||
@@ -162,6 +166,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
currentLyricsList = lyricsList;
|
||||
lastLineIdx = null;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
@@ -194,7 +199,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||
|
||||
if (hasStructuredLyrics(currentLyricsList)) {
|
||||
setSyncLirics(currentLyricsList);
|
||||
setSyncLyrics(currentLyricsList);
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
@@ -241,14 +246,14 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void setSyncLirics(LyricsList lyricsList) {
|
||||
private void setSyncLyrics(LyricsList lyricsList) {
|
||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||
StringBuilder lyricsBuilder = new StringBuilder();
|
||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||
|
||||
if (lines != null) {
|
||||
for (Line line : lines) {
|
||||
lyricsBuilder.append(line.getValue().trim()).append("\n");
|
||||
lyricsBuilder.append(line.getValue().trim()).append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,67 +293,75 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||
|
||||
if (hasStructuredLyrics(lyricsList)) {
|
||||
StringBuilder lyricsBuilder = new StringBuilder();
|
||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||
|
||||
if (lines == null || lines.isEmpty()) return;
|
||||
|
||||
for (Line line : lines) {
|
||||
lyricsBuilder.append(line.getValue().trim()).append("\n");
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null);
|
||||
|
||||
if (toHighlight != null) {
|
||||
String lyrics = lyricsBuilder.toString();
|
||||
Spannable spannableString = new SpannableString(lyrics);
|
||||
|
||||
int startingPosition = getStartPosition(lines, toHighlight);
|
||||
int endingPosition = startingPosition + toHighlight.getValue().length();
|
||||
|
||||
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null)), 0, lyrics.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannableString.setSpan(new ForegroundColorSpan(requireContext().getResources().getColor(R.color.lyricsTextColor, null)), startingPosition, endingPosition, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
bind.nowPlayingSongLyricsTextView.setText(spannableString);
|
||||
|
||||
if (playerBottomSheetViewModel.getSyncLyricsState()) {
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(lines, toHighlight));
|
||||
// Find the index of the currently playing line
|
||||
int curIdx = 0;
|
||||
for (; curIdx < lines.size(); ++curIdx) {
|
||||
Integer start = lines.get(curIdx).getStart();
|
||||
if (start != null && start > timestamp) {
|
||||
curIdx--; // Found the first line that starts after the current timestamp
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getStartPosition(List<Line> lines, Line toHighlight) {
|
||||
int start = 0;
|
||||
// Only update if the highlighted line has changed
|
||||
if (lastLineIdx != null && curIdx == lastLineIdx) {
|
||||
return;
|
||||
}
|
||||
lastLineIdx = curIdx;
|
||||
|
||||
for (Line line : lines) {
|
||||
if (line != toHighlight) {
|
||||
start = start + line.getValue().length() + 1;
|
||||
} else {
|
||||
break;
|
||||
StringBuilder lyricsBuilder = new StringBuilder();
|
||||
for (Line line : lines) {
|
||||
lyricsBuilder.append(line.getValue().trim()).append("\n\n");
|
||||
}
|
||||
String lyrics = lyricsBuilder.toString();
|
||||
Spannable spannableString = new SpannableString(lyrics);
|
||||
|
||||
// Make each line clickable for navigation and highlight the current one
|
||||
int offset = 0;
|
||||
int highlightStart = -1;
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
boolean highlight = i == curIdx;
|
||||
if (highlight) highlightStart = offset;
|
||||
|
||||
int len = lines.get(i).getValue().length() + 2;
|
||||
final int lineStart = lines.get(i).getStart();
|
||||
spannableString.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View view) {
|
||||
// Seeking to 1ms after the actual start prevents scrolling / highlighting artifacts
|
||||
mediaBrowser.seekTo(lineStart + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
ds.setUnderlineText(false);
|
||||
if (highlight) {
|
||||
ds.setColor(requireContext().getResources().getColor(R.color.lyricsTextColor, null));
|
||||
} else {
|
||||
ds.setColor(requireContext().getResources().getColor(R.color.shadowsLyricsTextColor, null));
|
||||
}
|
||||
}
|
||||
}, offset, offset + len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
offset += len;
|
||||
}
|
||||
|
||||
bind.nowPlayingSongLyricsTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
bind.nowPlayingSongLyricsTextView.setText(spannableString);
|
||||
|
||||
// Scroll to the highlighted line, but only if there is one
|
||||
if (highlightStart >= 0 && playerBottomSheetViewModel.getSyncLyricsState()) {
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, getScroll(highlightStart));
|
||||
}
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
private int getLineCount(List<Line> lines, Line toHighlight) {
|
||||
int start = 0;
|
||||
|
||||
for (Line line : lines) {
|
||||
if (line != toHighlight) {
|
||||
bind.tempLyricsLineTextView.setText(line.getValue());
|
||||
start = start + bind.tempLyricsLineTextView.getLineCount();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
private int getScroll(List<Line> lines, Line toHighlight) {
|
||||
int startIndex = getStartPosition(lines, toHighlight);
|
||||
private int getScroll(int startIndex) {
|
||||
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
||||
if (layout == null) return 0;
|
||||
|
||||
|
||||
@@ -2,27 +2,41 @@ package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding;
|
||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
|
||||
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
@@ -30,6 +44,7 @@ import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@UnstableApi
|
||||
@@ -38,6 +53,18 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
private InnerFragmentPlayerQueueBinding bind;
|
||||
|
||||
private com.google.android.material.floatingactionbutton.FloatingActionButton fabMenuToggle;
|
||||
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabClearQueue;
|
||||
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabShuffleQueue;
|
||||
|
||||
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabSaveToPlaylist;
|
||||
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabDownloadAll;
|
||||
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabLoadQueue;
|
||||
|
||||
private boolean isMenuOpen = false;
|
||||
private final int ANIMATION_DURATION = 250;
|
||||
private final float FAB_VERTICAL_SPACING_DP = 70f;
|
||||
|
||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
@@ -52,6 +79,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
fabMenuToggle = bind.fabMenuToggle;
|
||||
fabClearQueue = bind.fabClearQueue;
|
||||
fabShuffleQueue = bind.fabShuffleQueue;
|
||||
|
||||
fabSaveToPlaylist = bind.fabSaveToPlaylist;
|
||||
fabDownloadAll = bind.fabDownloadAll;
|
||||
fabLoadQueue = bind.fabLoadQueue;
|
||||
|
||||
fabMenuToggle.setOnClickListener(v -> toggleFabMenu());
|
||||
fabClearQueue.setOnClickListener(v -> handleClearQueueClick());
|
||||
fabShuffleQueue.setOnClickListener(v -> handleShuffleQueueClick());
|
||||
|
||||
fabSaveToPlaylist.setOnClickListener(v -> handleSaveToPlaylistClick());
|
||||
fabDownloadAll.setOnClickListener(v -> handleDownloadAllClick());
|
||||
fabLoadQueue.setOnClickListener(v -> handleLoadQueueClick());
|
||||
|
||||
// Hide Load Queue FAB if sync is disabled
|
||||
if (!Preferences.isSyncronizationEnabled()) {
|
||||
fabLoadQueue.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
initQueueRecyclerView();
|
||||
|
||||
return view;
|
||||
@@ -61,8 +109,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeBrowser();
|
||||
bindMediaController();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
@@ -72,6 +118,16 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
super.onResume();
|
||||
setMediaBrowserListenableFuture();
|
||||
updateNowPlayingItem();
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
long position = mediaBrowserListenableFuture.get().getCurrentMediaItemIndex();
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
bind.playerQueueRecyclerView.scrollToPosition((int) position);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e("PlayerQueueFragment", "Failed to get mediaBrowserListenableFuture in onResume", e);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,18 +150,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
|
||||
private void bindMediaController() {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
initShuffleButton(mediaBrowser);
|
||||
initCleanButton(mediaBrowser);
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
@@ -138,18 +182,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
fromPosition = viewHolder.getBindingAdapterPosition();
|
||||
toPosition = target.getBindingAdapterPosition();
|
||||
|
||||
/*
|
||||
* Per spostare un elemento nella coda devo:
|
||||
* - Spostare graficamente la traccia da una posizione all'altra con Collections.swap()
|
||||
* - Spostare nel db la traccia, tramite QueueRepository
|
||||
* - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong()
|
||||
*
|
||||
* In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento
|
||||
* In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db
|
||||
* Al rilascio dell'elemento chiamo il metodo clearView()
|
||||
*/
|
||||
|
||||
Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition);
|
||||
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
|
||||
|
||||
@@ -177,46 +209,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
}).attachToRecyclerView(bind.playerQueueRecyclerView);
|
||||
}
|
||||
|
||||
private void initShuffleButton(MediaBrowser mediaBrowser) {
|
||||
bind.playerShuffleQueueFab.setOnClickListener(view -> {
|
||||
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
||||
int endPosition = playerSongQueueAdapter.getItems().size() - 1;
|
||||
|
||||
if (startPosition < endPosition) {
|
||||
ArrayList<Integer> pool = new ArrayList<>();
|
||||
|
||||
for (int i = startPosition; i <= endPosition; i++) {
|
||||
pool.add(i);
|
||||
}
|
||||
|
||||
while (pool.size() >= 2) {
|
||||
int fromPosition = (int) (Math.random() * (pool.size()));
|
||||
int positionA = pool.get(fromPosition);
|
||||
pool.remove(fromPosition);
|
||||
|
||||
int toPosition = (int) (Math.random() * (pool.size()));
|
||||
int positionB = pool.get(toPosition);
|
||||
pool.remove(toPosition);
|
||||
|
||||
Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB);
|
||||
bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB);
|
||||
}
|
||||
|
||||
MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initCleanButton(MediaBrowser mediaBrowser) {
|
||||
bind.playerCleanQueueButton.setOnClickListener(view -> {
|
||||
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
||||
int endPosition = playerSongQueueAdapter.getItems().size();
|
||||
|
||||
MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
||||
bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateNowPlayingItem() {
|
||||
playerSongQueueAdapter.notifyDataSetChanged();
|
||||
}
|
||||
@@ -248,4 +240,250 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the visibility and animates all six secondary FABs.
|
||||
*/
|
||||
private void toggleFabMenu() {
|
||||
if (isMenuOpen) {
|
||||
// CLOSE MENU (Reverse order for visual effect)
|
||||
if (Preferences.isSyncronizationEnabled()) {
|
||||
closeFab(fabLoadQueue, 4);
|
||||
}
|
||||
closeFab(fabSaveToPlaylist, 3);
|
||||
closeFab(fabClearQueue, 2);
|
||||
closeFab(fabDownloadAll, 1);
|
||||
closeFab(fabShuffleQueue, 0);
|
||||
|
||||
fabMenuToggle.animate().rotation(0f).setDuration(ANIMATION_DURATION).start();
|
||||
} else {
|
||||
// OPEN MENU (lowest index at bottom)
|
||||
openFab(fabShuffleQueue, 0);
|
||||
openFab(fabDownloadAll, 1);
|
||||
openFab(fabClearQueue, 2);
|
||||
openFab(fabSaveToPlaylist, 3);
|
||||
if (Preferences.isSyncronizationEnabled()) {
|
||||
openFab(fabLoadQueue, 4);
|
||||
}
|
||||
fabMenuToggle.animate().rotation(45f).setDuration(ANIMATION_DURATION).start();
|
||||
}
|
||||
isMenuOpen = !isMenuOpen;
|
||||
}
|
||||
|
||||
private void openFab(View fab, int index) {
|
||||
final float displacement = getResources().getDisplayMetrics().density * (FAB_VERTICAL_SPACING_DP * (index + 1));
|
||||
|
||||
fab.setVisibility(View.VISIBLE);
|
||||
fab.setAlpha(0f);
|
||||
fab.setTranslationY(displacement); // Start at the hidden (closed) position
|
||||
|
||||
fab.animate()
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.start();
|
||||
}
|
||||
|
||||
private void closeFab(View fab, int index) {
|
||||
final float displacement = getResources().getDisplayMetrics().density * (FAB_VERTICAL_SPACING_DP * (index + 1));
|
||||
|
||||
fab.animate()
|
||||
.translationY(displacement)
|
||||
.alpha(0f)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.withEndAction(() -> fab.setVisibility(View.GONE))
|
||||
.start();
|
||||
}
|
||||
|
||||
private void handleShuffleQueueClick() {
|
||||
Log.d(TAG, "Shuffle Queue Clicked!");
|
||||
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
||||
int endPosition = playerSongQueueAdapter.getItems().size() - 1;
|
||||
|
||||
if (startPosition < endPosition) {
|
||||
ArrayList<Integer> pool = new ArrayList<>();
|
||||
|
||||
for (int i = startPosition; i <= endPosition; i++) {
|
||||
pool.add(i);
|
||||
}
|
||||
|
||||
while (pool.size() >= 2) {
|
||||
int fromPosition = (int) (Math.random() * (pool.size()));
|
||||
int positionA = pool.get(fromPosition);
|
||||
pool.remove(fromPosition);
|
||||
|
||||
int toPosition = (int) (Math.random() * (pool.size()));
|
||||
int positionB = pool.get(toPosition);
|
||||
pool.remove(toPosition);
|
||||
|
||||
Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB);
|
||||
bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB);
|
||||
}
|
||||
|
||||
MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error shuffling queue", e);
|
||||
}
|
||||
|
||||
toggleFabMenu();
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private void handleClearQueueClick() {
|
||||
Log.d(TAG, "Clear Queue Clicked!");
|
||||
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
|
||||
int endPosition = playerSongQueueAdapter.getItems().size();
|
||||
|
||||
MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
|
||||
bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition - startPosition);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error clearing queue", e);
|
||||
}
|
||||
|
||||
toggleFabMenu();
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
private void handleSaveToPlaylistClick() {
|
||||
Log.d(TAG, "Save to Playlist Clicked!");
|
||||
|
||||
List<Child> queueSongs = playerSongQueueAdapter.getItems();
|
||||
|
||||
if (queueSongs == null || queueSongs.isEmpty()) {
|
||||
Toast.makeText(requireContext(), "Queue is empty", Toast.LENGTH_SHORT).show();
|
||||
toggleFabMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(queueSongs));
|
||||
|
||||
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
|
||||
toggleFabMenu();
|
||||
}
|
||||
|
||||
private void handleDownloadAllClick() {
|
||||
Log.d(TAG, "Download All Clicked!");
|
||||
|
||||
List<Child> queueSongs = playerSongQueueAdapter.getItems();
|
||||
|
||||
if (queueSongs == null || queueSongs.isEmpty()) {
|
||||
Toast.makeText(requireContext(), "Queue is empty", Toast.LENGTH_SHORT).show();
|
||||
toggleFabMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
int downloadCount = 0;
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<MediaItem> mediaItemsToDownload = MappingUtil.mapMediaItems(queueSongs);
|
||||
List<com.cappielloantonio.tempo.model.Download> downloadModels = new ArrayList<>();
|
||||
|
||||
for (Child child : queueSongs) {
|
||||
com.cappielloantonio.tempo.model.Download downloadModel =
|
||||
new com.cappielloantonio.tempo.model.Download(child);
|
||||
downloadModel.setArtist(child.getArtist());
|
||||
downloadModel.setAlbum(child.getAlbum());
|
||||
downloadModel.setCoverArtId(child.getCoverArtId());
|
||||
downloadModels.add(downloadModel);
|
||||
}
|
||||
|
||||
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
|
||||
if (downloaderManager != null) {
|
||||
downloaderManager.download(mediaItemsToDownload, downloadModels);
|
||||
downloadCount = queueSongs.size();
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, downloadCount, downloadCount),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
new Handler().postDelayed(() -> {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
playerSongQueueAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
Log.e(TAG, "DownloaderManager not initialized. Check DownloadUtil.");
|
||||
Toast.makeText(requireContext(), "Download service unavailable.", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
for (Child song : queueSongs) {
|
||||
if (ExternalAudioReader.getUri(song) == null) {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
downloadCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadCount > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, downloadCount, downloadCount),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
new Handler().postDelayed(() -> {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
playerSongQueueAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "All songs already downloaded", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
toggleFabMenu();
|
||||
}
|
||||
|
||||
private void handleLoadQueueClick() {
|
||||
Log.d(TAG, "Load Queue Clicked!");
|
||||
if (!Preferences.isSyncronizationEnabled()) {
|
||||
toggleFabMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerBottomSheetViewModel playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||
|
||||
playerBottomSheetViewModel.getPlayQueue().observe(getViewLifecycleOwner(), new Observer<PlayQueue>() {
|
||||
@Override
|
||||
public void onChanged(PlayQueue playQueue) {
|
||||
playerBottomSheetViewModel.getPlayQueue().removeObserver(this);
|
||||
|
||||
if (playQueue != null && playQueue.getEntries() != null && !playQueue.getEntries().isEmpty()) {
|
||||
int currentIndex = 0;
|
||||
for (int i = 0; i < playQueue.getEntries().size(); i++) {
|
||||
if (playQueue.getEntries().get(i).getId().equals(playQueue.getCurrent())) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, playQueue.getEntries(), currentIndex);
|
||||
|
||||
Toast.makeText(requireContext(), "Queue loaded", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "No saved queue found", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
toggleFabMenu();
|
||||
}
|
||||
});
|
||||
|
||||
new Handler().postDelayed(() -> {
|
||||
if (isMenuOpen) {
|
||||
toggleFabMenu();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
@@ -156,10 +156,10 @@ public class PlaylistCatalogueFragment extends Fragment implements ClickCallback
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
if (menuItem.getItemId() == R.id.menu_playlist_sort_name) {
|
||||
playlistHorizontalAdapter.sort(Constants.GENRE_ORDER_BY_NAME);
|
||||
playlistHorizontalAdapter.sort(Constants.PLAYLIST_ORDER_BY_NAME);
|
||||
return true;
|
||||
} else if (menuItem.getItemId() == R.id.menu_playlist_sort_random) {
|
||||
playlistHorizontalAdapter.sort(Constants.GENRE_ORDER_BY_RANDOM);
|
||||
playlistHorizontalAdapter.sort(Constants.PLAYLIST_ORDER_BY_RANDOM);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import android.media.audiofx.AudioEffect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.InputFilter;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -27,8 +29,10 @@ import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavOptions;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.SwitchPreference;
|
||||
|
||||
@@ -77,6 +81,13 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
result -> {}
|
||||
);
|
||||
|
||||
if (!BuildConfig.FLAVOR.equals("tempus")) {
|
||||
PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key");
|
||||
if (githubUpdateCategory != null) {
|
||||
getPreferenceScreen().removePreference(githubUpdateCategory);
|
||||
}
|
||||
}
|
||||
|
||||
directoryPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
@@ -133,6 +144,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
setStreamingCacheSize();
|
||||
setAppLanguage();
|
||||
setVersion();
|
||||
setNetorkPingTimeoutBase();
|
||||
|
||||
actionLogout();
|
||||
actionScan();
|
||||
@@ -253,6 +265,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
}
|
||||
|
||||
private void setNetorkPingTimeoutBase() {
|
||||
EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base");
|
||||
|
||||
if (networkPingTimeoutBase != null) {
|
||||
networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance());
|
||||
networkPingTimeoutBase.setOnBindEditTextListener(editText -> {
|
||||
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> {
|
||||
for (int i = start; i < end; i++) {
|
||||
if (!Character.isDigit(source.charAt(i))) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}});
|
||||
});
|
||||
|
||||
networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
String input = (String) newValue;
|
||||
return input != null && !input.isEmpty();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setStreamingCacheSize() {
|
||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.songListShuffleImageView.setOnClickListener(v -> {
|
||||
Collections.shuffle(songs);
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(25, songs.size())), 0);
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(500, songs.size())), 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
@@ -43,7 +42,6 @@ import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -61,7 +59,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||
|
||||
private boolean isFirstBatch = true;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private static final String TAG = "AlbumBottomSheetDialog";
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -114,33 +115,41 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
|
||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
albumBottomSheetViewModel.setFavorite(requireContext());
|
||||
});
|
||||
favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext()));
|
||||
|
||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
playRadio.setOnClickListener(v -> {
|
||||
AlbumRepository albumRepository = new AlbumRepository();
|
||||
albumRepository.getInstantMix(album, 20, new MediaCallback() {
|
||||
@Override
|
||||
public void onError(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
if (activity == null) return;
|
||||
|
||||
@Override
|
||||
public void onLoadMedia(List<?> media) {
|
||||
MusicUtil.ratingFilter((ArrayList<Child>) media);
|
||||
ListenableFuture<MediaBrowser> activityBrowserFuture = activity.getMediaBrowserListenableFuture();
|
||||
if (activityBrowserFuture == null) return;
|
||||
|
||||
if (!media.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
|
||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||
isFirstBatch = true;
|
||||
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||
|
||||
albumBottomSheetViewModel.getAlbumInstantMix(activity, album).observe(activity, media -> {
|
||||
if (media == null || media.isEmpty()) return;
|
||||
if (getActivity() == null) return;
|
||||
|
||||
MusicUtil.ratingFilter(media);
|
||||
|
||||
if (isFirstBatch) {
|
||||
isFirstBatch = false;
|
||||
|
||||
MediaManager.startQueue(activityBrowserFuture, media, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
|
||||
if (isAdded()) {
|
||||
dismissBottomSheet();
|
||||
}
|
||||
|
||||
dismissBottomSheet();
|
||||
} else {
|
||||
MediaManager.enqueue(activityBrowserFuture, media, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
TextView playRandom = view.findViewById(R.id.play_random_text_view);
|
||||
playRandom.setOnClickListener(v -> {
|
||||
AlbumRepository albumRepository = new AlbumRepository();
|
||||
@@ -186,18 +195,16 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
});
|
||||
|
||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||
addToPlaylist.setOnClickListener(v -> {
|
||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
|
||||
addToPlaylist.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
|
||||
|
||||
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
|
||||
dismissBottomSheet();
|
||||
});
|
||||
});
|
||||
dismissBottomSheet();
|
||||
}));
|
||||
|
||||
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
@@ -291,4 +298,5 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private void refreshShares() {
|
||||
homeViewModel.refreshShares(requireActivity());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
||||
@UnstableApi
|
||||
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||
private static final String TAG = "AlbumBottomSheetDialog";
|
||||
@@ -38,6 +39,8 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private boolean isFirstBatch = true;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
@@ -86,20 +89,31 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
|
||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
playRadio.setOnClickListener(v -> {
|
||||
ArtistRepository artistRepository = new ArtistRepository();
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
if (activity == null) return;
|
||||
|
||||
artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
|
||||
// navidrome may return null for this
|
||||
if (songs == null)
|
||||
return;
|
||||
MusicUtil.ratingFilter(songs);
|
||||
ListenableFuture<MediaBrowser> activityBrowserFuture = activity.getMediaBrowserListenableFuture();
|
||||
if (activityBrowserFuture == null) return;
|
||||
|
||||
if (!songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||
isFirstBatch = true;
|
||||
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||
|
||||
artistBottomSheetViewModel.getArtistInstantMix(activity, artist).observe(activity, media -> {
|
||||
if (media == null || media.isEmpty()) return;
|
||||
if (getActivity() == null) return;
|
||||
|
||||
MusicUtil.ratingFilter(media);
|
||||
|
||||
if (isFirstBatch) {
|
||||
isFirstBatch = false;
|
||||
MediaManager.startQueue(activityBrowserFuture, media, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
if (isAdded()) {
|
||||
dismissBottomSheet();
|
||||
}
|
||||
} else {
|
||||
MediaManager.enqueue(activityBrowserFuture, media, true);
|
||||
}
|
||||
|
||||
dismissBottomSheet();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,16 +122,10 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
ArtistRepository artistRepository = new ArtistRepository();
|
||||
artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> {
|
||||
MusicUtil.ratingFilter(songs);
|
||||
|
||||
if (!songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||
|
||||
dismissBottomSheet();
|
||||
} else {
|
||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
dismissBottomSheet();
|
||||
});
|
||||
});
|
||||
@@ -139,4 +147,5 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
private void releaseMediaBrowser() {
|
||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.media3.common.util.UnstableApi;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.dialog.ShareUpdateDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.UIUtil;
|
||||
@@ -24,6 +25,8 @@ import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.ShareBottomSheetViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@UnstableApi
|
||||
public class ShareBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||
|
||||
@@ -50,8 +53,15 @@ public class ShareBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private void init(View view) {
|
||||
ImageView shareCover = view.findViewById(R.id.share_cover_image_view);
|
||||
|
||||
String coverArtId = null;
|
||||
List<Child> entries = shareBottomSheetViewModel.getShare().getEntries();
|
||||
|
||||
if (entries != null && !entries.isEmpty()) {
|
||||
coverArtId = entries.get(0).getCoverArtId();
|
||||
}
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), shareBottomSheetViewModel.getShare().getEntries().get(0).getCoverArtId(), CustomGlideRequest.ResourceType.Unknown)
|
||||
.from(requireContext(), coverArtId, CustomGlideRequest.ResourceType.Unknown)
|
||||
.build()
|
||||
.into(shareCover);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.ClipboardManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -44,8 +45,6 @@ import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -67,7 +66,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private AssetLinkUtil.AssetLink currentAlbumLink;
|
||||
private AssetLinkUtil.AssetLink currentArtistLink;
|
||||
|
||||
private boolean isFirstBatch = true;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private static final String TAG = "SongBottomSheetDialog";
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
@@ -143,20 +144,34 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
|
||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
playRadio.setOnClickListener(v -> {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, song);
|
||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
if (activity == null) return;
|
||||
|
||||
songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
|
||||
MusicUtil.ratingFilter(songs);
|
||||
ListenableFuture<MediaBrowser> activityBrowserFuture = activity.getMediaBrowserListenableFuture();
|
||||
if (activityBrowserFuture == null) {
|
||||
Log.e(TAG, "MediaBrowser Future is null in MainActivity");
|
||||
return;
|
||||
}
|
||||
|
||||
if (songs == null) {
|
||||
dismissBottomSheet();
|
||||
return;
|
||||
}
|
||||
isFirstBatch = true;
|
||||
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||
|
||||
if (!songs.isEmpty()) {
|
||||
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
|
||||
dismissBottomSheet();
|
||||
songBottomSheetViewModel.getInstantMix(activity, song).observe(activity, media -> {
|
||||
|
||||
if (media == null || media.isEmpty()) return;
|
||||
if (getActivity() == null) return;
|
||||
|
||||
MusicUtil.ratingFilter(media);
|
||||
|
||||
if (isFirstBatch) {
|
||||
isFirstBatch = false;
|
||||
MediaManager.startQueue(activityBrowserFuture, media, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
if (isAdded()) {
|
||||
dismissBottomSheet();
|
||||
}
|
||||
} else {
|
||||
MediaManager.enqueue(activityBrowserFuture, media, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -327,16 +342,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
chip.setVisibility(View.VISIBLE);
|
||||
|
||||
chip.setOnClickListener(v -> {
|
||||
if (assetLink != null) {
|
||||
((MainActivity) requireActivity()).openAssetLink(assetLink);
|
||||
}
|
||||
((MainActivity) requireActivity()).openAssetLink(assetLink);
|
||||
});
|
||||
|
||||
chip.setOnLongClickListener(v -> {
|
||||
if (assetLink != null) {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -397,4 +408,5 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private void refreshShares() {
|
||||
homeViewModel.refreshShares(requireActivity());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,13 +61,6 @@ object Constants {
|
||||
const val MEDIA_TYPE_VIDEO = "video"
|
||||
const val MEDIA_TYPE_RADIO = "radio"
|
||||
|
||||
const val MEDIA_PLAYBACK_SPEED_080 = 0.8f
|
||||
const val MEDIA_PLAYBACK_SPEED_100 = 1.0f
|
||||
const val MEDIA_PLAYBACK_SPEED_125 = 1.25f
|
||||
const val MEDIA_PLAYBACK_SPEED_150 = 1.50f
|
||||
const val MEDIA_PLAYBACK_SPEED_175 = 1.75f
|
||||
const val MEDIA_PLAYBACK_SPEED_200 = 2.0f
|
||||
|
||||
const val MEDIA_RECENTLY_PLAYED = "MEDIA_RECENTLY_PLAYED"
|
||||
const val MEDIA_MOST_PLAYED = "MEDIA_MOST_PLAYED"
|
||||
const val MEDIA_RECENTLY_ADDED = "MEDIA_RECENTLY_ADDED"
|
||||
@@ -133,4 +126,7 @@ object Constants {
|
||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF"
|
||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE"
|
||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
||||
enum class SeedType {
|
||||
ARTIST, ALBUM, TRACK
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,16 @@ class DynamicMediaSourceFactory(
|
||||
|
||||
else -> {
|
||||
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
|
||||
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||
.createMediaSource(mediaItem)
|
||||
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||
|
||||
val uri = mediaItem.localConfiguration?.uri
|
||||
val isTranscoding = uri?.getQueryParameter("format") != null && uri.getQueryParameter("format") != "raw"
|
||||
|
||||
if (isTranscoding && OpenSubsonicExtensionsUtil.isTranscodeOffsetExtensionAvailable()) {
|
||||
TranscodingMediaSource(mediaItem, dataSourceFactory, progressiveFactory)
|
||||
} else {
|
||||
progressiveFactory.createMediaSource(mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
@@ -14,6 +16,7 @@ import androidx.media3.common.HeartRating;
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
|
||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||
@@ -35,84 +38,106 @@ public class MappingUtil {
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
private static final String TAG = "MappingUtil";
|
||||
|
||||
public static MediaItem mapMediaItem(Child media) {
|
||||
Uri uri = getUri(media);
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(media.getCoverArtId(), Preferences.getImageSize()));
|
||||
try {
|
||||
Uri uri = getUri(media);
|
||||
String coverArtId = media.getCoverArtId();
|
||||
Uri artworkUri = null;
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", media.getId());
|
||||
bundle.putString("parentId", media.getParentId());
|
||||
bundle.putBoolean("isDir", media.isDir());
|
||||
bundle.putString("title", media.getTitle());
|
||||
bundle.putString("album", media.getAlbum());
|
||||
bundle.putString("artist", media.getArtist());
|
||||
bundle.putInt("track", media.getTrack() != null ? media.getTrack() : 0);
|
||||
bundle.putInt("year", media.getYear() != null ? media.getYear() : 0);
|
||||
bundle.putString("genre", media.getGenre());
|
||||
bundle.putString("coverArtId", media.getCoverArtId());
|
||||
bundle.putLong("size", media.getSize() != null ? media.getSize() : 0);
|
||||
bundle.putString("contentType", media.getContentType());
|
||||
bundle.putString("suffix", media.getSuffix());
|
||||
bundle.putString("transcodedContentType", media.getTranscodedContentType());
|
||||
bundle.putString("transcodedSuffix", media.getTranscodedSuffix());
|
||||
bundle.putInt("duration", media.getDuration() != null ? media.getDuration() : 0);
|
||||
bundle.putInt("bitrate", media.getBitrate() != null ? media.getBitrate() : 0);
|
||||
bundle.putInt("samplingRate", media.getSamplingRate() != null ? media.getSamplingRate() : 0);
|
||||
bundle.putInt("bitDepth", media.getBitDepth() != null ? media.getBitDepth() : 0);
|
||||
bundle.putString("path", media.getPath());
|
||||
bundle.putBoolean("isVideo", media.isVideo());
|
||||
bundle.putInt("userRating", media.getUserRating() != null ? media.getUserRating() : 0);
|
||||
bundle.putDouble("averageRating", media.getAverageRating() != null ? media.getAverageRating() : 0);
|
||||
bundle.putLong("playCount", media.getPlayCount() != null ? media.getPlayCount() : 0);
|
||||
bundle.putInt("discNumber", media.getDiscNumber() != null ? media.getDiscNumber() : 0);
|
||||
bundle.putLong("created", media.getCreated() != null ? media.getCreated().getTime() : 0);
|
||||
bundle.putLong("starred", media.getStarred() != null ? media.getStarred().getTime() : 0);
|
||||
bundle.putString("albumId", media.getAlbumId());
|
||||
bundle.putString("artistId", media.getArtistId());
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC);
|
||||
bundle.putLong("bookmarkPosition", media.getBookmarkPosition() != null ? media.getBookmarkPosition() : 0);
|
||||
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
||||
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
||||
bundle.putString("uri", uri.toString());
|
||||
bundle.putString("assetLinkSong", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId()));
|
||||
bundle.putString("assetLinkAlbum", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId()));
|
||||
bundle.putString("assetLinkArtist", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId()));
|
||||
bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre()));
|
||||
Integer year = media.getYear();
|
||||
bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null);
|
||||
if (coverArtId != null) {
|
||||
artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
|
||||
}
|
||||
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(media.getId())
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(media.getTitle())
|
||||
.setTrackNumber(media.getTrack() != null ? media.getTrack() : 0)
|
||||
.setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0)
|
||||
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
|
||||
.setAlbumTitle(media.getAlbum())
|
||||
.setArtist(media.getArtist())
|
||||
.setArtworkUri(artworkUri)
|
||||
.setUserRating(new HeartRating(media.getStarred() != null))
|
||||
.setSupportedCommands(
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", media.getId());
|
||||
bundle.putString("parentId", media.getParentId());
|
||||
bundle.putBoolean("isDir", media.isDir());
|
||||
|
||||
bundle.putString("title", media.getTitle());
|
||||
bundle.putString("album", media.getAlbum());
|
||||
bundle.putString("artist", media.getArtist());
|
||||
|
||||
bundle.putInt("track", media.getTrack() != null ? media.getTrack() : 0);
|
||||
bundle.putInt("year", media.getYear() != null ? media.getYear() : 0);
|
||||
bundle.putString("genre", media.getGenre());
|
||||
bundle.putString("coverArtId", coverArtId);
|
||||
bundle.putLong("size", media.getSize() != null ? media.getSize() : 0);
|
||||
bundle.putString("contentType", media.getContentType());
|
||||
bundle.putString("suffix", media.getSuffix());
|
||||
bundle.putString("transcodedContentType", media.getTranscodedContentType());
|
||||
bundle.putString("transcodedSuffix", media.getTranscodedSuffix());
|
||||
bundle.putInt("duration", media.getDuration() != null ? media.getDuration() : 0);
|
||||
bundle.putInt("bitrate", media.getBitrate() != null ? media.getBitrate() : 0);
|
||||
bundle.putInt("samplingRate", media.getSamplingRate() != null ? media.getSamplingRate() : 0);
|
||||
bundle.putInt("bitDepth", media.getBitDepth() != null ? media.getBitDepth() : 0);
|
||||
bundle.putString("path", media.getPath());
|
||||
bundle.putBoolean("isVideo", media.isVideo());
|
||||
bundle.putInt("userRating", media.getUserRating() != null ? media.getUserRating() : 0);
|
||||
bundle.putDouble("averageRating", media.getAverageRating() != null ? media.getAverageRating() : 0);
|
||||
bundle.putLong("playCount", media.getPlayCount() != null ? media.getPlayCount() : 0);
|
||||
bundle.putInt("discNumber", media.getDiscNumber() != null ? media.getDiscNumber() : 0);
|
||||
bundle.putLong("created", media.getCreated() != null ? media.getCreated().getTime() : 0);
|
||||
bundle.putLong("starred", media.getStarred() != null ? media.getStarred().getTime() : 0);
|
||||
bundle.putString("albumId", media.getAlbumId());
|
||||
bundle.putString("artistId", media.getArtistId());
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC);
|
||||
bundle.putLong("bookmarkPosition", media.getBookmarkPosition() != null ? media.getBookmarkPosition() : 0);
|
||||
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
||||
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
||||
bundle.putString("uri", uri.toString());
|
||||
|
||||
bundle.putString("assetLinkSong", media.getId() != null ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId()) : null);
|
||||
bundle.putString("assetLinkAlbum", media.getAlbumId() != null ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId()) : null);
|
||||
bundle.putString("assetLinkArtist", media.getArtistId() != null ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId()) : null);
|
||||
bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre()));
|
||||
Integer year = media.getYear();
|
||||
bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null);
|
||||
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(media.getId())
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(media.getTitle())
|
||||
.setTrackNumber(media.getTrack() != null ? media.getTrack() : 0)
|
||||
.setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0)
|
||||
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
|
||||
.setAlbumTitle(media.getAlbum())
|
||||
.setArtist(media.getArtist())
|
||||
.setArtworkUri(artworkUri)
|
||||
.setUserRating(new HeartRating(media.getStarred() != null))
|
||||
.setSupportedCommands(
|
||||
ImmutableList.of(
|
||||
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||
)
|
||||
)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
.build()
|
||||
)
|
||||
.setRequestMetadata(
|
||||
new MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setExtras(bundle)
|
||||
.build()
|
||||
)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.setUri(uri)
|
||||
.build();
|
||||
)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
.build()
|
||||
)
|
||||
.setRequestMetadata(
|
||||
new MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setExtras(bundle)
|
||||
.build()
|
||||
)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.setUri(uri)
|
||||
.build();
|
||||
|
||||
} catch (Exception e) {
|
||||
String id = media != null ? media.getId() : "NULL_MEDIA_OBJECT";
|
||||
String title = media != null ? media.getTitle() : "N/A";
|
||||
|
||||
Log.e(TAG, "Instant Mix CRASH! Failed to map song to MediaItem. " +
|
||||
"Problematic Song ID: " + id +
|
||||
", Title: " + title +
|
||||
". Inspect this song's Subsonic data for missing fields.", e);
|
||||
throw new RuntimeException("Mapping failed for song ID: " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static MediaItem mapMediaItem(MediaItem old) {
|
||||
@@ -212,7 +237,7 @@ public class MappingUtil {
|
||||
|
||||
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
|
||||
Uri uri = getUri(podcastEpisode);
|
||||
Uri artworkUri = Uri.parse(CustomGlideRequest.createUrl(podcastEpisode.getCoverArtId(), Preferences.getImageSize()));
|
||||
Uri artworkUri = AlbumArtContentProvider.contentUri(podcastEpisode.getCoverArtId());
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", podcastEpisode.getId());
|
||||
|
||||
@@ -31,7 +31,7 @@ public class MusicUtil {
|
||||
private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+");
|
||||
|
||||
public static Uri getStreamUri(String id) {
|
||||
public static Uri getStreamUri(String id, int timeOffset) {
|
||||
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
|
||||
|
||||
StringBuilder uri = new StringBuilder();
|
||||
@@ -58,6 +58,8 @@ public class MusicUtil {
|
||||
uri.append("&format=").append(getTranscodingFormatPreference());
|
||||
if (Preferences.askForEstimateContentLength())
|
||||
uri.append("&estimateContentLength=true");
|
||||
if (timeOffset > 0)
|
||||
uri.append("&timeOffset=").append(timeOffset);
|
||||
|
||||
uri.append("&id=").append(id);
|
||||
|
||||
@@ -66,6 +68,10 @@ public class MusicUtil {
|
||||
return Uri.parse(uri.toString());
|
||||
}
|
||||
|
||||
public static Uri getStreamUri(String id) {
|
||||
return getStreamUri(id, 0);
|
||||
}
|
||||
|
||||
public static Uri updateStreamUri(Uri uri) {
|
||||
String s = uri.toString();
|
||||
Matcher m1 = BITRATE_PATTERN.matcher(s);
|
||||
|
||||
@@ -29,6 +29,7 @@ object Preferences {
|
||||
private const val REPEAT_MODE = "repeat_mode"
|
||||
private const val IMAGE_CACHE_SIZE = "image_cache_size"
|
||||
private const val STREAMING_CACHE_SIZE = "streaming_cache_size"
|
||||
private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row"
|
||||
private const val IMAGE_SIZE = "image_size"
|
||||
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
|
||||
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
|
||||
@@ -70,9 +71,12 @@ object Preferences {
|
||||
private const val SONG_RATING_PER_ITEM = "song_rating_per_item"
|
||||
private const val RATING_PER_ITEM = "rating_per_item"
|
||||
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
||||
private const val GITHUB_UPDATE_CHECK = "github_update_check"
|
||||
private const val CONTINUOUS_PLAY = "continuous_play"
|
||||
private const val LAST_INSTANT_MIX = "last_instant_mix"
|
||||
private const val ALLOW_PLAYLIST_DUPLICATES = "allow_playlist_duplicates"
|
||||
private const val HOME_SORT_PLAYLISTS = "home_sort_playlists"
|
||||
private const val DEFAULT_HOME_SORT_PLAYLISTS_SORT_ORDER = Constants.PLAYLIST_ORDER_BY_RANDOM
|
||||
private const val EQUALIZER_ENABLED = "equalizer_enabled"
|
||||
private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels"
|
||||
private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility"
|
||||
@@ -80,6 +84,10 @@ object Preferences {
|
||||
private const val ALBUM_SORT_ORDER = "album_sort_order"
|
||||
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
|
||||
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
|
||||
private const val SORT_SEARCH_CHRONOLOGICALLY= "sort_search_chronologically"
|
||||
private const val ARTIST_DISPLAY_BIOGRAPHY= "artist_display_biography"
|
||||
private const val NETWORK_PING_TIMEOUT = "network_ping_timeout_base"
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun getServer(): String? {
|
||||
@@ -91,6 +99,19 @@ object Preferences {
|
||||
App.getInstance().preferences.edit().putString(SERVER, server).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getNetworkPingTimeout(): Int {
|
||||
val timeoutString = App.getInstance().preferences.getString(NETWORK_PING_TIMEOUT, "2") ?: "2"
|
||||
return (timeoutString.toIntOrNull() ?: 2).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setNetworkPingTimeout(pingTimeout: String?) {
|
||||
App.getInstance().preferences.edit().putString(NETWORK_PING_TIMEOUT, pingTimeout).apply()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun getUser(): String? {
|
||||
return App.getInstance().preferences.getString(USER, null)
|
||||
@@ -284,6 +305,11 @@ object Preferences {
|
||||
return App.getInstance().preferences.getString(IMAGE_CACHE_SIZE, "500")!!.toInt()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLandscapeItemsPerRow(): Int {
|
||||
return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getImageSize(): Int {
|
||||
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()
|
||||
@@ -574,15 +600,21 @@ object Preferences {
|
||||
return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false)
|
||||
}
|
||||
|
||||
|
||||
@JvmStatic
|
||||
fun showTempoUpdateDialog(): Boolean {
|
||||
fun isGithubUpdateEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(GITHUB_UPDATE_CHECK, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showTempusUpdateDialog(): Boolean {
|
||||
return App.getInstance().preferences.getLong(
|
||||
NEXT_UPDATE_CHECK, 0
|
||||
) + 86400000 < System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setTempoUpdateReminder() {
|
||||
fun setTempusUpdateReminder() {
|
||||
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
|
||||
}
|
||||
|
||||
@@ -616,6 +648,16 @@ object Preferences {
|
||||
return App.getInstance().preferences.getBoolean(ALLOW_PLAYLIST_DUPLICATES, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getHomeSortPlaylists(): String {
|
||||
return App.getInstance().preferences.getString(HOME_SORT_PLAYLISTS, DEFAULT_HOME_SORT_PLAYLISTS_SORT_ORDER) ?: DEFAULT_HOME_SORT_PLAYLISTS_SORT_ORDER
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getHomeSortPlaylists(sortOrder: String) {
|
||||
App.getInstance().preferences.edit().putString(HOME_SORT_PLAYLISTS, sortOrder).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setEqualizerEnabled(enabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(EQUALIZER_ENABLED, enabled).apply()
|
||||
@@ -667,4 +709,19 @@ object Preferences {
|
||||
else
|
||||
return Constants.ARTIST_ORDER_BY_NAME
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isSearchSortingChronologicallyEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(SORT_SEARCH_CHRONOLOGICALLY, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getArtistDisplayBiography(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(ARTIST_DISPLAY_BIOGRAPHY, true)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setArtistDisplayBiography(displayBiographyEnabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(ARTIST_DISPLAY_BIOGRAPHY, displayBiographyEnabled).apply()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Metadata;
|
||||
import androidx.media3.common.Tracks;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.extractor.metadata.id3.InternalFrame;
|
||||
|
||||
import com.cappielloantonio.tempo.model.ReplayGain;
|
||||
|
||||
@@ -17,7 +19,7 @@ import java.util.Objects;
|
||||
public class ReplayGainUtil {
|
||||
private static final String[] tags = {"REPLAYGAIN_TRACK_GAIN", "REPLAYGAIN_ALBUM_GAIN", "R128_TRACK_GAIN", "R128_ALBUM_GAIN"};
|
||||
|
||||
public static void setReplayGain(ExoPlayer player, Tracks tracks) {
|
||||
public static void setReplayGain(Player player, Tracks tracks) {
|
||||
List<Metadata> metadata = getMetadata(tracks);
|
||||
List<ReplayGain> gains = getReplayGains(metadata);
|
||||
|
||||
@@ -62,7 +64,7 @@ public class ReplayGainUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (gains.size() == 0) gains.add(0, new ReplayGain());
|
||||
if (gains.isEmpty()) gains.add(0, new ReplayGain());
|
||||
if (gains.size() == 1) gains.add(1, new ReplayGain());
|
||||
|
||||
return gains;
|
||||
@@ -81,26 +83,32 @@ public class ReplayGainUtil {
|
||||
private static ReplayGain setReplayGains(Metadata.Entry entry) {
|
||||
ReplayGain replayGain = new ReplayGain();
|
||||
|
||||
if (entry.toString().contains(tags[0])) {
|
||||
replayGain.setTrackGain(parseReplayGainTag(entry));
|
||||
// The logic below assumes .toString() contains the dB value. That's not the case for InternalFrame
|
||||
String str = entry.toString();
|
||||
if (entry instanceof InternalFrame) {
|
||||
str = ((InternalFrame) entry).description + ((InternalFrame) entry).text;
|
||||
}
|
||||
|
||||
if (entry.toString().contains(tags[1])) {
|
||||
replayGain.setAlbumGain(parseReplayGainTag(entry));
|
||||
if (str.contains(tags[0])) {
|
||||
replayGain.setTrackGain(parseReplayGainTag(str));
|
||||
}
|
||||
|
||||
if (entry.toString().contains(tags[2])) {
|
||||
replayGain.setTrackGain(parseReplayGainTag(entry) / 256f);
|
||||
if (str.contains(tags[1])) {
|
||||
replayGain.setAlbumGain(parseReplayGainTag(str));
|
||||
}
|
||||
|
||||
if (entry.toString().contains(tags[3])) {
|
||||
replayGain.setAlbumGain(parseReplayGainTag(entry) / 256f);
|
||||
if (str.contains(tags[2])) {
|
||||
replayGain.setTrackGain(parseReplayGainTag(str) / 256f);
|
||||
}
|
||||
|
||||
if (str.contains(tags[3])) {
|
||||
replayGain.setAlbumGain(parseReplayGainTag(str) / 256f);
|
||||
}
|
||||
|
||||
return replayGain;
|
||||
}
|
||||
|
||||
private static Float parseReplayGainTag(Metadata.Entry entry) {
|
||||
private static Float parseReplayGainTag(String entry) {
|
||||
try {
|
||||
return Float.parseFloat(entry.toString().replaceAll("[^\\d.-]", ""));
|
||||
} catch (NumberFormatException exception) {
|
||||
@@ -108,7 +116,7 @@ public class ReplayGainUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void applyReplayGain(Player player, List<ReplayGain> gains) {
|
||||
if (Objects.equals(Preferences.getReplayGainMode(), "disabled") || gains == null || gains.isEmpty()) {
|
||||
setNoReplayGain(player);
|
||||
return;
|
||||
@@ -137,33 +145,33 @@ public class ReplayGainUtil {
|
||||
setNoReplayGain(player);
|
||||
}
|
||||
|
||||
private static void setNoReplayGain(ExoPlayer player) {
|
||||
private static void setNoReplayGain(Player player) {
|
||||
setReplayGain(player, 0f);
|
||||
}
|
||||
|
||||
private static void setTrackReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void setTrackReplayGain(Player player, List<ReplayGain> gains) {
|
||||
float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain();
|
||||
|
||||
setReplayGain(player, trackGain != 0f ? trackGain : 0f);
|
||||
}
|
||||
|
||||
private static void setAlbumReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void setAlbumReplayGain(Player player, List<ReplayGain> gains) {
|
||||
float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain();
|
||||
|
||||
setReplayGain(player, albumGain != 0f ? albumGain : 0f);
|
||||
}
|
||||
|
||||
private static void setAutoReplayGain(ExoPlayer player, List<ReplayGain> gains) {
|
||||
private static void setAutoReplayGain(Player player, List<ReplayGain> gains) {
|
||||
float albumGain = gains.get(0).getAlbumGain() != 0f ? gains.get(0).getAlbumGain() : gains.get(1).getAlbumGain();
|
||||
float trackGain = gains.get(0).getTrackGain() != 0f ? gains.get(0).getTrackGain() : gains.get(1).getTrackGain();
|
||||
|
||||
setReplayGain(player, albumGain != 0f ? albumGain : trackGain);
|
||||
}
|
||||
|
||||
private static boolean areTracksConsecutive(ExoPlayer player) {
|
||||
private static boolean areTracksConsecutive(Player player) {
|
||||
MediaItem currentMediaItem = player.getCurrentMediaItem();
|
||||
int currentMediaItemIndex = player.getCurrentMediaItemIndex();
|
||||
MediaItem pastMediaItem = currentMediaItemIndex > 0 ? player.getMediaItemAt(currentMediaItemIndex - 1) : null;
|
||||
int prevMediaItemIndex = player.getPreviousMediaItemIndex();
|
||||
MediaItem pastMediaItem = prevMediaItemIndex == C.INDEX_UNSET ? null : player.getMediaItemAt(prevMediaItemIndex);
|
||||
|
||||
return currentMediaItem != null &&
|
||||
pastMediaItem != null &&
|
||||
@@ -172,7 +180,7 @@ public class ReplayGainUtil {
|
||||
pastMediaItem.mediaMetadata.albumTitle.toString().equals(currentMediaItem.mediaMetadata.albumTitle.toString());
|
||||
}
|
||||
|
||||
private static void setReplayGain(ExoPlayer player, float gain) {
|
||||
private static void setReplayGain(Player player, float gain) {
|
||||
player.setVolume((float) Math.pow(10f, gain / 20f));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.cappielloantonio.tempo.util
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.common.util.Util
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.TransferListener
|
||||
import androidx.media3.decoder.DecoderInputBuffer
|
||||
import androidx.media3.exoplayer.FormatHolder
|
||||
import androidx.media3.exoplayer.LoadingInfo
|
||||
import androidx.media3.exoplayer.SeekParameters
|
||||
import androidx.media3.exoplayer.source.CompositeMediaSource
|
||||
import androidx.media3.exoplayer.source.ForwardingTimeline
|
||||
import androidx.media3.exoplayer.source.MediaPeriod
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.source.SampleStream
|
||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection
|
||||
import androidx.media3.exoplayer.upstream.Allocator
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class TranscodingMediaSource(
|
||||
private val mediaItem: MediaItem,
|
||||
private val dataSourceFactory: DataSource.Factory,
|
||||
private val progressiveMediaSourceFactory: ProgressiveMediaSource.Factory
|
||||
) : CompositeMediaSource<Void>() {
|
||||
|
||||
private var durationUs: Long = C.TIME_UNSET
|
||||
private var currentSource: MediaSource? = null
|
||||
|
||||
init {
|
||||
val extras = mediaItem.mediaMetadata.extras
|
||||
if (extras != null && extras.containsKey("duration")) {
|
||||
val seconds = extras.getInt("duration")
|
||||
if (seconds > 0) {
|
||||
durationUs = Util.msToUs(seconds * 1000L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMediaItem() = mediaItem
|
||||
|
||||
override fun prepareSourceInternal(mediaTransferListener: TransferListener?) {
|
||||
super.prepareSourceInternal(mediaTransferListener)
|
||||
val initialSource = progressiveMediaSourceFactory.createMediaSource(mediaItem)
|
||||
currentSource = initialSource
|
||||
prepareChildSource(null, initialSource)
|
||||
}
|
||||
|
||||
override fun onChildSourceInfoRefreshed(
|
||||
childSourceId: Void?,
|
||||
mediaSource: MediaSource,
|
||||
newTimeline: Timeline
|
||||
) {
|
||||
val timeline =
|
||||
if (durationUs != C.TIME_UNSET) {
|
||||
DurationOverridingTimeline(newTimeline, durationUs)
|
||||
} else {
|
||||
newTimeline
|
||||
}
|
||||
refreshSourceInfo(timeline)
|
||||
}
|
||||
|
||||
override fun createPeriod(
|
||||
id: MediaSource.MediaPeriodId,
|
||||
allocator: Allocator,
|
||||
startPositionUs: Long
|
||||
): MediaPeriod {
|
||||
val source = currentSource ?: throw IllegalStateException("Source not ready")
|
||||
val childPeriod = source.createPeriod(id, allocator, startPositionUs)
|
||||
return TranscodingMediaPeriod(childPeriod, source, id, allocator)
|
||||
}
|
||||
|
||||
override fun releasePeriod(mediaPeriod: MediaPeriod) {
|
||||
val transcodingPeriod = mediaPeriod as TranscodingMediaPeriod
|
||||
transcodingPeriod.release()
|
||||
|
||||
if (transcodingPeriod.currentOffsetUs > 0) {
|
||||
releaseChildSource(null)
|
||||
val initialSource = progressiveMediaSourceFactory.createMediaSource(mediaItem)
|
||||
currentSource = initialSource
|
||||
prepareChildSource(null, initialSource)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMediaPeriodIdForChildMediaPeriodId(
|
||||
childSourceId: Void?,
|
||||
mediaPeriodId: MediaSource.MediaPeriodId
|
||||
) = mediaPeriodId
|
||||
|
||||
private inner class TranscodingMediaPeriod(
|
||||
private var currentPeriod: MediaPeriod,
|
||||
private var source: MediaSource,
|
||||
private val id: MediaSource.MediaPeriodId,
|
||||
private val allocator: Allocator
|
||||
) : MediaPeriod, MediaPeriod.Callback {
|
||||
|
||||
private var localCallback: MediaPeriod.Callback? = null
|
||||
internal var currentOffsetUs: Long = 0
|
||||
private var isReloading = false
|
||||
|
||||
private var lastSelections: Array<out ExoTrackSelection?>? = null
|
||||
private var lastMayRetainStreamFlags: BooleanArray? = null
|
||||
private var activeWrappers: Array<OffsetSampleStream?> = emptyArray()
|
||||
|
||||
fun release() {
|
||||
source.releasePeriod(currentPeriod)
|
||||
}
|
||||
|
||||
override fun prepare(callback: MediaPeriod.Callback, positionUs: Long) {
|
||||
localCallback = callback
|
||||
currentPeriod.prepare(this, positionUs)
|
||||
}
|
||||
|
||||
override fun maybeThrowPrepareError() {
|
||||
if (!isReloading) currentPeriod.maybeThrowPrepareError()
|
||||
}
|
||||
|
||||
override fun getTrackGroups() = currentPeriod.trackGroups
|
||||
|
||||
override fun getStreamKeys(trackSelections: MutableList<ExoTrackSelection>) =
|
||||
currentPeriod.getStreamKeys(trackSelections)
|
||||
|
||||
override fun selectTracks(
|
||||
selections: Array<out ExoTrackSelection?>,
|
||||
mayRetainStreamFlags: BooleanArray,
|
||||
streams: Array<SampleStream?>,
|
||||
streamResetFlags: BooleanArray,
|
||||
positionUs: Long
|
||||
): Long {
|
||||
lastSelections = selections
|
||||
lastMayRetainStreamFlags = mayRetainStreamFlags
|
||||
|
||||
val childStreams = arrayOfNulls<SampleStream>(streams.size)
|
||||
streams.forEachIndexed { i, stream ->
|
||||
childStreams[i] = (stream as? OffsetSampleStream)?.childStream
|
||||
}
|
||||
|
||||
val startPos =
|
||||
currentPeriod.selectTracks(
|
||||
selections,
|
||||
mayRetainStreamFlags,
|
||||
childStreams,
|
||||
streamResetFlags,
|
||||
positionUs - currentOffsetUs
|
||||
)
|
||||
|
||||
val newWrappers = arrayOfNulls<OffsetSampleStream>(streams.size)
|
||||
for (i in streams.indices) {
|
||||
val child = childStreams[i]
|
||||
if (child == null) {
|
||||
streams[i] = null
|
||||
} else {
|
||||
val existingWrapper = streams[i] as? OffsetSampleStream
|
||||
if (existingWrapper != null && existingWrapper.childStream === child) {
|
||||
newWrappers[i] = existingWrapper
|
||||
} else {
|
||||
val wrapper = OffsetSampleStream(child)
|
||||
newWrappers[i] = wrapper
|
||||
streams[i] = wrapper
|
||||
}
|
||||
}
|
||||
}
|
||||
activeWrappers = newWrappers
|
||||
|
||||
return startPos + currentOffsetUs
|
||||
}
|
||||
|
||||
override fun discardBuffer(positionUs: Long, toKeyframe: Boolean) {
|
||||
if (!isReloading) {
|
||||
currentPeriod.discardBuffer(positionUs - currentOffsetUs, toKeyframe)
|
||||
}
|
||||
}
|
||||
|
||||
override fun readDiscontinuity(): Long {
|
||||
if (isReloading) return C.TIME_UNSET
|
||||
val discontinuity = currentPeriod.readDiscontinuity()
|
||||
return if (discontinuity == C.TIME_UNSET) C.TIME_UNSET
|
||||
else discontinuity + currentOffsetUs
|
||||
}
|
||||
|
||||
override fun seekToUs(positionUs: Long): Long {
|
||||
if (positionUs == 0L && currentOffsetUs == 0L) {
|
||||
return currentPeriod.seekToUs(positionUs)
|
||||
}
|
||||
|
||||
reloadSource(positionUs)
|
||||
return positionUs
|
||||
}
|
||||
|
||||
override fun getAdjustedSeekPositionUs(positionUs: Long, seekParameters: SeekParameters) =
|
||||
positionUs
|
||||
|
||||
override fun getBufferedPositionUs(): Long {
|
||||
if (isReloading) return currentOffsetUs
|
||||
val buffered = currentPeriod.bufferedPositionUs
|
||||
if (buffered == C.TIME_END_OF_SOURCE) return C.TIME_END_OF_SOURCE
|
||||
return if (buffered == C.TIME_UNSET) C.TIME_UNSET else buffered + currentOffsetUs
|
||||
}
|
||||
|
||||
override fun getNextLoadPositionUs(): Long {
|
||||
if (isReloading) return C.TIME_UNSET
|
||||
val next = currentPeriod.nextLoadPositionUs
|
||||
if (next == C.TIME_END_OF_SOURCE) return C.TIME_END_OF_SOURCE
|
||||
return if (next == C.TIME_UNSET) C.TIME_UNSET else next + currentOffsetUs
|
||||
}
|
||||
|
||||
override fun reevaluateBuffer(positionUs: Long) {
|
||||
if (!isReloading) currentPeriod.reevaluateBuffer(positionUs - currentOffsetUs)
|
||||
}
|
||||
|
||||
override fun continueLoading(isLoading: LoadingInfo): Boolean {
|
||||
if (isReloading) return false
|
||||
val builder = isLoading.buildUpon()
|
||||
builder.setPlaybackPositionUs(isLoading.playbackPositionUs - currentOffsetUs)
|
||||
return currentPeriod.continueLoading(builder.build())
|
||||
}
|
||||
|
||||
override fun isLoading() = isReloading || currentPeriod.isLoading
|
||||
|
||||
override fun onPrepared(mediaPeriod: MediaPeriod) {
|
||||
if (isReloading && mediaPeriod == currentPeriod) {
|
||||
isReloading = false
|
||||
restoreTracks()
|
||||
localCallback?.onContinueLoadingRequested(this)
|
||||
} else {
|
||||
localCallback?.onPrepared(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContinueLoadingRequested(source: MediaPeriod) {
|
||||
if (!isReloading) localCallback?.onContinueLoadingRequested(this)
|
||||
}
|
||||
|
||||
private fun reloadSource(positionUs: Long) {
|
||||
isReloading = true
|
||||
currentOffsetUs = positionUs
|
||||
|
||||
activeWrappers.forEach { it?.childStream = null }
|
||||
|
||||
source.releasePeriod(currentPeriod)
|
||||
releaseChildSource(null)
|
||||
|
||||
val seconds = Util.usToMs(positionUs) / 1000
|
||||
val newUri = MusicUtil.getStreamUri(mediaItem.mediaId, seconds.toInt())
|
||||
val newMediaItem = mediaItem.buildUpon().setUri(newUri).build()
|
||||
|
||||
val newSource = progressiveMediaSourceFactory.createMediaSource(newMediaItem)
|
||||
|
||||
source = newSource
|
||||
currentSource = newSource
|
||||
prepareChildSource(null, newSource)
|
||||
|
||||
val newPeriod = newSource.createPeriod(id, allocator, 0)
|
||||
currentPeriod = newPeriod
|
||||
newPeriod.prepare(this, 0)
|
||||
}
|
||||
|
||||
private fun restoreTracks() {
|
||||
val selections = lastSelections ?: return
|
||||
val flags = lastMayRetainStreamFlags ?: return
|
||||
|
||||
val childStreams = arrayOfNulls<SampleStream>(activeWrappers.size)
|
||||
val streamResetFlags = BooleanArray(activeWrappers.size)
|
||||
|
||||
currentPeriod.selectTracks(selections, flags, childStreams, streamResetFlags, 0)
|
||||
|
||||
for (i in activeWrappers.indices) {
|
||||
activeWrappers[i]?.childStream = childStreams[i]
|
||||
}
|
||||
}
|
||||
|
||||
private inner class OffsetSampleStream(var childStream: SampleStream?) : SampleStream {
|
||||
override fun isReady() = childStream?.isReady ?: false
|
||||
override fun maybeThrowError() {
|
||||
childStream?.maybeThrowError()
|
||||
}
|
||||
|
||||
override fun readData(
|
||||
formatHolder: FormatHolder,
|
||||
buffer: DecoderInputBuffer,
|
||||
readFlags: Int
|
||||
): Int {
|
||||
val stream = childStream ?: return C.RESULT_NOTHING_READ
|
||||
val result = stream.readData(formatHolder, buffer, readFlags)
|
||||
if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream) {
|
||||
buffer.timeUs += currentOffsetUs
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun skipData(positionUs: Long) =
|
||||
childStream?.skipData(positionUs - currentOffsetUs) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private class DurationOverridingTimeline(timeline: Timeline, private val durationUs: Long) :
|
||||
ForwardingTimeline(timeline) {
|
||||
|
||||
override fun getWindow(
|
||||
windowIndex: Int,
|
||||
window: Window,
|
||||
defaultPositionProjectionUs: Long
|
||||
): Window {
|
||||
super.getWindow(windowIndex, window, defaultPositionProjectionUs)
|
||||
window.durationUs = durationUs
|
||||
window.isSeekable = true
|
||||
window.isDynamic = false
|
||||
window.liveConfiguration = null
|
||||
return window
|
||||
}
|
||||
|
||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||
super.getPeriod(periodIndex, period, setIds)
|
||||
period.durationUs = durationUs
|
||||
return period
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
@@ -24,6 +27,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -33,8 +37,8 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
private final ArtistRepository artistRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final SharingRepository sharingRepository;
|
||||
|
||||
private AlbumID3 album;
|
||||
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
||||
|
||||
public AlbumBottomSheetViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
@@ -116,6 +120,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
MutableLiveData<List<Child>> tracksLiveData = albumRepository.getAlbumTracks(album.getId());
|
||||
|
||||
tracksLiveData.observeForever(new Observer<List<Child>>() {
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
@@ -129,4 +134,12 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getAlbumInstantMix(LifecycleOwner owner, AlbumID3 album) {
|
||||
instantMix.setValue(Collections.emptyList());
|
||||
|
||||
albumRepository.getInstantMix(album, 30).observe(owner, instantMix::postValue);
|
||||
|
||||
return instantMix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
@@ -21,7 +20,6 @@ import java.util.List;
|
||||
|
||||
public class AlbumListPageViewModel extends AndroidViewModel {
|
||||
private final AlbumRepository albumRepository;
|
||||
private final DownloadRepository downloadRepository;
|
||||
|
||||
public String title;
|
||||
public ArtistID3 artist;
|
||||
@@ -32,9 +30,7 @@ public class AlbumListPageViewModel extends AndroidViewModel {
|
||||
|
||||
public AlbumListPageViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
||||
albumRepository = new AlbumRepository();
|
||||
downloadRepository = new DownloadRepository();
|
||||
}
|
||||
|
||||
public LiveData<List<AlbumID3>> getAlbumList(LifecycleOwner owner) {
|
||||
|
||||
@@ -8,18 +8,23 @@ import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class AlbumPageViewModel extends AndroidViewModel {
|
||||
private final AlbumRepository albumRepository;
|
||||
private final ArtistRepository artistRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private String albumId;
|
||||
private String artistId;
|
||||
private final MutableLiveData<AlbumID3> album = new MutableLiveData<>(null);
|
||||
@@ -29,6 +34,7 @@ public class AlbumPageViewModel extends AndroidViewModel {
|
||||
|
||||
albumRepository = new AlbumRepository();
|
||||
artistRepository = new ArtistRepository();
|
||||
favoriteRepository = new FavoriteRepository();
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getAlbumSongLiveList() {
|
||||
@@ -49,6 +55,61 @@ public class AlbumPageViewModel extends AndroidViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
public void setFavorite() {
|
||||
AlbumID3 currentAlbum = album.getValue();
|
||||
if (currentAlbum == null) return;
|
||||
|
||||
if (currentAlbum.getStarred() != null) {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
removeFavoriteOffline(currentAlbum);
|
||||
} else {
|
||||
removeFavoriteOnline(currentAlbum);
|
||||
}
|
||||
} else {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
setFavoriteOffline(currentAlbum);
|
||||
} else {
|
||||
setFavoriteOnline(currentAlbum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFavoriteOffline(AlbumID3 album) {
|
||||
favoriteRepository.starLater(null, album.getId(), null, false);
|
||||
album.setStarred(null);
|
||||
this.album.postValue(album);
|
||||
}
|
||||
|
||||
private void removeFavoriteOnline(AlbumID3 album) {
|
||||
favoriteRepository.unstar(null, album.getId(), null, new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
favoriteRepository.starLater(null, album.getId(), null, false);
|
||||
}
|
||||
});
|
||||
|
||||
album.setStarred(null);
|
||||
this.album.postValue(album);
|
||||
}
|
||||
|
||||
private void setFavoriteOffline(AlbumID3 album) {
|
||||
favoriteRepository.starLater(null, album.getId(), null, true);
|
||||
album.setStarred(new Date());
|
||||
this.album.postValue(album);
|
||||
}
|
||||
|
||||
private void setFavoriteOnline(AlbumID3 album) {
|
||||
favoriteRepository.star(null, album.getId(), null, new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
favoriteRepository.starLater(null, album.getId(), null, true);
|
||||
}
|
||||
});
|
||||
|
||||
album.setStarred(new Date());
|
||||
this.album.postValue(album);
|
||||
}
|
||||
|
||||
public LiveData<ArtistID3> getArtist() {
|
||||
return artistRepository.getArtistInfo(artistId);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
@@ -17,6 +22,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
@@ -24,6 +30,7 @@ import java.util.List;
|
||||
public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
private final ArtistRepository artistRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
|
||||
|
||||
private ArtistID3 artist;
|
||||
|
||||
@@ -95,6 +102,7 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
Log.d("ArtistSync", "Starting artist sync for: " + artist.getName());
|
||||
|
||||
artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() {
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@Override
|
||||
public void onSongsCollected(List<Child> songs) {
|
||||
Log.d("ArtistSync", "Callback triggered with songs: " + (songs != null ? songs.size() : 0));
|
||||
@@ -114,5 +122,12 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
Log.d("ArtistSync", "Artist sync preference is disabled");
|
||||
}
|
||||
}
|
||||
///
|
||||
|
||||
public LiveData<List<Child>> getArtistInstantMix(LifecycleOwner owner, ArtistID3 artist) {
|
||||
instantMix.setValue(Collections.emptyList());
|
||||
|
||||
artistRepository.getInstantMix(artist, 30).observe(owner, instantMix::postValue);
|
||||
|
||||
return instantMix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ArtistPageViewModel extends AndroidViewModel {
|
||||
private final AlbumRepository albumRepository;
|
||||
private final ArtistRepository artistRepository;
|
||||
private final FavoriteRepository favoriteRepository;
|
||||
|
||||
private ArtistID3 artist;
|
||||
|
||||
@@ -26,6 +40,7 @@ public class ArtistPageViewModel extends AndroidViewModel {
|
||||
|
||||
albumRepository = new AlbumRepository();
|
||||
artistRepository = new ArtistRepository();
|
||||
favoriteRepository = new FavoriteRepository();
|
||||
}
|
||||
|
||||
public LiveData<List<AlbumID3>> getAlbumList() {
|
||||
@@ -45,7 +60,7 @@ public class ArtistPageViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getArtistInstantMix() {
|
||||
return artistRepository.getInstantMix(artist, 20);
|
||||
return artistRepository.getInstantMix(artist, 30);
|
||||
}
|
||||
|
||||
public ArtistID3 getArtist() {
|
||||
@@ -55,4 +70,70 @@ public class ArtistPageViewModel extends AndroidViewModel {
|
||||
public void setArtist(ArtistID3 artist) {
|
||||
this.artist = artist;
|
||||
}
|
||||
|
||||
public void setFavorite(Context context) {
|
||||
if (artist.getStarred() != null) {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
removeFavoriteOffline();
|
||||
} else {
|
||||
removeFavoriteOnline();
|
||||
}
|
||||
} else {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
setFavoriteOffline();
|
||||
} else {
|
||||
setFavoriteOnline(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeFavoriteOffline() {
|
||||
favoriteRepository.starLater(null, null, artist.getId(), false);
|
||||
artist.setStarred(null);
|
||||
}
|
||||
|
||||
private void removeFavoriteOnline() {
|
||||
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
favoriteRepository.starLater(null, null, artist.getId(), false);
|
||||
}
|
||||
});
|
||||
|
||||
artist.setStarred(null);
|
||||
}
|
||||
|
||||
private void setFavoriteOffline() {
|
||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||
artist.setStarred(new Date());
|
||||
}
|
||||
|
||||
private void setFavoriteOnline(Context context) {
|
||||
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||
}
|
||||
});
|
||||
|
||||
artist.setStarred(new Date());
|
||||
|
||||
if (Preferences.isStarredArtistsSyncEnabled()) {
|
||||
artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() {
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@Override
|
||||
public void onSongsCollected(List<Child> songs) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d("ArtistSync", "Artist sync preference is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.google.gson.Gson;
|
||||
@@ -34,7 +36,6 @@ import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HomeViewModel extends AndroidViewModel {
|
||||
private static final String TAG = "HomeViewModel";
|
||||
@@ -100,7 +101,7 @@ public class HomeViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getRandomShuffleSample() {
|
||||
return songRepository.getRandomSample(1000, null, null);
|
||||
return songRepository.getRandomSample(100, null, null);
|
||||
}
|
||||
|
||||
public LiveData<List<Chronology>> getChronologySample(LifecycleOwner owner) {
|
||||
@@ -223,7 +224,7 @@ public class HomeViewModel extends AndroidViewModel {
|
||||
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
||||
mediaInstantMix.setValue(Collections.emptyList());
|
||||
|
||||
songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
|
||||
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 20).observe(owner, mediaInstantMix::postValue);
|
||||
|
||||
return mediaInstantMix;
|
||||
}
|
||||
@@ -248,15 +249,22 @@ public class HomeViewModel extends AndroidViewModel {
|
||||
pinnedPlaylists.setValue(Collections.emptyList());
|
||||
|
||||
playlistRepository.getPlaylists(false, -1).observe(owner, remotes -> {
|
||||
playlistRepository.getPinnedPlaylists().observe(owner, locals -> {
|
||||
if (remotes != null && locals != null) {
|
||||
List<Playlist> toReturn = remotes.stream()
|
||||
.filter(remote -> locals.stream().anyMatch(local -> local.getId().equals(remote.getId())))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
pinnedPlaylists.setValue(toReturn);
|
||||
if (remotes != null && !remotes.isEmpty()) {
|
||||
List<Playlist> playlists = new ArrayList<>(remotes);
|
||||
String result = Preferences.getHomeSortPlaylists();
|
||||
if (Preferences.getHomeSortPlaylists().equals(Constants.PLAYLIST_ORDER_BY_RANDOM))
|
||||
{
|
||||
Collections.shuffle(playlists);
|
||||
}
|
||||
});
|
||||
else {
|
||||
playlists.sort(Comparator.comparing(Playlist::getName));
|
||||
}
|
||||
List<Playlist> subsetPlaylists = playlists.size() > 5
|
||||
? playlists.subList(0, 5)
|
||||
: playlists;
|
||||
|
||||
pinnedPlaylists.setValue(subsetPlaylists);
|
||||
}
|
||||
});
|
||||
|
||||
return pinnedPlaylists;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.viewmodel;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
@@ -276,7 +277,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
|
||||
instantMix.setValue(Collections.emptyList());
|
||||
|
||||
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
|
||||
songRepository.getInstantMix(media.getId(), Constants.SeedType.TRACK, 20).observe(owner, instantMix::postValue);
|
||||
|
||||
return instantMix;
|
||||
}
|
||||
@@ -291,13 +292,13 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||
List<String> ids = queue.stream().map(Child::getId).collect(Collectors.toList());
|
||||
|
||||
if (media != null) {
|
||||
queueRepository.savePlayQueue(ids, media.getId(), 0);
|
||||
// TODO: We need to get the actual playback position here
|
||||
Log.d(TAG, "Saving play queue - Current: " + media.getId() + ", Items: " + ids.size());
|
||||
queueRepository.savePlayQueue(ids, media.getId(), 0); // Still hardcoded to 0 for now
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void observeCachedLyrics(LifecycleOwner owner, String songId) {
|
||||
if (TextUtils.isEmpty(songId)) {
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Dialog;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
@@ -21,8 +20,17 @@ import java.util.List;
|
||||
|
||||
public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||
private final PlaylistRepository playlistRepository;
|
||||
|
||||
private final MutableLiveData<List<Playlist>> playlists = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Boolean> playlistIsPublic = new MutableLiveData<>(false);
|
||||
|
||||
public Boolean getIsPlaylistPublic() {
|
||||
return playlistIsPublic.getValue();
|
||||
}
|
||||
|
||||
public void setIsPlaylistPublic(boolean isPublic) {
|
||||
playlistIsPublic.setValue(isPublic);
|
||||
}
|
||||
|
||||
private ArrayList<Child> toAdd = new ArrayList<>();
|
||||
|
||||
public PlaylistChooserViewModel(@NonNull Application application) {
|
||||
@@ -39,7 +47,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||
public void addSongsToPlaylist(LifecycleOwner owner, Dialog dialog, String playlistId) {
|
||||
List<String> songIds = Lists.transform(toAdd, Child::getId);
|
||||
if (Preferences.allowPlaylistDuplicates()) {
|
||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
|
||||
dialog.dismiss();
|
||||
} else {
|
||||
playlistRepository.getPlaylistSongs(playlistId).observe(owner, playlistSongs -> {
|
||||
@@ -47,7 +55,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||
List<String> playlistSongIds = Lists.transform(playlistSongs, Child::getId);
|
||||
songIds.removeAll(playlistSongIds);
|
||||
}
|
||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds));
|
||||
playlistRepository.addSongToPlaylist(playlistId, new ArrayList<>(songIds), getIsPlaylistPublic());
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
|
||||
import com.cappielloantonio.tempo.repository.PodcastRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.PodcastChannel;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class PodcastChannelBottomSheetViewModel extends AndroidViewModel {
|
||||
private static final String TAG = "PodcastChannelBottomSheetViewModel";
|
||||
private final PodcastRepository podcastRepository;
|
||||
|
||||
private PodcastChannel podcastChannel;
|
||||
@@ -28,6 +38,59 @@ public class PodcastChannelBottomSheetViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public void deletePodcastChannel() {
|
||||
if (podcastChannel != null) podcastRepository.deletePodcastChannel(podcastChannel.getId());
|
||||
if (podcastChannel != null && podcastChannel.getId() != null) {
|
||||
podcastRepository.deletePodcastChannel(podcastChannel.getId())
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.code() == 501) {
|
||||
Toast.makeText(getApplication(),
|
||||
"Podcasts are not supported by this server",
|
||||
Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse apiResponse = response.body();
|
||||
|
||||
String status = apiResponse.subsonicResponse.getStatus();
|
||||
|
||||
if ("ok".equals(status)) {
|
||||
Toast.makeText(getApplication(),
|
||||
"Podcast channel deleted",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
//TODO refresh the UI after deleting
|
||||
//podcastRepository.refreshPodcasts();
|
||||
}
|
||||
} else {
|
||||
handleHttpError(response);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Toast.makeText(getApplication(),
|
||||
"Network error: " + t.getMessage(),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void handleHttpError(Response<ApiResponse> response) {
|
||||
String errorMsg = "HTTP error: " + response.code();
|
||||
if (response.errorBody() != null) {
|
||||
try {
|
||||
String serverMsg = response.errorBody().string();
|
||||
if (!serverMsg.isEmpty()) {
|
||||
errorMsg += " - " + serverMsg;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error reading error body", e);
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(getApplication(), errorMsg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +1,99 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.repository.PodcastRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class PodcastChannelEditorViewModel extends AndroidViewModel {
|
||||
private static final String TAG = "RadioEditorViewModel";
|
||||
private static final String TAG = "PodcastChannelEditorViewModel";
|
||||
|
||||
private final PodcastRepository podcastRepository;
|
||||
|
||||
private InternetRadioStation toEdit;
|
||||
private final MutableLiveData<Boolean> isSuccess = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
||||
|
||||
public PodcastChannelEditorViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
||||
podcastRepository = new PodcastRepository();
|
||||
}
|
||||
|
||||
public void createChannel(String url) {
|
||||
podcastRepository.createPodcastChannel(url);
|
||||
public LiveData<Boolean> getIsSuccess() {
|
||||
return isSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<String> getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void clearError() {
|
||||
errorMessage.setValue(null);
|
||||
}
|
||||
|
||||
public void createChannel(String url) {
|
||||
errorMessage.setValue(null);
|
||||
isSuccess.setValue(false);
|
||||
|
||||
podcastRepository.createPodcastChannel(url)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.code() == 501) {
|
||||
showError(getApplication().getString(R.string.podcast_channel_not_supported_snackbar));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse apiResponse = response.body();
|
||||
|
||||
String status = apiResponse.subsonicResponse.getStatus();
|
||||
if ("ok".equals(status)) {
|
||||
isSuccess.setValue(true);
|
||||
}
|
||||
} else {
|
||||
handleHttpError(response);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
showError("Network error: " + t.getMessage());
|
||||
Log.e(TAG, "Network error", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleHttpError(Response<ApiResponse> response) {
|
||||
String errorMsg = "HTTP error: " + response.code();
|
||||
if (response.errorBody() != null) {
|
||||
try {
|
||||
String serverMsg = response.errorBody().string();
|
||||
if (!serverMsg.isEmpty()) {
|
||||
errorMsg += " - " + serverMsg;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error reading error body", e);
|
||||
}
|
||||
}
|
||||
showError(errorMsg);
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show();
|
||||
errorMessage.setValue(message);
|
||||
Log.e(TAG, "Error shown: " + message);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,47 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.repository.RadioRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class RadioEditorViewModel extends AndroidViewModel {
|
||||
private static final String TAG = "RadioEditorViewModel";
|
||||
|
||||
private final RadioRepository radioRepository;
|
||||
|
||||
private InternetRadioStation toEdit;
|
||||
|
||||
private final MutableLiveData<Boolean> isSuccess = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
||||
|
||||
public RadioEditorViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
|
||||
radioRepository = new RadioRepository();
|
||||
}
|
||||
|
||||
|
||||
public LiveData<Boolean> getIsSuccess() { return isSuccess; }
|
||||
public LiveData<String> getErrorMessage() { return errorMessage; }
|
||||
|
||||
public void clearError() {
|
||||
errorMessage.setValue(null);
|
||||
}
|
||||
|
||||
public InternetRadioStation getRadioToEdit() {
|
||||
return toEdit;
|
||||
}
|
||||
@@ -30,14 +51,120 @@ public class RadioEditorViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public void createRadio(String name, String streamURL, String homepageURL) {
|
||||
radioRepository.createInternetRadioStation(name, streamURL, homepageURL);
|
||||
errorMessage.setValue(null);
|
||||
isSuccess.setValue(false);
|
||||
|
||||
radioRepository.createInternetRadioStation(name, streamURL, homepageURL)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
// Handle HTTP 501 (Not Implemented) from Navidrome
|
||||
if (response.code() == 501) {
|
||||
showError(getApplication().getString(R.string.radio_dialog_not_supported_snackbar));
|
||||
return;
|
||||
}
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse apiResponse = response.body();
|
||||
String status = apiResponse.subsonicResponse.getStatus();
|
||||
if ("ok".equals(status)) {
|
||||
isSuccess.setValue(true);
|
||||
} else if ("failed".equals(status)) {
|
||||
handleFailedResponse(apiResponse);
|
||||
}
|
||||
} else {
|
||||
errorMessage.setValue("HTTP error: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
errorMessage.setValue("Network error: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateRadio(String name, String streamURL, String homepageURL) {
|
||||
if (toEdit != null) radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL);
|
||||
if (toEdit != null && toEdit.getId() != null) {
|
||||
errorMessage.setValue(null);
|
||||
isSuccess.setValue(false);
|
||||
|
||||
radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse apiResponse = response.body();
|
||||
if (apiResponse.subsonicResponse != null) {
|
||||
String status = apiResponse.subsonicResponse.getStatus();
|
||||
if ("ok".equals(status)) {
|
||||
isSuccess.setValue(true);
|
||||
} else if ("failed".equals(status)) {
|
||||
handleFailedResponse(apiResponse);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorMessage.setValue("HTTP error: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
errorMessage.setValue("Network error: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteRadio() {
|
||||
if (toEdit != null) radioRepository.deleteInternetRadioStation(toEdit.getId());
|
||||
if (toEdit != null && toEdit.getId() != null) {
|
||||
errorMessage.setValue(null);
|
||||
isSuccess.setValue(false);
|
||||
|
||||
radioRepository.deleteInternetRadioStation(toEdit.getId())
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ApiResponse apiResponse = response.body();
|
||||
|
||||
String status = apiResponse.subsonicResponse.getStatus();
|
||||
|
||||
if ("ok".equals(status)) {
|
||||
isSuccess.setValue(true);
|
||||
} else if ("failed".equals(status)) {
|
||||
handleFailedResponse(apiResponse);
|
||||
}
|
||||
} else {
|
||||
errorMessage.setValue("HTTP error: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
errorMessage.setValue("Network error: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showError(String message) {
|
||||
Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show();
|
||||
errorMessage.setValue(message);
|
||||
}
|
||||
|
||||
private void handleFailedResponse(ApiResponse apiResponse) {
|
||||
String errorMsg = "Unknown server error";
|
||||
|
||||
if (apiResponse.subsonicResponse.getError() != null) {
|
||||
errorMsg = apiResponse.subsonicResponse.getError().getMessage();
|
||||
|
||||
if ("Not implemented".equals(errorMsg)) {
|
||||
errorMsg = getApplication().getString((R.string.radio_dialog_not_supported_snackbar));
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(getApplication(), errorMsg, Toast.LENGTH_LONG).show();
|
||||
|
||||
errorMessage.setValue(errorMsg);
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,11 @@ public class SearchViewModel extends AndroidViewModel {
|
||||
}
|
||||
|
||||
public void insertNewSearch(String search) {
|
||||
searchingRepository.insert(new RecentSearch(search));
|
||||
searchingRepository.insert(new RecentSearch(search, System.currentTimeMillis() / 1000L));
|
||||
}
|
||||
|
||||
public void deleteRecentSearch(String search) {
|
||||
searchingRepository.delete(new RecentSearch(search));
|
||||
searchingRepository.delete(new RecentSearch(search, 0));
|
||||
}
|
||||
|
||||
public LiveData<List<String>> getSearchSuggestion(String query) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
@@ -21,6 +22,7 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
@@ -128,11 +130,22 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
|
||||
instantMix.setValue(Collections.emptyList());
|
||||
|
||||
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
|
||||
songRepository.getInstantMix(media.getId(), SeedType.TRACK, 30).observe(owner, instantMix::postValue);
|
||||
|
||||
return instantMix;
|
||||
}
|
||||
|
||||
public void getInstantMix(Child media, int count, MediaCallback callback) {
|
||||
|
||||
songRepository.getInstantMix(media.getId(), SeedType.TRACK, count, songs -> {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
callback.onLoadMedia(songs);
|
||||
} else {
|
||||
callback.onLoadMedia(Collections.emptyList());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public MutableLiveData<Share> shareTrack() {
|
||||
return sharingRepository.createShare(song.getId(), song.getTitle(), null);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class SongListPageViewModel extends AndroidViewModel {
|
||||
|
||||
public int year = 0;
|
||||
public int maxNumberByYear = 500;
|
||||
public int maxNumberByGenre = 100;
|
||||
public int maxNumberByGenre = 500;
|
||||
|
||||
public SongListPageViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
@@ -51,7 +51,7 @@ public class SongListPageViewModel extends AndroidViewModel {
|
||||
|
||||
switch (title) {
|
||||
case Constants.MEDIA_BY_GENRE:
|
||||
songList = songRepository.getSongsByGenre(genre.getGenre(), 0);
|
||||
songList = songRepository.getRandomSampleWithGenre(maxNumberByGenre, 0, 3000, genre.getGenre());
|
||||
break;
|
||||
case Constants.MEDIA_BY_ARTIST:
|
||||
songList = artistRepository.getTopSongs(artist.getName(), 50);
|
||||
|
||||
14
app/src/main/res/drawable/ic_graphic_eq_land.xml
Normal file
14
app/src/main/res/drawable/ic_graphic_eq_land.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotY="12"
|
||||
android:pivotX="12"
|
||||
android:rotation="270">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8,18c0.55,0 1,-0.45 1,-1L9,7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v10c0,0.55 0.45,1 1,1zM12,22c0.55,0 1,-0.45 1,-1L13,3c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v18c0,0.55 0.45,1 1,1zM4,14c0.55,0 1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v2c0,0.55 0.45,1 1,1zM16,18c0.55,0 1,-0.45 1,-1L17,7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v10c0,0.55 0.45,1 1,1zM19,11v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1z" />
|
||||
</group>
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/ic_home_land.xml
Normal file
14
app/src/main/res/drawable/ic_home_land.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotY="12"
|
||||
android:pivotX="12"
|
||||
android:rotation="270">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,5.69l5,4.5V18h-2v-6H9v6H7v-7.81l5,-4.5M12,3L2,12h3v8h6v-6h2v6h6v-8h3L12,3z" />
|
||||
</group>
|
||||
</vector>
|
||||
14
app/src/main/res/drawable/ic_play_for_work_land.xml
Normal file
14
app/src/main/res/drawable/ic_play_for_work_land.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group
|
||||
android:pivotY="12"
|
||||
android:pivotX="12"
|
||||
android:rotation="270">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11,5v5.59L7.5,10.59l4.5,4.5 4.5,-4.5L13,10.59L13,5h-2zM6,14c0,3.31 2.69,6 6,6s6,-2.69 6,-6h-2c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4L6,14z"/>
|
||||
</group>
|
||||
</vector>
|
||||
68
app/src/main/res/layout-land/activity_main.xml
Normal file
68
app/src/main/res/layout-land/activity_main.xml
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
android:id="@+id/bottom_navigation"
|
||||
android:layout_width="250dp"
|
||||
android:layout_height="75dp"
|
||||
android:rotation="90"
|
||||
android:layout_gravity="center"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:visibility="gone"
|
||||
app:menu="@menu/bottom_nav_menu" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_graph" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/player_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="@dimen/bottom_sheet_peek_height"
|
||||
app:layout_behavior="@string/bottom_sheet_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/offline_mode_text_view"
|
||||
style="@style/NoConnectionTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/activity_info_offline_mode"
|
||||
android:textSize="6sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
305
app/src/main/res/layout-land/fragment_album_page.xml
Normal file
305
app/src/main/res/layout-land/fragment_album_page.xml
Normal file
@@ -0,0 +1,305 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/anim_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorSurface"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_arrow_back" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/album_info_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:paddingStart="20dp"
|
||||
app:layout_scrollFlags="exitUntilCollapsed">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/album_cover_image_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginLeft="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginRight="64dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_name_label"
|
||||
style="@style/LabelExtraLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingTop="8dp"
|
||||
android:singleLine="false"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="@+id/album_cover_image_view"
|
||||
app:layout_constraintStart_toStartOf="@+id/album_cover_image_view"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_cover_image_view" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/album_other_info_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/album_name_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/album_name_label"
|
||||
app:layout_constraintTop_toTopOf="@+id/album_name_label">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="@drawable/ic_arrow_down" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_artist_label"
|
||||
style="@style/LabelMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_name_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_release_year_label"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_artist_label" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/album_detail_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_release_year_label"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_genres_textview"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_song_count_duration_textview"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:paddingVertical="2dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_genres_textview" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_notes_textview"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:justificationMode="inter_word"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_song_count_duration_textview" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_release_years_textview"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:paddingVertical="4dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_notes_textview" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/upper_button_divider"
|
||||
style="@style/Divider"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_detail_view" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/album_page_button_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
android:id="@+id/album_page_play_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/album_page_play_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_play"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/album_page_shuffle_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/album_page_shuffle_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_shuffle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/button_favorite"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="@drawable/button_favorite_selector"
|
||||
android:checked="false"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text=""
|
||||
android:textOff=""
|
||||
android:textOn="" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_bio_label"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/album_page_button_layout"
|
||||
tools:ignore="NotSibling" />
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_button_divider"
|
||||
style="@style/Divider"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:layout_marginBottom="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_bio_label" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/song_recycler_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="75dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
218
app/src/main/res/layout-land/fragment_playlist_page.xml
Normal file
218
app/src/main/res/layout-land/fragment_playlist_page.xml
Normal file
@@ -0,0 +1,218 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/anim_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorSurface"
|
||||
app:layout_collapseMode="pin"
|
||||
app:navigationIcon="@drawable/ic_arrow_back" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="@dimen/global_padding_bottom"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/playlist_info_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playlist_cover_image_view_top_left"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_top_right"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playlist_cover_image_view_top_right"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="64dp"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_top_left"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playlist_cover_image_view_bottom_left"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="64dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toStartOf="@id/playlist_cover_image_view_bottom_right"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/playlist_cover_image_view_top_left" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/playlist_cover_image_view_bottom_right"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="64dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintDimensionRatio="H,1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/playlist_cover_image_view_bottom_left"
|
||||
app:layout_constraintTop_toTopOf="@id/playlist_cover_image_view_bottom_left" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_name_label"
|
||||
style="@style/LabelExtraLarge"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:paddingTop="8dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playlist_cover_image_view_bottom_left" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_song_count_label"
|
||||
style="@style/LabelMedium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playlist_name_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_duration_label"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playlist_song_count_label" />
|
||||
|
||||
<View
|
||||
android:id="@+id/upper_button_divider"
|
||||
style="@style/Divider"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playlist_duration_label" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/playlist_page_button_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
|
||||
|
||||
<Button
|
||||
android:id="@+id/playlist_page_play_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/playlist_page_play_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_play"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/playlist_page_shuffle_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/playlist_page_shuffle_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_shuffle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/album_bio_label"
|
||||
style="@style/LabelSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:text="@string/label_placeholder"
|
||||
android:textAlignment="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_button_divider"
|
||||
style="@style/Divider"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:layout_marginBottom="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/playlist_page_button_layout" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/song_recycler_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false"
|
||||
android:paddingTop="8dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
@@ -174,7 +174,6 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_notes_textview" />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<View
|
||||
@@ -188,43 +187,69 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_detail_view" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/album_page_button_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/upper_button_divider">
|
||||
|
||||
<Button
|
||||
android:id="@+id/album_page_play_button"
|
||||
<LinearLayout
|
||||
android:id="@+id/album_page_button_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/album_page_play_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_play"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/album_page_play_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/album_page_play_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_play"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/album_page_shuffle_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/album_page_shuffle_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_shuffle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/button_favorite"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="@drawable/button_favorite_selector"
|
||||
android:checked="false"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text=""
|
||||
android:textOff=""
|
||||
android:textOn="" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/album_page_shuffle_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/album_page_shuffle_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_shuffle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
@@ -239,7 +264,8 @@
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_page_button_layout" />
|
||||
app:layout_constraintTop_toBottomOf="@id/album_page_button_layout"
|
||||
tools:ignore="NotSibling" />
|
||||
|
||||
<View
|
||||
android:id="@+id/bottom_button_divider"
|
||||
@@ -249,7 +275,7 @@
|
||||
android:layout_marginBottom="18dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_page_button_layout" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/album_bio_label" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
||||
@@ -63,40 +63,80 @@
|
||||
android:layout_marginEnd="18dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/album_page_button_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/artist_page_shuffle_button"
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/album_page_button_layout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/artist_page_shuffle_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_shuffle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/artist_page_shuffle_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/artist_page_shuffle_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_shuffle"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/artist_page_radio_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:padding="10dp"
|
||||
android:text="@string/artist_page_radio_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_feed"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/button_favorite"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="@drawable/button_favorite_selector"
|
||||
android:checked="false"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text=""
|
||||
android:textOff=""
|
||||
android:textOn="" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/artist_page_radio_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="10dp"
|
||||
android:text="@string/artist_page_radio_button"
|
||||
android:textAllCaps="false"
|
||||
app:icon="@drawable/ic_feed"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="18dp" />
|
||||
android:id="@+id/button_toggle_bio"
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="34dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:background="@drawable/ic_info_stream"
|
||||
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:gravity="center"
|
||||
android:text=""
|
||||
android:textOff=""
|
||||
android:textOn="" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
|
||||
@@ -379,16 +379,6 @@
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Best of -->
|
||||
<LinearLayout
|
||||
android:id="@+id/home_best_of_artist_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/most_streamed_song_pre_text_view"
|
||||
@@ -400,6 +390,16 @@
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/home_subtitle_best_of"
|
||||
android:textAllCaps="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Best of -->
|
||||
<LinearLayout
|
||||
android:id="@+id/home_best_of_artist_sector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/best_of_artist_text_view_refreshable"
|
||||
@@ -566,6 +566,7 @@
|
||||
android:paddingBottom="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!--Starred Albums-->
|
||||
<LinearLayout
|
||||
android:id="@+id/starred_albums_sector"
|
||||
android:layout_width="match_parent"
|
||||
@@ -615,6 +616,7 @@
|
||||
android:paddingBottom="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!--Starred Artists-->
|
||||
<LinearLayout
|
||||
android:id="@+id/starred_artists_sector"
|
||||
android:layout_width="match_parent"
|
||||
@@ -913,16 +915,36 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<!-- Label and button -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pinned_playlists_text_view"
|
||||
style="@style/TitleLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/home_title_pinned_playlists" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playlist_catalogue_text_view_clickable"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/library_title_playlist_see_all_button" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/pinned_playlists_recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -20,6 +20,20 @@
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/global_padding_bottom">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/podcast_channels_pre_text_view"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/home_subtitle_new_podcast_channel"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/home_podcast_channels_sector"
|
||||
android:layout_width="match_parent"
|
||||
@@ -29,17 +43,6 @@
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/podcast_channels_pre_text_view"
|
||||
style="@style/TitleMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/home_subtitle_new_podcast_channel"
|
||||
android:textAllCaps="true" />
|
||||
|
||||
<!-- Label and button -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -169,4 +172,4 @@
|
||||
android:gravity="center"
|
||||
android:text="@string/podcast_info_empty_button" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user