Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e09169b111 | |||
| 4ee1822057 | |||
|
|
a179db6323 | ||
|
|
1beeab28a6 | ||
|
|
ad6a569961 | ||
|
|
0f5a8f6b97 | ||
|
|
3cd1bdf229 | ||
|
|
d7389db265 | ||
|
|
b3c93b3885 | ||
|
|
25864accc9 | ||
|
|
f734ced2cb | ||
|
|
4a3d2305c0 | ||
|
|
8db6797eaa | ||
|
|
cb4c19757d | ||
|
|
b6e75afe12 | ||
|
|
b621be06df | ||
|
|
7a17e91690 | ||
|
|
1036829186 | ||
|
|
becfc1d589 | ||
|
|
44bf346332 | ||
|
|
896e5fb3bd | ||
|
|
3086a8b9f9 | ||
|
|
10c2172be0 | ||
|
|
918bf6928e | ||
|
|
c9cf86acb5 | ||
|
|
0487f3bb9b | ||
|
|
c7f2524085 | ||
|
|
88c2129cd4 | ||
|
|
aa5d0f92db | ||
|
|
3ba2255205 | ||
|
|
145bb82eb0 | ||
|
|
932d1aaa8c | ||
|
|
4f8212d491 | ||
|
|
b403d69982 | ||
|
|
a49f2b97a2 | ||
|
|
c44e60c0e5 | ||
|
|
4cd15b4284 | ||
|
|
72d7aea6e3 | ||
|
|
9adaf8c013 | ||
|
|
661346ca3a | ||
|
|
dbd32baa12 | ||
|
|
3958cbcc1c | ||
|
|
fb568d1d74 | ||
|
|
e06a168350 | ||
|
|
b8dc985279 | ||
|
|
090701b92b | ||
|
|
7767a66fb8 | ||
|
|
d1122bef4e | ||
|
|
72d4495582 | ||
|
|
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 |
153
CHANGELOG.md
@@ -1,8 +1,158 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Pending release...
|
## What's Changed
|
||||||
|
## [4.13.0](https://github.com/eddyizm/tempo/releases/tag/v4.13.0) (2026-03-25)
|
||||||
|
* chore(i18n): Improve Russian translation by @NikkoFox in https://github.com/eddyizm/tempus/pull/503
|
||||||
|
* feat: tile size manager by @MaFo-28 in https://github.com/eddyizm/tempus/pull/440
|
||||||
|
* chore(i18n): Translated to zh_TW by @olivertzeng in https://github.com/eddyizm/tempus/pull/494
|
||||||
|
* fix: Show full album name when displaying details by @jaime-grj in https://github.com/eddyizm/tempus/pull/508
|
||||||
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/509
|
||||||
|
* fix: Relocate "Offline mode" text by @jaime-grj in https://github.com/eddyizm/tempus/pull/510
|
||||||
|
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/516
|
||||||
|
* refactor: navigation and bottom sheet by @tvillega in https://github.com/eddyizm/tempus/pull/491
|
||||||
|
* feat: Logo refresh by @eddyizm in https://github.com/eddyizm/tempus/pull/498
|
||||||
|
* feat: Add 'genres' page/function to Android Auto by @Jorilx in https://github.com/eddyizm/tempus/pull/505
|
||||||
|
* feat: Added all-songs feature by @unknown0816 in https://github.com/eddyizm/tempus/pull/517
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @NikkoFox made their first contribution in https://github.com/eddyizm/tempus/pull/503
|
||||||
|
* @olivertzeng made their first contribution in https://github.com/eddyizm/tempus/pull/494
|
||||||
|
* @Jorilx made their first contribution in https://github.com/eddyizm/tempus/pull/505
|
||||||
|
* @unknown0816 made their first contribution in https://github.com/eddyizm/tempus/pull/517
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.6...v4.13.0
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.12.6](https://github.com/eddyizm/tempo/releases/tag/v4.12.6) (2026-03-06)
|
||||||
|
* doc: update USAGE with android auto configuration by @MaFo-28 in https://github.com/eddyizm/tempus/pull/481
|
||||||
|
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/483
|
||||||
|
* fix: remove material you dynamic theming by @tvillega in https://github.com/eddyizm/tempus/pull/484
|
||||||
|
* fix: collapse sheet on navitation change by @tvillega in https://github.com/eddyizm/tempus/pull/482
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.4...v4.12.5
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.12.4](https://github.com/eddyizm/tempo/releases/tag/v4.12.4) (2026-03-01)
|
||||||
|
* feat: advertise existing long press to refresh per section on library page by @tvillega in https://github.com/eddyizm/tempus/pull/467
|
||||||
|
* fix: playlist filter returns properly filtered list and reset correctly by @eddyizm in https://github.com/eddyizm/tempus/pull/476
|
||||||
|
* feat: toggle player bitrate visibility on touch by @tvillega in https://github.com/eddyizm/tempus/pull/466
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.12.0...v4.12.3
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.12.0](https://github.com/eddyizm/tempo/releases/tag/v4.12.0) (2026-02-28)
|
||||||
|
* chore(i18n): Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/441
|
||||||
|
* feat: radio logos support for AndroidAuto by @dmachard in https://github.com/eddyizm/tempus/pull/435
|
||||||
|
* feat: Port remove song of playlist from tempus ng by @tvillega in https://github.com/eddyizm/tempus/pull/457
|
||||||
|
* fix: artist sort by name case sensitive by @tvillega in https://github.com/eddyizm/tempus/pull/462
|
||||||
|
* feat: added slide out enhanced navigation for tab mode and optionally portrait mode by @tvillega in https://github.com/eddyizm/tempus/pull/450
|
||||||
|
* feat: Android Auto: improve media service browsing by @MaFo-28 in https://github.com/eddyizm/tempus/pull/437
|
||||||
|
* feat: Support specifying a client certificate for mTLS auth by @tinsukE in https://github.com/eddyizm/tempus/pull/458
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @MaFo-28 made their first contribution in https://github.com/eddyizm/tempus/pull/437
|
||||||
|
* @tinsukE made their first contribution in https://github.com/eddyizm/tempus/pull/458
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.11.0...v4.12.0
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
## [4.11.0](https://github.com/eddyizm/tempo/releases/tag/v4.11.0) (2026-02-15)
|
||||||
|
* fix: added dynamic application id from gradle variant by @eddyizm in https://github.com/eddyizm/tempus/pull/425
|
||||||
|
* fix: Use Bluetooth tethering connection by @jaime-grj in https://github.com/eddyizm/tempus/pull/428
|
||||||
|
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/427
|
||||||
|
* fix: visual glitches on landscape navbar by @tvillega in https://github.com/eddyizm/tempus/pull/429
|
||||||
|
* fix: radio playback "source error" on android auto by @dmachard in https://github.com/eddyizm/tempus/pull/426
|
||||||
|
* fix: speed button overlaps with shuffle on landscape by @tvillega in https://github.com/eddyizm/tempus/pull/430
|
||||||
|
* fix: local url used in share link instead of server url by @tvillega in https://github.com/eddyizm/tempus/pull/431
|
||||||
|
* Feat :prefer downloaded files by @eddyizm in https://github.com/eddyizm/tempus/pull/433
|
||||||
|
* fix: radio metadata displayed by @TrackArcher in https://github.com/eddyizm/tempus/pull/352
|
||||||
|
* feat: improve playlist chooser dialog UI by @tvillega in https://github.com/eddyizm/tempus/pull/439
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* @dmachard made their first contribution in https://github.com/eddyizm/tempus/pull/426
|
||||||
|
* @TrackArcher made their first contribution in https://github.com/eddyizm/tempus/pull/352
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.10.1...v4.11.0
|
||||||
|
|
||||||
|
## 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)
|
## [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: 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
|
* 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
|
* fix: checks preference and writes files externally, updates the ui by @eddyizm in https://github.com/eddyizm/tempus/pull/312
|
||||||
@@ -14,6 +164,7 @@
|
|||||||
* @tyren234 made their first contribution in https://github.com/eddyizm/tempus/pull/307
|
* @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
|
* @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)
|
## [4.5.0](https://github.com/eddyizm/tempo/releases/tag/v4.5.0) (2025-12-12)
|
||||||
## What's Changed
|
## What's Changed
|
||||||
|
|||||||
66
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Tempus" title="Tempus" src="mockup/svg/tempus_horizontal_logo.png" width="250">
|
<img alt="Tempor" title="Tempor" src="mockup/svg/tempus-horizontal-banner.png" width="250">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -10,28 +10,38 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<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 -->
|
<!-- 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://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>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<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://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://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://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>
|
</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>
|
<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>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
**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.
|
**Tempor** 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.
|
Tempor 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).
|
The project is a fork of [Tempo](#credits).
|
||||||
|
|
||||||
**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.**
|
[Changelog](CHANGELOG.md)
|
||||||
|
[Wiki](USAGE.md)
|
||||||
|
[Donate](https://github.com/eddyizm/tempus#donate)
|
||||||
|
|
||||||
|
**If you find Tempor 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.**
|
**Use the Github version of the app for full Android Auto and Chromecast support.**
|
||||||
|
|
||||||
@@ -46,29 +56,27 @@ 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.
|
`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
|
## Features
|
||||||
- **Subsonic Integration**: Tempus seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
|
- **Subsonic Integration**: Tempor seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
|
||||||
- **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history.
|
- **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history.
|
||||||
- **Browse and Search**: Easily navigate through your music library using various browsing and searching options, including artists, albums, genres, playlists, decades and more.
|
- **Browse and Search**: Easily navigate through your music library using various browsing and searching options, including artists, albums, genres, playlists, decades and more.
|
||||||
- **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.
|
- **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.
|
- **Playlist Management**: Create, edit, and manage playlists to curate your perfect music collection.
|
||||||
- **Gapless Playback**: Experience uninterrupted playback with gapless listening mode.
|
- **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.
|
- **Scrobbling Integration**: Optionally integrate Tempor 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.
|
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempor, 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.
|
- **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.
|
- **Multiple Libraries**: Tempor handles multi-library setups gracefully. They are displayed as Library folders.
|
||||||
- **Equalizer**: Option to use in app equalizer.
|
- **Equalizer**: Option to use in app equalizer.
|
||||||
- **Widget**: New widget to keeping the basic controls on your screen at all times.
|
- **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
|
- **Available in 11 languages**: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish
|
||||||
|
|
||||||
|
**Github version only*
|
||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -76,13 +84,13 @@ Please note the two variants in the release assets include release/debug and 32/
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_light.png" width=200>
|
<img src="mockup/1_light_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_light.png" width=200>
|
<img src="mockup/2_light_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_light.png" width=200>
|
<img src="mockup/3_light_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_light.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_light.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_light.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_light.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_light.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_light.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_light.png" width=200>
|
<!-- <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_light.png" width=200> -->
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
@@ -92,13 +100,13 @@ Please note the two variants in the release assets include release/debug and 32/
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_dark.png" width=200>
|
<img src="mockup/1_dark_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_dark.png" width=200>
|
<img src="mockup/2_dark_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_dark.png" width=200>
|
<img src="mockup/3_dark_tempus.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_dark.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_dark.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_dark.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_dark.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_dark.png" width=200>
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_dark.png" width=200>
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_dark.png" width=200>
|
<!-- <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_dark.png" width=200> -->
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -112,16 +120,20 @@ 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.
|
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)
|
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
|
||||||
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
|
bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5`
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
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.
|
Tempor 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
|
## Credits
|
||||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
|
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.
|
[SeattleGuy](https://github.com/SeattleGuy) for the new logo design.
|
||||||
|
|||||||
102
USAGE.md
@@ -1,4 +1,4 @@
|
|||||||
# Tempus Usage Guide
|
# Tempor Usage Guide
|
||||||
[<- back home](README.md)
|
[<- back home](README.md)
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
@@ -6,14 +6,13 @@
|
|||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Server Configuration](#server-configuration)
|
- [Server Configuration](#server-configuration)
|
||||||
- [Main Features](#main-features)
|
- [Main Features](#main-features)
|
||||||
|
|
||||||
- [Navigation](#navigation)
|
- [Navigation](#navigation)
|
||||||
- [Playback Controls](#playback-controls)
|
- [Playback Controls](#playback-controls)
|
||||||
- [Favorites](#favorites)
|
- [Favorites](#favorites)
|
||||||
- [Playlist Management](#playlist-management)
|
- [Playlist Management](#playlist-management)
|
||||||
- [Android Auto](#android-auto)
|
- [Android Auto](#android-auto)
|
||||||
- [Settings](#settings)
|
- [Settings](#settings)
|
||||||
- [Troubleshooting](#troubleshooting)
|
- [Known Issues](#known-issues)
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ This app works with any service that implements the Subsonic API, including:
|
|||||||
- [Gonic](https://github.com/sentriz/gonic)
|
- [Gonic](https://github.com/sentriz/gonic)
|
||||||
- [Ampache](https://github.com/ampache/ampache)
|
- [Ampache](https://github.com/ampache/ampache)
|
||||||
- [NextCloud Music](https://apps.nextcloud.com/apps/music)
|
- [NextCloud Music](https://apps.nextcloud.com/apps/music)
|
||||||
|
- [Airsonic Advanced](https://github.com/kagemomiji/airsonic-advanced)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -63,15 +62,15 @@ This app works with any service that implements the Subsonic API, including:
|
|||||||
|
|
||||||
**Multi-library**
|
**Multi-library**
|
||||||
|
|
||||||
Tempus handles multi-library setups gracefully. They are displayed as Library folders.
|
Tempor handles multi-library setups gracefully. They are displayed as Library folders.
|
||||||
|
|
||||||
However, if you want to limit or change libraries you could use a workaround, if your server supports it.
|
However, if you want to limit or change libraries you could use a workaround, if your server supports it.
|
||||||
|
|
||||||
You can create multiple users , one for each library, and save each of them in Tempus app.
|
You can create multiple users , one for each library, and save each of them in Tempor.
|
||||||
|
|
||||||
### Folder or index playback
|
### 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:
|
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), Tempor lets you play an entire folder from anywhere in the library hierarchy:
|
||||||
|
|
||||||
<p align="left">
|
<p align="left">
|
||||||
<img src="mockup/usage/music_folders_root.png" width=317 style="margin-right:16px;">
|
<img src="mockup/usage/music_folders_root.png" width=317 style="margin-right:16px;">
|
||||||
@@ -82,7 +81,7 @@ If your Subsonic-compatible server exposes the folder tree **or** provides an ar
|
|||||||
- 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.
|
- 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.
|
- 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.
|
No extra config is needed—Tempor adjusts based on the connected backend.
|
||||||
|
|
||||||
### Now Playing Screen
|
### Now Playing Screen
|
||||||
|
|
||||||
@@ -159,9 +158,81 @@ If your server supports it - add a internet radio station feed
|
|||||||
|
|
||||||
## Android Auto
|
## Android Auto
|
||||||
|
|
||||||
### Enabling on your head unit
|
**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 Tempor app on your car's head unit, "Unknown sources" needs to be enabled in the Android Auto "Developer settings". This is because Tempor 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>
|
||||||
|
|
||||||
|
**Interface Configuration**
|
||||||
|
|
||||||
|
The Android Auto interface can be configured by user to best suit their preferences.
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/aa_preferences.png" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/aa_functions.png" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
4 tabs can be configured with the following functions:
|
||||||
|
- Do not display : This tab is not used
|
||||||
|
- Home : Displays all functions not used in other tabs
|
||||||
|
- Recent : The 15 most recently listened-to albums
|
||||||
|
- Albums : Albums sorted by name
|
||||||
|
- Artists : Albums sorted by artist
|
||||||
|
- Playlists
|
||||||
|
- Podcast : The 100 podcasts recently added
|
||||||
|
- Radio
|
||||||
|
- Folder : Navigation through music directories
|
||||||
|
- Albums most played : The 15 most played albums
|
||||||
|
- Albums added : The 15 recently added albums
|
||||||
|
- Star tracks
|
||||||
|
- Star albums
|
||||||
|
- Star artists
|
||||||
|
- Random : 100 random songs
|
||||||
|
- Genres : 500 songs of the chosen genre OR 100 random songs if "shuffle genre songs" is selected
|
||||||
|
|
||||||
|
If all tabs are set to "Do not display", then "Home" tab will be created with all functions inside.
|
||||||
|
|
||||||
|
If "Home" is selected after another tab, it becomes "More"
|
||||||
|
|
||||||
|
In addition, you can choose to display the following functions as thumbnails or lists:
|
||||||
|
- Home
|
||||||
|
- Albums (Last played, Most played, Recently added, Artists, Star tracks, Star albums, Star artists, Random)
|
||||||
|
- Playlists
|
||||||
|
- Radio
|
||||||
|
- Podcast
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/aa_thumbnails.jpg" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/aa_list.jpg" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The A-Z button allows you to jump to items starting with the chosen letter.
|
||||||
|
|
||||||
|
Search button returns albums or artists, even if they are not displayed by the selected function.
|
||||||
|
|
||||||
|
Results of the A-Z jump or search will always be displayed as a list.
|
||||||
|
|
||||||
|
<p align="left">
|
||||||
|
<img src="mockup/usage/aa_AZ.jpg" width=317 style="margin-right:16px;">
|
||||||
|
<img src="mockup/usage/aa_search.jpg" width=317>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Display of albums and artists is limited to 500. For large libraries, it's preferable to use star albums or star artists.
|
||||||
|
|
||||||
### Server Settings
|
### Server Settings
|
||||||
**IN PROGRESS**
|
**IN PROGRESS**
|
||||||
@@ -177,15 +248,12 @@ If your server supports it - add a internet radio station feed
|
|||||||
### Appearance
|
### Appearance
|
||||||
**TODO**
|
**TODO**
|
||||||
|
|
||||||
## Troubleshooting
|
## Known Issues
|
||||||
|
|
||||||
### Connection Issues
|
### Airsonic Distorted Playback
|
||||||
|
|
||||||
**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.
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**TODO**
|
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
For additional help:
|
For additional help:
|
||||||
|
|||||||
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
markdown: GFM
|
||||||
@@ -10,8 +10,8 @@ android {
|
|||||||
minSdkVersion 24
|
minSdkVersion 24
|
||||||
targetSdk 35
|
targetSdk 35
|
||||||
|
|
||||||
versionCode 11
|
versionCode 24
|
||||||
versionName '4.6.0'
|
versionName '4.13.0'
|
||||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||||
|
|
||||||
javaCompileOptions {
|
javaCompileOptions {
|
||||||
@@ -47,12 +47,12 @@ android {
|
|||||||
productFlavors {
|
productFlavors {
|
||||||
tempus {
|
tempus {
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
applicationId 'com.eddyizm.tempus'
|
applicationId 'ru.benya.tempor'
|
||||||
}
|
}
|
||||||
|
|
||||||
degoogled {
|
degoogled {
|
||||||
dimension = "default"
|
dimension = "default"
|
||||||
applicationId "com.eddyizm.degoogled.tempus"
|
applicationId "ru.benya.tempor.degoogled"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,7 @@ dependencies {
|
|||||||
implementation 'androidx.room:room-runtime:2.6.1'
|
implementation 'androidx.room:room-runtime:2.6.1'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0"
|
||||||
|
|
||||||
// Android Material
|
// Android Material
|
||||||
implementation 'com.google.android.material:material:1.10.0'
|
implementation 'com.google.android.material:material:1.10.0'
|
||||||
|
|||||||
1164
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/14.json
Normal file
|
Before Width: | Height: | Size: 20 KiB |
10
app/src/degoogled/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF36C12C"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
</vector>
|
||||||
@@ -1,54 +1,78 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="108">
|
||||||
<group android:scaleX="0.49"
|
|
||||||
android:scaleY="0.49"
|
|
||||||
android:translateX="130.56"
|
|
||||||
android:translateY="130.56">
|
|
||||||
|
|
||||||
<path
|
<group
|
||||||
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
|
android:scaleX="0.13"
|
||||||
android:fillColor="#8CC152"/> <path
|
android:scaleY="0.13"
|
||||||
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
|
android:translateX="21.5"
|
||||||
android:fillColor="#62A43B"/> <path
|
android:translateY="21.5">
|
||||||
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
|
|
||||||
android:fillColor="#8CC152"/> <path
|
<path
|
||||||
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
<path
|
<gradient
|
||||||
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
|
android:startX="122.34"
|
||||||
android:fillColor="#E6E9ED"/>
|
android:startY="23.55"
|
||||||
<path
|
android:endX="377.69"
|
||||||
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
|
android:endY="465.83"
|
||||||
android:fillColor="#E6E9ED"/>
|
android:type="linear">
|
||||||
<path
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
android:fillColor="#E6E9ED"/>
|
</gradient>
|
||||||
<path
|
</aapt:attr>
|
||||||
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
|
</path>
|
||||||
android:fillColor="#CCD1D9"/>
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
android:fillColor="#434A54"/>
|
<aapt:attr name="android:fillColor">
|
||||||
<path
|
<gradient
|
||||||
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
|
android:startX="116.21"
|
||||||
android:fillColor="#656D78"/>
|
android:startY="67.61"
|
||||||
<path
|
android:endX="403.29"
|
||||||
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
|
android:endY="429.34"
|
||||||
android:fillColor="#656D78"/>
|
android:type="linear">
|
||||||
<path
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
android:fillColor="#FFCE54"/>
|
</gradient>
|
||||||
<path
|
</aapt:attr>
|
||||||
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
|
</path>
|
||||||
android:fillColor="#F6BB42"/>
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
android:fillColor="#434A54"/>
|
<aapt:attr name="android:fillColor">
|
||||||
<path
|
<gradient
|
||||||
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
|
android:startX="420.63"
|
||||||
android:fillColor="#434A54"/>
|
android:startY="403.74"
|
||||||
</group>
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#66FFFFFF"
|
||||||
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
@@ -1,53 +1,77 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="108"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="108">
|
||||||
<group android:scaleX="0.55"
|
|
||||||
android:scaleY="0.55"
|
<group
|
||||||
android:translateX="150.56"
|
android:scaleX="0.13"
|
||||||
android:translateY="150.56">
|
android:scaleY="0.13"
|
||||||
|
android:translateX="21.5"
|
||||||
|
android:translateY="21.5">
|
||||||
<path
|
<path
|
||||||
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
android:fillColor="#8CC152"/> <path
|
<aapt:attr name="android:fillColor">
|
||||||
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
|
<gradient
|
||||||
android:fillColor="#62A43B"/> <path
|
android:startX="122.34"
|
||||||
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
|
android:startY="23.55"
|
||||||
android:fillColor="#8CC152"/> <path
|
android:endX="377.69"
|
||||||
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
|
android:endY="465.83"
|
||||||
android:fillColor="#E6E9ED"/>
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="116.21"
|
||||||
|
android:startY="67.61"
|
||||||
|
android:endX="403.29"
|
||||||
|
android:endY="429.34"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
android:fillColor="#E6E9ED"/>
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
|
android:fillColor="#66FFFFFF"
|
||||||
android:fillColor="#CCD1D9"/>
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
<path
|
</group>
|
||||||
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
|
|
||||||
android:fillColor="#434A54"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
|
|
||||||
android:fillColor="#656D78"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
|
|
||||||
android:fillColor="#656D78"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
|
|
||||||
android:fillColor="#FFCE54"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
|
|
||||||
android:fillColor="#F6BB42"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
|
|
||||||
android:fillColor="#434A54"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
|
|
||||||
android:fillColor="#434A54"/>
|
|
||||||
</group>
|
|
||||||
</vector>
|
</vector>
|
||||||
77
app/src/degoogled/res/drawable/ic_toolbar_tempo.xml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="522">
|
||||||
|
<group
|
||||||
|
android:scaleX="1.0"
|
||||||
|
android:scaleY="1.0"
|
||||||
|
android:translateX="14.0"
|
||||||
|
android:translateY="14.0">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="122.34"
|
||||||
|
android:startY="23.55"
|
||||||
|
android:endX="377.69"
|
||||||
|
android:endY="465.83"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="116.21"
|
||||||
|
android:startY="67.61"
|
||||||
|
android:endX="403.29"
|
||||||
|
android:endY="429.34"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#66FFFFFF"
|
||||||
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
78
app/src/degoogled/res/drawable/logo.xml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
|
||||||
|
<group
|
||||||
|
android:scaleX="0.16"
|
||||||
|
android:scaleY="0.16"
|
||||||
|
android:translateX="14.0"
|
||||||
|
android:translateY="14.0">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250,0c138.07,0 250,111.93 250,250S388.07,500 250,500 0,388.07 0,250 111.93,0 250,0ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="122.34"
|
||||||
|
android:startY="23.55"
|
||||||
|
android:endX="377.69"
|
||||||
|
android:endY="465.83"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#FF36C12C" />
|
||||||
|
<item android:offset="1.0" android:color="#FF36C12C" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M250.41,20.5c126.89,0 229.75,102.86 229.75,229.75c0,126.89 -102.86,229.75 -229.75,229.75c-126.89,0 -229.75,-102.86 -229.75,-229.75C20.66,123.36 123.53,20.5 250.41,20.5ZM250.85,161.82c-49.09,0 -88.88,39.79 -88.88,88.88c0,49.09 39.79,88.88 88.88,88.88c49.09,0 88.88,-39.79 88.88,-88.88c0,-49.09 -39.79,-88.88 -88.88,-88.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="116.21"
|
||||||
|
android:startY="67.61"
|
||||||
|
android:endX="403.29"
|
||||||
|
android:endY="429.34"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#66060606" />
|
||||||
|
<item android:offset="1.0" android:color="#CC060606" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M453.23,307.8c-18.5,72.24 -73.8,129.26 -144.2,148.92l-36.39,-138.74c21.97,-7.21 39.22,-24.84 45.88,-47.06l134.71,36.88Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:pathData="M228.3,183.04c-21.73,7.15 -38.82,24.5 -45.62,46.39L47.5,192.42c18.5,-72.24 73.8,-129.26 144.2,-148.92l36.6,139.54Z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:startX="420.63"
|
||||||
|
android:startY="403.74"
|
||||||
|
android:endX="78.4"
|
||||||
|
android:endY="117.92"
|
||||||
|
android:type="linear">
|
||||||
|
<item android:offset="0.0" android:color="#33FFFFFF" />
|
||||||
|
<item android:offset="1.0" android:color="#4DFFFFFF" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#66FFFFFF"
|
||||||
|
android:pathData="M250.5,179.5c39.21,0 71,31.79 71,71s-31.79,71 -71,71s-71,-31.79 -71,-71s31.79,-71 71,-71ZM250,235c-8.28,0 -15,6.72 -15,15c0,8.28 6.72,15 15,15c8.28,0 15,-6.72 15,-15c0,-8.28 -6.72,-15 -15,-15Z" />
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background_tempor_b"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground_tempor_b"/>
|
||||||
</adaptive-icon>
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome_tempor_b"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.2 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#626A75</color>
|
|
||||||
</resources>
|
|
||||||
@@ -96,7 +96,12 @@
|
|||||||
android:resource="@xml/widget_info"/>
|
android:resource="@xml/widget_info"/>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name=".provider.AlbumArtContentProvider"
|
||||||
|
android:authorities="${applicationId}.albumart.provider"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 20 KiB |
@@ -11,6 +11,7 @@ import com.cappielloantonio.tempo.github.Github;
|
|||||||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||||
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
||||||
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
|
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
|
||||||
|
import com.cappielloantonio.tempo.util.ClientCertManager;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
@@ -31,6 +32,8 @@ public class App extends Application {
|
|||||||
instance = new App();
|
instance = new App();
|
||||||
context = getApplicationContext();
|
context = getApplicationContext();
|
||||||
preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
ClientCertManager.setupSslSocketFactory(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static App getInstance() {
|
public static App getInstance() {
|
||||||
@@ -55,6 +58,48 @@ public class App extends Application {
|
|||||||
}
|
}
|
||||||
return subsonic;
|
return subsonic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Subsonic getSubsonicPublicClientInstance(boolean override) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
If I do the shortcut that the IDE suggests:
|
||||||
|
SubsonicPreferences preferences = getSubsonicPreferences1();
|
||||||
|
During the chain of calls it will run the following:
|
||||||
|
String server = Preferences.getInUseServerAddress();
|
||||||
|
Which could return Local URL, causing issues like generating public shares with Local URL
|
||||||
|
|
||||||
|
To prevent this I just replicated the entire chain of functions here,
|
||||||
|
if you need a call to Subsonic using the Server (Public) URL use this function.
|
||||||
|
*/
|
||||||
|
|
||||||
|
String server = Preferences.getServer();
|
||||||
|
String username = Preferences.getUser();
|
||||||
|
String password = Preferences.getPassword();
|
||||||
|
String token = Preferences.getToken();
|
||||||
|
String salt = Preferences.getSalt();
|
||||||
|
boolean isLowSecurity = Preferences.isLowScurity();
|
||||||
|
|
||||||
|
SubsonicPreferences preferences = new SubsonicPreferences();
|
||||||
|
preferences.setServerUrl(server);
|
||||||
|
preferences.setUsername(username);
|
||||||
|
preferences.setAuthentication(password, token, salt, isLowSecurity);
|
||||||
|
|
||||||
|
if (subsonic == null || override) {
|
||||||
|
|
||||||
|
if (preferences.getAuthentication() != null) {
|
||||||
|
if (preferences.getAuthentication().getPassword() != null)
|
||||||
|
Preferences.setPassword(preferences.getAuthentication().getPassword());
|
||||||
|
if (preferences.getAuthentication().getToken() != null)
|
||||||
|
Preferences.setToken(preferences.getAuthentication().getToken());
|
||||||
|
if (preferences.getAuthentication().getSalt() != null)
|
||||||
|
Preferences.setSalt(preferences.getAuthentication().getSalt());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Subsonic(preferences);
|
||||||
|
}
|
||||||
|
|
||||||
public static Github getGithubClientInstance() {
|
public static Github getGithubClientInstance() {
|
||||||
if (github == null) {
|
if (github == null) {
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
|||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Database(
|
@Database(
|
||||||
version = 13,
|
version = 14,
|
||||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class, LyricsCache.class},
|
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)}
|
autoMigrations = {
|
||||||
|
@AutoMigration(from = 10, to = 11),
|
||||||
|
@AutoMigration(from = 11, to = 12),
|
||||||
|
@AutoMigration(from = 13, to = 14),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
@TypeConverters({DateConverters.class})
|
@TypeConverters({DateConverters.class})
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public interface PlaylistDao {
|
|||||||
@Query("SELECT * FROM playlist")
|
@Query("SELECT * FROM playlist")
|
||||||
LiveData<List<Playlist>> getAll();
|
LiveData<List<Playlist>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlist")
|
||||||
|
List<Playlist> getAllSync();
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
void insert(Playlist playlist);
|
void insert(Playlist playlist);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.cappielloantonio.tempo.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.annotation.Nullable
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
@@ -35,5 +34,8 @@ data class Server(
|
|||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
|
|
||||||
@ColumnInfo(name = "low_security", defaultValue = "false")
|
@ColumnInfo(name = "low_security", defaultValue = "false")
|
||||||
val isLowSecurity: Boolean
|
val isLowSecurity: Boolean,
|
||||||
|
|
||||||
|
@ColumnInfo(name = "client_cert")
|
||||||
|
val clientCert: String?,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.cappielloantonio.tempo.model
|
package com.cappielloantonio.tempo.model
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
@@ -13,6 +14,7 @@ import androidx.room.ColumnInfo
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest
|
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.Child
|
||||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
|
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
|
||||||
@@ -193,11 +195,20 @@ class SessionMediaItem() {
|
|||||||
title = internetRadioStation.name
|
title = internetRadioStation.name
|
||||||
streamUrl = internetRadioStation.streamUrl
|
streamUrl = internetRadioStation.streamUrl
|
||||||
type = Constants.MEDIA_TYPE_RADIO
|
type = Constants.MEDIA_TYPE_RADIO
|
||||||
|
|
||||||
|
val homePageUrl = internetRadioStation.homePageUrl
|
||||||
|
if (homePageUrl != null && homePageUrl.isNotEmpty() && MusicUtil.isImageUrl(homePageUrl)) {
|
||||||
|
val encodedUrl = android.util.Base64.encodeToString(
|
||||||
|
homePageUrl.toByteArray(java.nio.charset.StandardCharsets.UTF_8),
|
||||||
|
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP
|
||||||
|
)
|
||||||
|
coverArtId = "ir_$encodedUrl"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMediaItem(): MediaItem {
|
fun getMediaItem(): MediaItem {
|
||||||
val uri: Uri = getStreamUri()
|
val uri: Uri = getStreamUri()
|
||||||
val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize()))
|
val artworkUri = if (coverArtId != null) AlbumArtContentProvider.contentUri(coverArtId!!) else null
|
||||||
|
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
bundle.putString("id", id)
|
bundle.putString("id", id)
|
||||||
@@ -227,7 +238,7 @@ class SessionMediaItem() {
|
|||||||
bundle.putLong("starred", starred?.time ?: 0)
|
bundle.putLong("starred", starred?.time ?: 0)
|
||||||
bundle.putString("albumId", albumId)
|
bundle.putString("albumId", albumId)
|
||||||
bundle.putString("artistId", artistId)
|
bundle.putString("artistId", artistId)
|
||||||
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC)
|
bundle.putString("type", type)
|
||||||
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
|
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
|
||||||
bundle.putInt("originalWidth", originalWidth ?: 0)
|
bundle.putInt("originalWidth", originalWidth ?: 0)
|
||||||
bundle.putInt("originalHeight", originalHeight ?: 0)
|
bundle.putInt("originalHeight", originalHeight ?: 0)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.cappielloantonio.tempo.navigation;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
public class NavigationController {
|
||||||
|
|
||||||
|
NavigationHelper helper;
|
||||||
|
|
||||||
|
public NavigationController(@NonNull NavigationHelper helper) {
|
||||||
|
this.helper = helper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWithBottomSheetBehavior(BottomSheetBehavior<View> bottomSheetBehavior,
|
||||||
|
NavController navController) {
|
||||||
|
helper.syncWithBottomSheetBehavior(bottomSheetBehavior, navController);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNavbarVisibility(boolean visibility) {
|
||||||
|
helper.setBottomNavigationBarVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDrawerLock(boolean visibility) {
|
||||||
|
helper.setNavigationDrawerLock(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigationDrawerLocked() {
|
||||||
|
return helper.isNavigationDrawerLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleDrawerLockOnOrientation(AppCompatActivity activity) {
|
||||||
|
helper.toggleNavigationDrawerLockOnOrientationChange(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSystemBarsVisibility(AppCompatActivity activity, boolean visibility) {
|
||||||
|
helper.setSystemBarsVisibility(activity, visibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.cappielloantonio.tempo.navigation;
|
||||||
|
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat;
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.navigation.NavController;
|
||||||
|
import androidx.navigation.NavDestination;
|
||||||
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.navigation.ui.NavigationUI;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
import com.google.android.material.navigation.NavigationView;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
|
public class NavigationHelper {
|
||||||
|
/* UI components */
|
||||||
|
private BottomNavigationView bottomNavigationView;
|
||||||
|
private FrameLayout bottomNavigationViewFrame;
|
||||||
|
private DrawerLayout drawerLayout;
|
||||||
|
|
||||||
|
/* Navigation components */
|
||||||
|
private NavigationView navigationView;
|
||||||
|
private NavHostFragment navHostFragment;
|
||||||
|
|
||||||
|
/* States that need to be remembered */
|
||||||
|
// -- //
|
||||||
|
|
||||||
|
/* Private constructor */
|
||||||
|
public NavigationHelper(@NonNull BottomNavigationView bottomNavigationView,
|
||||||
|
@NonNull FrameLayout bottomNavigationViewFrame,
|
||||||
|
@NonNull DrawerLayout drawerLayout,
|
||||||
|
@NonNull NavigationView navigationView,
|
||||||
|
@NonNull NavHostFragment navHostFragment) {
|
||||||
|
this.bottomNavigationView = bottomNavigationView;
|
||||||
|
this.bottomNavigationViewFrame = bottomNavigationViewFrame;
|
||||||
|
this.drawerLayout = drawerLayout;
|
||||||
|
this.navigationView = navigationView;
|
||||||
|
this.navHostFragment = navHostFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWithBottomSheetBehavior(@NonNull BottomSheetBehavior<View> bottomSheetBehavior,
|
||||||
|
@NonNull NavController navController) {
|
||||||
|
navController.addOnDestinationChangedListener(
|
||||||
|
(controller, destination, arguments) -> {
|
||||||
|
// React to the user clicking one of these on bottom-navbar/drawer
|
||||||
|
boolean isTarget = isTargetDestination(destination);
|
||||||
|
int currentState = bottomSheetBehavior.getState();
|
||||||
|
|
||||||
|
if (isTarget && currentState == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NavigationUI.setupWithNavController(bottomNavigationView, navController);
|
||||||
|
NavigationUI.setupWithNavController(navigationView, navController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract(pure = true)
|
||||||
|
private static boolean isTargetDestination(NavDestination destination) {
|
||||||
|
int destId = destination.getId();
|
||||||
|
return destId == R.id.homeFragment ||
|
||||||
|
destId == R.id.libraryFragment ||
|
||||||
|
destId == R.id.downloadFragment ||
|
||||||
|
destId == R.id.albumCatalogueFragment ||
|
||||||
|
destId == R.id.artistCatalogueFragment ||
|
||||||
|
destId == R.id.genreCatalogueFragment ||
|
||||||
|
destId == R.id.playlistCatalogueFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Clean public methods
|
||||||
|
Removes the need to invoke the activity on the fragment
|
||||||
|
*/
|
||||||
|
|
||||||
|
public void setBottomNavigationBarVisibility(boolean visible) {
|
||||||
|
int visibility = visible
|
||||||
|
? View.VISIBLE
|
||||||
|
: View.GONE;
|
||||||
|
bottomNavigationView.setVisibility(visibility);
|
||||||
|
bottomNavigationViewFrame.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNavigationDrawerLock(boolean locked) {
|
||||||
|
int mode = locked
|
||||||
|
? DrawerLayout.LOCK_MODE_LOCKED_CLOSED
|
||||||
|
: DrawerLayout.LOCK_MODE_UNLOCKED;
|
||||||
|
drawerLayout.setDrawerLockMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigationDrawerLocked() {
|
||||||
|
return drawerLayout.getDrawerLockMode(navigationView) != DrawerLayout.LOCK_MODE_UNLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public void toggleNavigationDrawerLockOnOrientationChange(
|
||||||
|
AppCompatActivity activity) {
|
||||||
|
|
||||||
|
int orientation = activity.getResources().getConfiguration().orientation;
|
||||||
|
boolean isLandscape = orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||||
|
|
||||||
|
if (Preferences.getEnableDrawerOnPortrait()) {
|
||||||
|
setNavigationDrawerLock(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNavigationDrawerLock(!isLandscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
All of these are the "backward compatible" changes that don't break the assumption
|
||||||
|
that everything was defined on the activity and is gobally available
|
||||||
|
*/
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public BottomNavigationView getBottomNavigationView() {
|
||||||
|
return bottomNavigationView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public FrameLayout getBottomNavigationViewFrame() {
|
||||||
|
return bottomNavigationViewFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public DrawerLayout getDrawerLayout() {
|
||||||
|
return drawerLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Auxiliar functions, could be moved somewhere else
|
||||||
|
*/
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public void setSystemBarsVisibility(AppCompatActivity activity, boolean visibility) {
|
||||||
|
WindowInsetsControllerCompat insetsController;
|
||||||
|
Window window = activity.getWindow();
|
||||||
|
View decorView = window.getDecorView();
|
||||||
|
insetsController = new WindowInsetsControllerCompat(window, decorView);
|
||||||
|
|
||||||
|
if (visibility) {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, true);
|
||||||
|
insetsController.show(WindowInsetsCompat.Type.navigationBars());
|
||||||
|
insetsController.show(WindowInsetsCompat.Type.statusBars());
|
||||||
|
insetsController.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT);
|
||||||
|
} else {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false);
|
||||||
|
insetsController.hide(WindowInsetsCompat.Type.navigationBars());
|
||||||
|
insetsController.hide(WindowInsetsCompat.Type.statusBars());
|
||||||
|
insetsController.setSystemBarsBehavior(
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
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 android.util.Base64;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.cappielloantonio.tempo.BuildConfig;
|
||||||
|
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 = BuildConfig.APPLICATION_ID + ".albumart.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;
|
||||||
|
|
||||||
|
if (albumId != null && albumId.startsWith("ir_")) {
|
||||||
|
String encodedUrl = albumId.substring("ir_".length());
|
||||||
|
String decodedUrl = new String(Base64.decode(encodedUrl, Base64.URL_SAFE | Base64.NO_WRAP));
|
||||||
|
artworkUri = Uri.parse(decodedUrl);
|
||||||
|
} else {
|
||||||
|
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.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
|
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
|
||||||
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
@@ -204,40 +205,12 @@ public class AlbumRepository {
|
|||||||
return albumInfo;
|
return albumInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) {
|
public MutableLiveData<List<Child>> getInstantMix(AlbumID3 album, int count) {
|
||||||
Log.d("AlbumRepository", "Attempting getInstantMix for AlbumID: " + album.getId());
|
// Delegate to the centralized SongRepository
|
||||||
|
return new SongRepository().getInstantMix(album.getId(), SeedType.ALBUM, count);
|
||||||
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) {
|
|
||||||
|
|
||||||
List<Child> similarSongs = response.body().getSubsonicResponse().getSimilarSongs2().getSongs();
|
|
||||||
|
|
||||||
if (similarSongs == null) {
|
|
||||||
Log.w("AlbumRepository", "API successful but 'songs' list was NULL for AlbumID: " + album.getId());
|
|
||||||
} else {
|
|
||||||
songs.addAll(similarSongs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.onLoadMedia(songs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
|
||||||
callback.onLoadMedia(new ArrayList<>());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public MutableLiveData<List<Integer>> getDecades() {
|
public MutableLiveData<List<Integer>> getDecades() {
|
||||||
MutableLiveData<List<Integer>> decades = new MutableLiveData<>();
|
MutableLiveData<List<Integer>> decades = new MutableLiveData<>();
|
||||||
|
|
||||||
@@ -248,7 +221,7 @@ public class AlbumRepository {
|
|||||||
@Override
|
@Override
|
||||||
public void onLoadYear(int last) {
|
public void onLoadYear(int last) {
|
||||||
if (first != -1 && last != -1) {
|
if (first != -1 && last != -1) {
|
||||||
List<Integer> decadeList = new ArrayList();
|
List<Integer> decadeList = new ArrayList<>();
|
||||||
|
|
||||||
int startDecade = first - (first % 10);
|
int startDecade = first - (first % 10);
|
||||||
int lastDecade = last - (last % 10);
|
int lastDecade = last - (last % 10);
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import androidx.lifecycle.MutableLiveData;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -149,7 +151,7 @@ public class ArtistRepository {
|
|||||||
|
|
||||||
if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) {
|
if(response.body().getSubsonicResponse().getArtists() != null && response.body().getSubsonicResponse().getArtists().getIndices() != null) {
|
||||||
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
|
for (IndexID3 index : response.body().getSubsonicResponse().getArtists().getIndices()) {
|
||||||
if(index != null && index.getArtists() != null) {
|
if(index.getArtists() != null) {
|
||||||
artists.addAll(index.getArtists());
|
artists.addAll(index.getArtists());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -287,26 +289,8 @@ public class ArtistRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
|
public MutableLiveData<List<Child>> getInstantMix(ArtistID3 artist, int count) {
|
||||||
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
// Delegate to the centralized SongRepository
|
||||||
|
return new SongRepository().getInstantMix(artist.getId(), SeedType.ARTIST, count);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getRandomSong(ArtistID3 artist, int count) {
|
public MutableLiveData<List<Child>> getRandomSong(ArtistID3 artist, int count) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
|||||||
import com.cappielloantonio.tempo.model.Chronology;
|
import com.cappielloantonio.tempo.model.Chronology;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.model.SessionMediaItem;
|
import com.cappielloantonio.tempo.model.SessionMediaItem;
|
||||||
|
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
|
||||||
import com.cappielloantonio.tempo.service.DownloaderManager;
|
import com.cappielloantonio.tempo.service.DownloaderManager;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
@@ -34,6 +35,7 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Genre;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
@@ -67,10 +69,20 @@ public class AutomotiveRepository {
|
|||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getAlbumList2() != null && response.body().getSubsonicResponse().getAlbumList2().getAlbums() != null) {
|
||||||
List<AlbumID3> albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums();
|
List<AlbumID3> albums = response.body().getSubsonicResponse().getAlbumList2().getAlbums();
|
||||||
|
|
||||||
|
// add by MFO
|
||||||
|
// Hack for artist view
|
||||||
|
if("alphabeticalByArtist".equals(type))for(AlbumID3 album : albums){
|
||||||
|
String artistName = album.getArtist();
|
||||||
|
String albumName = album.getName();
|
||||||
|
album.setName(artistName);
|
||||||
|
album.setArtist(albumName);
|
||||||
|
}
|
||||||
|
// end add by MFO
|
||||||
|
|
||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (AlbumID3 album : albums) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(album.getName())
|
.setTitle(album.getName())
|
||||||
@@ -217,7 +229,7 @@ public class AutomotiveRepository {
|
|||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (AlbumID3 album : albums) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(album.getName())
|
.setTitle(album.getName())
|
||||||
@@ -272,7 +284,7 @@ public class AutomotiveRepository {
|
|||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (ArtistID3 artist : artists) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(artist.getName())
|
.setTitle(artist.getName())
|
||||||
@@ -397,7 +409,7 @@ public class AutomotiveRepository {
|
|||||||
List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren();
|
List<Child> children = response.body().getSubsonicResponse().getIndexes().getChildren();
|
||||||
|
|
||||||
for (Child song : children) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(song.getTitle())
|
.setTitle(song.getTitle())
|
||||||
@@ -451,7 +463,7 @@ public class AutomotiveRepository {
|
|||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (Child child : directory.getChildren()) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(child.getTitle())
|
.setTitle(child.getTitle())
|
||||||
@@ -550,7 +562,7 @@ public class AutomotiveRepository {
|
|||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (PodcastEpisode episode : episodes) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(episode.getTitle())
|
.setTitle(episode.getTitle())
|
||||||
@@ -604,20 +616,7 @@ public class AutomotiveRepository {
|
|||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (InternetRadioStation radioStation : radioStations) {
|
for (InternetRadioStation radioStation : radioStations) {
|
||||||
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
mediaItems.add(MappingUtil.mapInternetRadioStation(radioStation));
|
||||||
.setTitle(radioStation.getName())
|
|
||||||
.setIsBrowsable(false)
|
|
||||||
.setIsPlayable(true)
|
|
||||||
.setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
MediaItem mediaItem = new MediaItem.Builder()
|
|
||||||
.setMediaId(radioStation.getId())
|
|
||||||
.setMediaMetadata(mediaMetadata)
|
|
||||||
.setUri(radioStation.getStreamUrl())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
mediaItems.add(mediaItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInternetRadioStationsMetadata(radioStations);
|
setInternetRadioStationsMetadata(radioStations);
|
||||||
@@ -687,7 +686,7 @@ public class AutomotiveRepository {
|
|||||||
List<MediaItem> mediaItems = new ArrayList<>();
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
for (AlbumID3 album : albums) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(album.getName())
|
.setTitle(album.getName())
|
||||||
@@ -791,7 +790,7 @@ public class AutomotiveRepository {
|
|||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 20, 20, 20)
|
.search3(query, 20, 0, 20, 0, 20, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
@@ -800,7 +799,7 @@ public class AutomotiveRepository {
|
|||||||
|
|
||||||
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
|
if (response.body().getSubsonicResponse().getSearchResult3().getArtists() != null) {
|
||||||
for (ArtistID3 artist : response.body().getSubsonicResponse().getSearchResult3().getArtists()) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(artist.getName())
|
.setTitle(artist.getName())
|
||||||
@@ -822,7 +821,7 @@ public class AutomotiveRepository {
|
|||||||
|
|
||||||
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
|
if (response.body().getSubsonicResponse().getSearchResult3().getAlbums() != null) {
|
||||||
for (AlbumID3 album : response.body().getSubsonicResponse().getSearchResult3().getAlbums()) {
|
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()
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
.setTitle(album.getName())
|
.setTitle(album.getName())
|
||||||
@@ -954,6 +953,116 @@ public class AutomotiveRepository {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getGenres(String prefix) {
|
||||||
|
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
|
||||||
|
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getBrowsingClient()
|
||||||
|
.getGenres()
|
||||||
|
.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().getGenres() != null && response.body().getSubsonicResponse().getGenres().getGenres() != null) {
|
||||||
|
List<Genre> genres = response.body().getSubsonicResponse().getGenres().getGenres();
|
||||||
|
|
||||||
|
// Sort genres alphabetically by name
|
||||||
|
genres.sort((g1, g2) -> {
|
||||||
|
String name1 = g1.getGenre() != null ? g1.getGenre() : "";
|
||||||
|
String name2 = g2.getGenre() != null ? g2.getGenre() : "";
|
||||||
|
return name1.compareToIgnoreCase(name2);
|
||||||
|
});
|
||||||
|
|
||||||
|
List<MediaItem> mediaItems = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Genre genre : genres) {
|
||||||
|
MediaMetadata mediaMetadata = new MediaMetadata.Builder()
|
||||||
|
.setTitle(genre.getGenre())
|
||||||
|
.setIsBrowsable(true)
|
||||||
|
.setIsPlayable(false)
|
||||||
|
.setMediaType(MediaMetadata.MEDIA_TYPE_PLAYLIST)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
MediaItem mediaItem = new MediaItem.Builder()
|
||||||
|
.setMediaId(prefix + genre.getGenre())
|
||||||
|
.setMediaMetadata(mediaMetadata)
|
||||||
|
.setUri("")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
mediaItems.add(mediaItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
|
||||||
|
|
||||||
|
listenableFuture.set(libraryResult);
|
||||||
|
} else {
|
||||||
|
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
listenableFuture.setException(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return listenableFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getSongsByGenre(String genre, int count, boolean shuffle) {
|
||||||
|
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
|
||||||
|
|
||||||
|
Call<ApiResponse> call;
|
||||||
|
if (shuffle) {
|
||||||
|
call = App.getSubsonicClientInstance(false)
|
||||||
|
.getAlbumSongListClient()
|
||||||
|
.getRandomSongs(count, null, null, genre);
|
||||||
|
} else {
|
||||||
|
call = App.getSubsonicClientInstance(false)
|
||||||
|
.getAlbumSongListClient()
|
||||||
|
.getSongsByGenre(genre, count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
call.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
List<com.cappielloantonio.tempo.subsonic.models.Child> songs;
|
||||||
|
if (shuffle) {
|
||||||
|
songs = response.body().getSubsonicResponse().getRandomSongs() != null
|
||||||
|
? response.body().getSubsonicResponse().getRandomSongs().getSongs()
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
songs = response.body().getSubsonicResponse().getSongsByGenre() != null
|
||||||
|
? response.body().getSubsonicResponse().getSongsByGenre().getSongs()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songs != null) {
|
||||||
|
setChildrenMetadata(songs);
|
||||||
|
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(songs);
|
||||||
|
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
|
||||||
|
listenableFuture.set(libraryResult);
|
||||||
|
} else {
|
||||||
|
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
listenableFuture.setException(t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return listenableFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getSongsByGenre(String genre, int count) {
|
||||||
|
return getSongsByGenre(genre, count, false);
|
||||||
|
}
|
||||||
|
|
||||||
private static class GetMediaItemThreadSafe implements Runnable {
|
private static class GetMediaItemThreadSafe implements Runnable {
|
||||||
private final SessionMediaItemDao sessionMediaItemDao;
|
private final SessionMediaItemDao sessionMediaItemDao;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
import static android.provider.Settings.System.getString;
|
|
||||||
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
@@ -26,8 +26,45 @@ import retrofit2.Callback;
|
|||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class PlaylistRepository {
|
public class PlaylistRepository {
|
||||||
|
private static final MutableLiveData<Boolean> playlistUpdateTrigger = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public LiveData<Boolean> getPlaylistUpdateTrigger() {
|
||||||
|
return playlistUpdateTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyPlaylistChanged() {
|
||||||
|
playlistUpdateTrigger.postValue(true);
|
||||||
|
refreshAllPlaylists();
|
||||||
|
}
|
||||||
|
|
||||||
@androidx.media3.common.util.UnstableApi
|
@androidx.media3.common.util.UnstableApi
|
||||||
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
||||||
|
private static final MutableLiveData<List<Playlist>> allPlaylistsLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public LiveData<List<Playlist>> getAllPlaylists(LifecycleOwner owner) {
|
||||||
|
refreshAllPlaylists();
|
||||||
|
return allPlaylistsLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshAllPlaylists() {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylists()
|
||||||
|
.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().getPlaylists() != null) {
|
||||||
|
List<Playlist> playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||||
|
allPlaylistsLiveData.postValue(playlists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
||||||
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
@@ -107,27 +144,62 @@ public class PlaylistRepository {
|
|||||||
return playlistLiveData;
|
return playlistLiveData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
public interface AddToPlaylistCallback {
|
||||||
|
void onSuccess();
|
||||||
|
void onFailure();
|
||||||
|
void onAllSkipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic, AddToPlaylistCallback callback) {
|
||||||
|
android.util.Log.d("PlaylistRepository", "addSongToPlaylist: id=" + playlistId + ", songs=" + songsId);
|
||||||
if (songsId.isEmpty()) {
|
if (songsId.isEmpty()) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
if (callback != null) callback.onAllSkipped();
|
||||||
} else{
|
} else{
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
.updatePlaylist(playlistId, null, playlistVisibilityIsPublic, songsId, null)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
|
if (callback != null) callback.onSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
|
if (callback != null) callback.onFailure();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeSongFromPlaylist(String playlistId, int index, AddToPlaylistCallback callback) {
|
||||||
|
ArrayList<Integer> indexes = new ArrayList<>();
|
||||||
|
indexes.add(index);
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.updatePlaylist(playlistId, null, true, null, indexes)
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
|
if (callback != null) {
|
||||||
|
if (response.isSuccessful()) callback.onSuccess();
|
||||||
|
else callback.onFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
if (callback != null) callback.onFailure();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||||
|
addSongToPlaylist(playlistId, songsId, playlistVisibilityIsPublic, null);
|
||||||
|
}
|
||||||
|
|
||||||
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
@@ -135,7 +207,7 @@ public class PlaylistRepository {
|
|||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -148,20 +220,45 @@ public class PlaylistRepository {
|
|||||||
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.deletePlaylist(playlistId)
|
.updatePlaylist(playlistId, name, true, null, null)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
createPlaylist(null, name, songsId);
|
if (response.isSuccessful()) {
|
||||||
|
// After renaming, we need to handle the song list update.
|
||||||
|
// Subsonic doesn't have a "replace all songs" in updatePlaylist.
|
||||||
|
// So we might still need to recreate if the songs changed significantly,
|
||||||
|
// but if we just renamed, we should update the local pinned database.
|
||||||
|
updateLocalPinnedPlaylistName(playlistId, name);
|
||||||
|
notifyPlaylistChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If songsId is provided, we might want to re-sync them.
|
||||||
|
// For now, let's at least fix the name duplication issue.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
private void updateLocalPinnedPlaylistName(String id, String newName) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Playlist> pinned = playlistDao.getAllSync();
|
||||||
|
if (pinned != null) {
|
||||||
|
for (Playlist p : pinned) {
|
||||||
|
if (p.getId().equals(id)) {
|
||||||
|
p.setName(newName);
|
||||||
|
playlistDao.insert(p); // Replace strategy will update it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
public void deletePlaylist(String playlistId) {
|
public void deletePlaylist(String playlistId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
@@ -169,7 +266,7 @@ public class PlaylistRepository {
|
|||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -197,6 +294,49 @@ public class PlaylistRepository {
|
|||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
|
public void updatePinnedPlaylists() {
|
||||||
|
updatePinnedPlaylists(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
|
public void updatePinnedPlaylists(List<String> forceIds) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Playlist> pinned = playlistDao.getAllSync();
|
||||||
|
if (pinned != null && !pinned.isEmpty()) {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylists()
|
||||||
|
.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().getPlaylists() != null) {
|
||||||
|
List<Playlist> remotes = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||||
|
new Thread(() -> {
|
||||||
|
for (Playlist p : pinned) {
|
||||||
|
for (Playlist r : remotes) {
|
||||||
|
if (p.getId().equals(r.getId())) {
|
||||||
|
p.setName(r.getName());
|
||||||
|
p.setSongCount(r.getSongCount());
|
||||||
|
p.setDuration(r.getDuration());
|
||||||
|
p.setCoverArtId(r.getCoverArtId());
|
||||||
|
playlistDao.insert(p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
private static class InsertThreadSafe implements Runnable {
|
private static class InsertThreadSafe implements Runnable {
|
||||||
private final PlaylistDao playlistDao;
|
private final PlaylistDao playlistDao;
|
||||||
private final Playlist playlist;
|
private final Playlist playlist;
|
||||||
|
|||||||
@@ -66,88 +66,33 @@ public class PodcastRepository {
|
|||||||
return liveNewestPodcastEpisodes;
|
return liveNewestPodcastEpisodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshPodcasts() {
|
public Call<ApiResponse> refreshPodcasts() {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getPodcastClient()
|
.getPodcastClient()
|
||||||
.refreshPodcasts()
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createPodcastChannel(String url) {
|
public Call<ApiResponse> createPodcastChannel(String url) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getPodcastClient()
|
.getPodcastClient()
|
||||||
.createPodcastChannel(url)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deletePodcastChannel(String channelId) {
|
public Call<ApiResponse> deletePodcastChannel(String channelId) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getPodcastClient()
|
.getPodcastClient()
|
||||||
.deletePodcastChannel(channelId)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deletePodcastEpisode(String episodeId) {
|
public Call<ApiResponse> deletePodcastEpisode(String episodeId) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getPodcastClient()
|
.getPodcastClient()
|
||||||
.deletePodcastEpisode(episodeId)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void downloadPodcastEpisode(String episodeId) {
|
public Call<ApiResponse> downloadPodcastEpisode(String episodeId) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getPodcastClient()
|
.getPodcastClient()
|
||||||
.downloadPodcastEpisode(episodeId)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,54 +38,22 @@ public class RadioRepository {
|
|||||||
return radioStation;
|
return radioStation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createInternetRadioStation(String name, String streamURL, String homepageURL) {
|
public Call<ApiResponse> createInternetRadioStation(String name, String streamURL, String homepageURL) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getInternetRadioClient()
|
.getInternetRadioClient()
|
||||||
.createInternetRadioStation(streamURL, name, homepageURL)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
|
public Call<ApiResponse> updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getInternetRadioClient()
|
.getInternetRadioClient()
|
||||||
.updateInternetRadioStation(id, streamURL, name, homepageURL)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteInternetRadioStation(String id) {
|
public Call<ApiResponse> deleteInternetRadioStation(String id) {
|
||||||
App.getSubsonicClientInstance(false)
|
return App.getSubsonicClientInstance(false)
|
||||||
.getInternetRadioClient()
|
.getInternetRadioClient()
|
||||||
.deleteInternetRadioStation(id)
|
.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) {
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||||
@@ -11,13 +16,18 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult2;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.ui.fragment.SearchFragment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -31,7 +41,7 @@ public class SearchingRepository {
|
|||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 20, 20, 20)
|
.search3(query, 20, 0, 20, 0, 20, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
@@ -49,12 +59,63 @@ public class SearchingRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<SearchResult3> search3(String query) {
|
@UnstableApi
|
||||||
|
public MutableLiveData<SearchResult3> search3(SearchFragment sf, String query) {
|
||||||
MutableLiveData<SearchResult3> result = new MutableLiveData<>();
|
MutableLiveData<SearchResult3> result = new MutableLiveData<>();
|
||||||
|
|
||||||
|
Executors.newSingleThreadExecutor().execute(() -> {
|
||||||
|
List<Child> allSongs = new ArrayList<>();
|
||||||
|
int offset = 0;
|
||||||
|
int limit = 1000;
|
||||||
|
boolean hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
try {
|
||||||
|
Response<ApiResponse> response = App.getSubsonicClientInstance(false)
|
||||||
|
.getSearchingClient()
|
||||||
|
.search3(query, limit, offset, 0, 0, 0, 0)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
SearchResult3 tmp = response.body().getSubsonicResponse().getSearchResult3();
|
||||||
|
if (tmp != null && tmp.getSongs() != null && !tmp.getSongs().isEmpty()) {
|
||||||
|
List<Child> fetchedSongs = tmp.getSongs();
|
||||||
|
allSongs.addAll(fetchedSongs);
|
||||||
|
|
||||||
|
offset += fetchedSongs.size();
|
||||||
|
hasMore = fetchedSongs.size() == limit;
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
hasMore = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlaylistWithSongs pws = new PlaylistWithSongs("allsongs", allSongs);
|
||||||
|
pws.setName(sf.getView().getContext().getString(R.string.search_all_songs, String.valueOf(allSongs.size())));
|
||||||
|
pws.setSongCount(allSongs.size());
|
||||||
|
List<Playlist> lpws = new ArrayList<>();
|
||||||
|
lpws.add(pws);
|
||||||
|
long duration = 0;
|
||||||
|
for (Child song: allSongs) {
|
||||||
|
if (song != null && song.getDuration() != null) {
|
||||||
|
duration += song.getDuration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pws.setDuration(duration);
|
||||||
|
|
||||||
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
|
sf.updateUI(lpws);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 20, 20, 20)
|
.search3(query, 20, 0, 20, 0, 20, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
@@ -77,7 +138,7 @@ public class SearchingRepository {
|
|||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getSearchingClient()
|
.getSearchingClient()
|
||||||
.search3(query, 5, 5, 5)
|
.search3(query, 5, 0, 5, 0, 5, 0)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class SharingRepository {
|
|||||||
public MutableLiveData<Share> createShare(String id, String description, Long expires) {
|
public MutableLiveData<Share> createShare(String id, String description, Long expires) {
|
||||||
MutableLiveData<Share> share = new MutableLiveData<>();
|
MutableLiveData<Share> share = new MutableLiveData<>();
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicPublicClientInstance(false)
|
||||||
.getSharingClient()
|
.getSharingClient()
|
||||||
.createShare(id, description, expires)
|
.createShare(id, description, expires)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@@ -64,7 +64,7 @@ public class SharingRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateShare(String id, String description, Long expires) {
|
public void updateShare(String id, String description, Long expires) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicPublicClientInstance(false)
|
||||||
.getSharingClient()
|
.getSharingClient()
|
||||||
.updateShare(id, description, expires)
|
.updateShare(id, description, expires)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
|||||||
@@ -1,23 +1,43 @@
|
|||||||
package com.cappielloantonio.tempo.repository;
|
package com.cappielloantonio.tempo.repository;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Directory;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Index;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Indexes;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.SearchResult3;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants.SeedType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class SongRepository {
|
public class SongRepository {
|
||||||
|
|
||||||
private static final String TAG = "SongRepository";
|
private static final String TAG = "SongRepository";
|
||||||
|
|
||||||
|
public interface MediaCallbackInternal {
|
||||||
|
void onSongsAvailable(List<Child> songs);
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
|
public MutableLiveData<List<Child>> getStarredSongs(boolean random, int size) {
|
||||||
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
|
MutableLiveData<List<Child>> starredSongs = new MutableLiveData<>(Collections.emptyList());
|
||||||
|
|
||||||
@@ -42,25 +62,216 @@ public class SongRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return starredSongs;
|
return starredSongs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
|
public MutableLiveData<List<Child>> getAllSongs() {
|
||||||
|
MutableLiveData<List<Child>> allSongs = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
|
Executors.newSingleThreadExecutor().execute(() -> {
|
||||||
|
List<Child> songs = fetchAllSongsViaSearch();
|
||||||
|
if (songs.isEmpty()) {
|
||||||
|
songs = fetchAllSongsViaBrowsing();
|
||||||
|
}
|
||||||
|
allSongs.postValue(songs);
|
||||||
|
});
|
||||||
|
|
||||||
|
return allSongs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<>();
|
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getBrowsingClient()
|
.getBrowsingClient()
|
||||||
.getSimilarSongs2(id, count)
|
.getSimilarSongs(id, count)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs2() != null) {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSimilarSongs() != null) {
|
||||||
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs2().getSongs());
|
instantMix.setValue(response.body().getSubsonicResponse().getSimilarSongs().getSongs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,188 +284,272 @@ public class SongRepository {
|
|||||||
return instantMix;
|
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) {
|
public MutableLiveData<List<Child>> getRandomSample(int number, Integer fromYear, Integer toYear) {
|
||||||
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getAlbumSongListClient()
|
List<Child> songs = new ArrayList<>();
|
||||||
.getRandomSongs(number, fromYear, toYear)
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||||
@Override
|
if (returned != null) {
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
songs.addAll(returned);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Override
|
randomSongsSample.setValue(songs);
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return randomSongsSample;
|
return randomSongsSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
|
public MutableLiveData<List<Child>> getRandomSampleWithGenre(int number, Integer fromYear, Integer toYear, String genre) {
|
||||||
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
MutableLiveData<List<Child>> randomSongsSample = new MutableLiveData<>();
|
||||||
|
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getRandomSongs(number, fromYear, toYear, genre).enqueue(new Callback<ApiResponse>() {
|
||||||
.getAlbumSongListClient()
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getRandomSongs(number, fromYear, toYear, genre)
|
List<Child> songs = new ArrayList<>();
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getRandomSongs() != null) {
|
||||||
@Override
|
List<Child> returned = response.body().getSubsonicResponse().getRandomSongs().getSongs();
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
if (returned != null) {
|
||||||
List<Child> songs = new ArrayList<>();
|
songs.addAll(returned);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Override
|
randomSongsSample.setValue(songs);
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return randomSongsSample;
|
return randomSongsSample;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void scrobble(String id, boolean submission) {
|
public void scrobble(String id, boolean submission) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false).getMediaAnnotationClient().scrobble(id, submission).enqueue(new Callback<ApiResponse>() {
|
||||||
.getMediaAnnotationClient()
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
|
||||||
.scrobble(id, submission)
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
.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) {
|
public void setRating(String id, int rating) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false).getMediaAnnotationClient().setRating(id, rating).enqueue(new Callback<ApiResponse>() {
|
||||||
.getMediaAnnotationClient()
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {}
|
||||||
.setRating(id, rating)
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
.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) {
|
public MutableLiveData<List<Child>> getSongsByGenre(String id, int page) {
|
||||||
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 100, 100 * page).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getAlbumSongListClient()
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||||
.getSongsByGenre(id, 100, 100 * page)
|
songsByGenre.setValue(response.body().getSubsonicResponse().getSongsByGenre().getSongs());
|
||||||
.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) {}
|
||||||
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;
|
return songsByGenre;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
|
public MutableLiveData<List<Child>> getSongsByGenres(ArrayList<String> genresId) {
|
||||||
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
MutableLiveData<List<Child>> songsByGenre = new MutableLiveData<>();
|
||||||
|
for (String id : genresId) {
|
||||||
for (String id : genresId)
|
App.getSubsonicClientInstance(false).getAlbumSongListClient().getSongsByGenre(id, 500, 0).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getAlbumSongListClient()
|
List<Child> songs = new ArrayList<>();
|
||||||
.getSongsByGenre(id, 500, 0)
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getSongsByGenre() != null) {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
List<Child> returned = response.body().getSubsonicResponse().getSongsByGenre().getSongs();
|
||||||
@Override
|
if (returned != null) {
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
songs.addAll(returned);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@Override
|
songsByGenre.setValue(songs);
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
}
|
||||||
|
@Override public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
return songsByGenre;
|
return songsByGenre;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<Child> getSong(String id) {
|
public MutableLiveData<Child> getSong(String id) {
|
||||||
MutableLiveData<Child> song = new MutableLiveData<>();
|
MutableLiveData<Child> song = new MutableLiveData<>();
|
||||||
|
App.getSubsonicClientInstance(false).getBrowsingClient().getSong(id).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getBrowsingClient()
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
.getSong(id)
|
song.setValue(response.body().getSubsonicResponse().getSong());
|
||||||
.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) {}
|
||||||
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;
|
return song;
|
||||||
}
|
}
|
||||||
|
|
||||||
public MutableLiveData<String> getSongLyrics(Child song) {
|
public MutableLiveData<String> getSongLyrics(Child song) {
|
||||||
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
|
MutableLiveData<String> lyrics = new MutableLiveData<>(null);
|
||||||
|
App.getSubsonicClientInstance(false).getMediaRetrievalClient().getLyrics(song.getArtist(), song.getTitle()).enqueue(new Callback<ApiResponse>() {
|
||||||
App.getSubsonicClientInstance(false)
|
@Override public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
.getMediaRetrievalClient()
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getLyrics() != null) {
|
||||||
.getLyrics(song.getArtist(), song.getTitle())
|
lyrics.setValue(response.body().getSubsonicResponse().getLyrics().getValue());
|
||||||
.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) {}
|
||||||
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;
|
return lyrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Child> fetchAllSongsViaSearch() {
|
||||||
|
LinkedHashMap<String, Child> songsById = new LinkedHashMap<>();
|
||||||
|
int offset = 0;
|
||||||
|
int limit = 500;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
Response<ApiResponse> response = App.getSubsonicClientInstance(false)
|
||||||
|
.getSearchingClient()
|
||||||
|
.search3("", limit, offset, 0, 0, 0, 0)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!response.isSuccessful() || response.body() == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchResult3 searchResult3 = response.body().getSubsonicResponse().getSearchResult3();
|
||||||
|
List<Child> batch = searchResult3 != null ? searchResult3.getSongs() : null;
|
||||||
|
if (batch == null || batch.isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Child child : batch) {
|
||||||
|
addPlayableChild(songsById, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += batch.size();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "fetchAllSongsViaSearch()", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<>(songsById.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Child> fetchAllSongsViaBrowsing() {
|
||||||
|
LinkedHashMap<String, Child> songsById = new LinkedHashMap<>();
|
||||||
|
Set<String> visitedDirectories = new HashSet<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Response<ApiResponse> musicFoldersResponse = App.getSubsonicClientInstance(false)
|
||||||
|
.getBrowsingClient()
|
||||||
|
.getMusicFolders()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (musicFoldersResponse.isSuccessful()
|
||||||
|
&& musicFoldersResponse.body() != null
|
||||||
|
&& musicFoldersResponse.body().getSubsonicResponse().getMusicFolders() != null
|
||||||
|
&& musicFoldersResponse.body().getSubsonicResponse().getMusicFolders().getMusicFolders() != null
|
||||||
|
&& !musicFoldersResponse.body().getSubsonicResponse().getMusicFolders().getMusicFolders().isEmpty()) {
|
||||||
|
for (MusicFolder musicFolder : musicFoldersResponse.body().getSubsonicResponse().getMusicFolders().getMusicFolders()) {
|
||||||
|
collectSongsFromIndexes(musicFolder.getId(), songsById, visitedDirectories);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
collectSongsFromIndexes(null, songsById, visitedDirectories);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "fetchAllSongsViaBrowsing()", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<>(songsById.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectSongsFromIndexes(String musicFolderId, LinkedHashMap<String, Child> songsById, Set<String> visitedDirectories) throws IOException {
|
||||||
|
Response<ApiResponse> indexesResponse = App.getSubsonicClientInstance(false)
|
||||||
|
.getBrowsingClient()
|
||||||
|
.getIndexes(musicFolderId, null)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!indexesResponse.isSuccessful() || indexesResponse.body() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Indexes indexes = indexesResponse.body().getSubsonicResponse().getIndexes();
|
||||||
|
if (indexes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexes.getChildren() != null) {
|
||||||
|
for (Child child : indexes.getChildren()) {
|
||||||
|
if (child == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (child.isDir()) {
|
||||||
|
collectSongsFromDirectory(child.getId(), songsById, visitedDirectories);
|
||||||
|
} else {
|
||||||
|
addPlayableChild(songsById, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexes.getIndices() != null) {
|
||||||
|
for (Index index : indexes.getIndices()) {
|
||||||
|
if (index == null || index.getArtists() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (com.cappielloantonio.tempo.subsonic.models.Artist artist : index.getArtists()) {
|
||||||
|
if (artist != null && artist.getId() != null && !artist.getId().isEmpty()) {
|
||||||
|
collectSongsFromDirectory(artist.getId(), songsById, visitedDirectories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectSongsFromDirectory(String directoryId, LinkedHashMap<String, Child> songsById, Set<String> visitedDirectories) throws IOException {
|
||||||
|
if (directoryId == null || directoryId.isEmpty() || !visitedDirectories.add(directoryId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response<ApiResponse> directoryResponse = App.getSubsonicClientInstance(false)
|
||||||
|
.getBrowsingClient()
|
||||||
|
.getMusicDirectory(directoryId)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
if (!directoryResponse.isSuccessful() || directoryResponse.body() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Directory directory = directoryResponse.body().getSubsonicResponse().getDirectory();
|
||||||
|
if (directory == null || directory.getChildren() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Child child : directory.getChildren()) {
|
||||||
|
if (child == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (child.isDir()) {
|
||||||
|
collectSongsFromDirectory(child.getId(), songsById, visitedDirectories);
|
||||||
|
} else {
|
||||||
|
addPlayableChild(songsById, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addPlayableChild(LinkedHashMap<String, Child> songsById, Child child) {
|
||||||
|
if (child == null || child.getId() == null || child.isDir() || child.isVideo()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
songsById.putIfAbsent(child.getId(), child);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
import android.app.TaskStackBuilder
|
import android.app.TaskStackBuilder
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@@ -23,6 +24,9 @@ import androidx.media3.exoplayer.source.MediaSource
|
|||||||
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder
|
||||||
import androidx.media3.session.*
|
import androidx.media3.session.*
|
||||||
import androidx.media3.session.MediaSession.ControllerInfo
|
import androidx.media3.session.MediaSession.ControllerInfo
|
||||||
|
import androidx.media3.extractor.metadata.icy.IcyInfo
|
||||||
|
import androidx.media3.extractor.metadata.id3.TextInformationFrame
|
||||||
|
import androidx.media3.extractor.metadata.vorbis.VorbisComment
|
||||||
import com.cappielloantonio.tempo.R
|
import com.cappielloantonio.tempo.R
|
||||||
import com.cappielloantonio.tempo.repository.QueueRepository
|
import com.cappielloantonio.tempo.repository.QueueRepository
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||||
@@ -31,6 +35,14 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
|||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.ScheduledExecutorService
|
||||||
|
import java.util.concurrent.ScheduledFuture
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
private const val TAG = "BaseMediaService"
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
open class BaseMediaService : MediaLibraryService() {
|
open class BaseMediaService : MediaLibraryService() {
|
||||||
@@ -67,6 +79,13 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val radioHeaderCheckExecutor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
|
||||||
|
private var radioHeaderCheckScheduled = false
|
||||||
|
private var radioHeaderCheckFuture: ScheduledFuture<*>? = null
|
||||||
|
private val radioHeaderCheckRunnable = Runnable {
|
||||||
|
checkRadioHttpHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
open fun playerInitHook() {
|
open fun playerInitHook() {
|
||||||
@@ -81,7 +100,7 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateMediaItems(player: Player) {
|
fun updateMediaItems(player: Player) {
|
||||||
Log.d(javaClass.toString(), "update items")
|
Log.d(TAG, "update items")
|
||||||
val n = player.mediaItemCount
|
val n = player.mediaItemCount
|
||||||
val k = player.currentMediaItemIndex
|
val k = player.currentMediaItemIndex
|
||||||
val current = player.currentPosition
|
val current = player.currentPosition
|
||||||
@@ -117,20 +136,33 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
updateWidget(player)
|
updateWidget(player)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var lastRadioArtist: String? = null
|
||||||
|
private var lastRadioTitle: String? = null
|
||||||
|
|
||||||
fun initializePlayerListener(player: Player) {
|
fun initializePlayerListener(player: Player) {
|
||||||
player.addListener(object : Player.Listener {
|
player.addListener(object : Player.Listener {
|
||||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||||
Log.d(javaClass.toString(), "onMediaItemTransition" + player.currentMediaItemIndex)
|
Log.d(TAG, "onMediaItemTransition" + player.currentMediaItemIndex)
|
||||||
if (mediaItem == null) return
|
if (mediaItem == null) return
|
||||||
|
|
||||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restart header checks for radio streams when media item changes
|
||||||
|
val mediaType = mediaItem.mediaMetadata.extras?.getString("type")
|
||||||
|
if (mediaType == Constants.MEDIA_TYPE_RADIO && player.isPlaying) {
|
||||||
|
stopRadioHeaderChecks()
|
||||||
|
scheduleRadioHeaderChecks()
|
||||||
|
} else if (mediaType != Constants.MEDIA_TYPE_RADIO) {
|
||||||
|
stopRadioHeaderChecks()
|
||||||
|
}
|
||||||
|
|
||||||
updateWidget(player)
|
updateWidget(player)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTracksChanged(tracks: Tracks) {
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
Log.d(javaClass.toString(), "onTracksChanged " + player.currentMediaItemIndex)
|
Log.d(TAG, "onTracksChanged " + player.currentMediaItemIndex)
|
||||||
ReplayGainUtil.setReplayGain(player, tracks)
|
ReplayGainUtil.setReplayGain(player, tracks)
|
||||||
val currentMediaItem = player.currentMediaItem
|
val currentMediaItem = player.currentMediaItem
|
||||||
if (currentMediaItem != null) {
|
if (currentMediaItem != null) {
|
||||||
@@ -138,14 +170,19 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
if (item.mediaMetadata.extras != null)
|
if (item.mediaMetadata.extras != null)
|
||||||
MediaManager.scrobble(item, false)
|
MediaManager.scrobble(item, false)
|
||||||
|
|
||||||
if (player.nextMediaItemIndex == C.INDEX_UNSET)
|
if (player.nextMediaItemIndex == C.INDEX_UNSET) {
|
||||||
MediaManager.continuousPlay(player.currentMediaItem)
|
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) {
|
if (player is ExoPlayer) {
|
||||||
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
|
// https://stackoverflow.com/questions/56937283/exoplayer-shuffle-doesnt-reproduce-all-the-songs
|
||||||
if (MediaManager.justStarted.get()) {
|
if (MediaManager.justStarted.get()) {
|
||||||
Log.d(javaClass.toString(), "update shuffle order")
|
Log.d(TAG, "update shuffle order")
|
||||||
MediaManager.justStarted.set(false)
|
MediaManager.justStarted.set(false)
|
||||||
val shuffledList = IntArray(player.mediaItemCount) { i -> i }
|
val shuffledList = IntArray(player.mediaItemCount) { i -> i }
|
||||||
shuffledList.shuffle()
|
shuffledList.shuffle()
|
||||||
@@ -162,8 +199,98 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMetadata(metadata: Metadata) {
|
||||||
|
// Handle streaming metadata (ICY, ID3) for radio / streaming content
|
||||||
|
val currentItem = player.currentMediaItem ?: return
|
||||||
|
val extras = currentItem.mediaMetadata.extras
|
||||||
|
if (extras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return
|
||||||
|
|
||||||
|
var artist: String? = null
|
||||||
|
var title: String? = null
|
||||||
|
|
||||||
|
// Extract metadata from ICY/ID3/Vorbis
|
||||||
|
for (i in 0 until metadata.length()) {
|
||||||
|
when (val entry = metadata[i]) {
|
||||||
|
is IcyInfo -> {
|
||||||
|
entry.title?.let { icyTitle ->
|
||||||
|
val parts = icyTitle.split(" - ", limit = 2)
|
||||||
|
if (parts.size == 2) {
|
||||||
|
artist = parts[0].trim().ifEmpty { null }
|
||||||
|
title = parts[1].trim().ifEmpty { null }
|
||||||
|
} else {
|
||||||
|
title = icyTitle.trim().ifEmpty { null }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is TextInformationFrame -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val value = entry.value
|
||||||
|
when (entry.id) {
|
||||||
|
"TPE1" -> if (!value.isNullOrBlank()) artist = value
|
||||||
|
"TIT2" -> if (!value.isNullOrBlank()) title = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is VorbisComment -> {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val value = entry.value
|
||||||
|
when (entry.key) {
|
||||||
|
"ARTIST" -> if (!value.isNullOrBlank()) artist = value
|
||||||
|
"TITLE" -> if (!value.isNullOrBlank()) title = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||||
|
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||||
|
|
||||||
|
lastRadioArtist = artist
|
||||||
|
lastRadioTitle = title
|
||||||
|
|
||||||
|
// Stop HTTP header checks since we have embedded metadata
|
||||||
|
stopRadioHeaderChecks()
|
||||||
|
|
||||||
|
val currentIndex = player.currentMediaItemIndex
|
||||||
|
if (currentIndex == C.INDEX_UNSET) return
|
||||||
|
|
||||||
|
val metadataBuilder = currentItem.mediaMetadata.buildUpon()
|
||||||
|
val newExtras = Bundle(extras ?: Bundle())
|
||||||
|
|
||||||
|
// Store individual values in extras for UI
|
||||||
|
artist?.let { newExtras.putString("radioArtist", it) }
|
||||||
|
title?.let { newExtras.putString("radioTitle", it) }
|
||||||
|
|
||||||
|
// Get station name (preserve if already set)
|
||||||
|
val stationName = extras?.getString("stationName")
|
||||||
|
?: currentItem.mediaMetadata.title?.toString()
|
||||||
|
?: ""
|
||||||
|
if (stationName.isNotBlank()) {
|
||||||
|
newExtras.putString("stationName", stationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||||
|
val formattedTitle = when {
|
||||||
|
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||||
|
!title.isNullOrBlank() -> title
|
||||||
|
!artist.isNullOrBlank() -> artist
|
||||||
|
else -> stationName
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataBuilder.setTitle(formattedTitle)
|
||||||
|
if (stationName.isNotBlank()) {
|
||||||
|
metadataBuilder.setArtist(stationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
(player as? ExoPlayer)?.let { exo ->
|
||||||
|
exo.replaceMediaItem(currentIndex, currentItem.buildUpon()
|
||||||
|
.setMediaMetadata(metadataBuilder.setExtras(newExtras).build())
|
||||||
|
.build())
|
||||||
|
updateWidget(exo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
Log.d(javaClass.toString(), "onIsPlayingChanged " + player.currentMediaItemIndex)
|
Log.d(TAG, "onIsPlayingChanged " + player.currentMediaItemIndex)
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
MediaManager.setPlayingPausedTimestamp(
|
MediaManager.setPlayingPausedTimestamp(
|
||||||
player.currentMediaItem,
|
player.currentMediaItem,
|
||||||
@@ -174,14 +301,16 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
scheduleWidgetUpdates()
|
scheduleWidgetUpdates()
|
||||||
|
scheduleRadioHeaderChecks()
|
||||||
} else {
|
} else {
|
||||||
stopWidgetUpdates()
|
stopWidgetUpdates()
|
||||||
|
stopRadioHeaderChecks()
|
||||||
}
|
}
|
||||||
updateWidget(player)
|
updateWidget(player)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||||
Log.d(javaClass.toString(), "onPlaybackStateChanged")
|
Log.d(TAG, "onPlaybackStateChanged")
|
||||||
super.onPlaybackStateChanged(playbackState)
|
super.onPlaybackStateChanged(playbackState)
|
||||||
if (!player.hasNextMediaItem() &&
|
if (!player.hasNextMediaItem() &&
|
||||||
playbackState == Player.STATE_ENDED &&
|
playbackState == Player.STATE_ENDED &&
|
||||||
@@ -198,7 +327,7 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
newPosition: Player.PositionInfo,
|
newPosition: Player.PositionInfo,
|
||||||
reason: Int
|
reason: Int
|
||||||
) {
|
) {
|
||||||
Log.d(javaClass.toString(), "onPositionDiscontinuity")
|
Log.d(TAG, "onPositionDiscontinuity")
|
||||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||||
|
|
||||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||||
@@ -222,7 +351,7 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
override fun onAudioSessionIdChanged(audioSessionId: Int) {
|
||||||
Log.d(javaClass.toString(), "onAudioSessionIdChanged")
|
Log.d(TAG, "onAudioSessionIdChanged")
|
||||||
attachEqualizerIfPossible(audioSessionId)
|
attachEqualizerIfPossible(audioSessionId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -279,6 +408,8 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
releaseNetworkCallback()
|
releaseNetworkCallback()
|
||||||
equalizerManager.release()
|
equalizerManager.release()
|
||||||
stopWidgetUpdates()
|
stopWidgetUpdates()
|
||||||
|
stopRadioHeaderChecks()
|
||||||
|
radioHeaderCheckExecutor.shutdown()
|
||||||
releasePlayers()
|
releasePlayers()
|
||||||
mediaLibrarySession.release()
|
mediaLibrarySession.release()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
@@ -314,7 +445,7 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initializeMediaLibrarySession(player: Player) {
|
private fun initializeMediaLibrarySession(player: Player) {
|
||||||
Log.d(javaClass.toString(), "initializeMediaLibrarySession")
|
Log.d(TAG, "initializeMediaLibrarySession")
|
||||||
val sessionActivityPendingIntent =
|
val sessionActivityPendingIntent =
|
||||||
TaskStackBuilder.create(this).run {
|
TaskStackBuilder.create(this).run {
|
||||||
addNextIntent(Intent(baseContext, MainActivity::class.java))
|
addNextIntent(Intent(baseContext, MainActivity::class.java))
|
||||||
@@ -397,6 +528,148 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
widgetUpdateScheduled = false
|
widgetUpdateScheduled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun scheduleRadioHeaderChecks() {
|
||||||
|
val player = mediaLibrarySession.player
|
||||||
|
val currentItem = player.currentMediaItem ?: return
|
||||||
|
val mediaType = currentItem.mediaMetadata.extras?.getString("type")
|
||||||
|
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||||
|
|
||||||
|
if (radioHeaderCheckScheduled) return
|
||||||
|
|
||||||
|
// Check immediately, then periodically
|
||||||
|
checkRadioHttpHeaders()
|
||||||
|
radioHeaderCheckFuture = radioHeaderCheckExecutor.scheduleWithFixedDelay(
|
||||||
|
radioHeaderCheckRunnable,
|
||||||
|
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||||
|
RADIO_HEADER_CHECK_INTERVAL_SECONDS,
|
||||||
|
TimeUnit.SECONDS
|
||||||
|
)
|
||||||
|
radioHeaderCheckScheduled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopRadioHeaderChecks() {
|
||||||
|
if (!radioHeaderCheckScheduled) return
|
||||||
|
radioHeaderCheckFuture?.cancel(false)
|
||||||
|
radioHeaderCheckFuture = null
|
||||||
|
radioHeaderCheckScheduled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkRadioHttpHeaders() {
|
||||||
|
val player = mediaLibrarySession.player
|
||||||
|
val currentItem = player.currentMediaItem ?: return
|
||||||
|
val extras = currentItem.mediaMetadata.extras
|
||||||
|
val mediaType = extras?.getString("type")
|
||||||
|
if (mediaType != Constants.MEDIA_TYPE_RADIO) return
|
||||||
|
|
||||||
|
// Skip if we already have embedded metadata (ICY/ID3) - HTTP headers are only fallback
|
||||||
|
val hasEmbeddedMetadata = !currentItem.mediaMetadata.artist.isNullOrBlank() ||
|
||||||
|
!currentItem.mediaMetadata.title.isNullOrBlank() ||
|
||||||
|
(extras != null && !extras.getString("radioArtist").isNullOrBlank()) ||
|
||||||
|
(extras != null && !extras.getString("radioTitle").isNullOrBlank())
|
||||||
|
if (hasEmbeddedMetadata) return
|
||||||
|
|
||||||
|
val streamUrl = extras?.getString("uri") ?: currentItem.requestMetadata.mediaUri?.toString()
|
||||||
|
if (streamUrl.isNullOrBlank()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
val url = URL(streamUrl)
|
||||||
|
val connection = url.openConnection() as? HttpURLConnection ?: return
|
||||||
|
|
||||||
|
// Only try HEAD request (lightweight) - skip GET fallback as it's unreliable
|
||||||
|
connection.requestMethod = "HEAD"
|
||||||
|
connection.setRequestProperty("Icy-MetaData", "1")
|
||||||
|
connection.setRequestProperty("User-Agent", "Tempus/1.0")
|
||||||
|
connection.connectTimeout = 3000 // Reduced timeout
|
||||||
|
connection.readTimeout = 3000
|
||||||
|
|
||||||
|
connection.connect()
|
||||||
|
|
||||||
|
if (connection.responseCode >= 400) {
|
||||||
|
connection.disconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for metadata in HTTP headers
|
||||||
|
val streamTitle = connection.getHeaderField("icy-name")
|
||||||
|
?: connection.getHeaderField("StreamTitle")
|
||||||
|
?: connection.getHeaderField("stream-title")
|
||||||
|
|
||||||
|
connection.disconnect()
|
||||||
|
|
||||||
|
if (!streamTitle.isNullOrBlank()) {
|
||||||
|
processStreamTitle(streamTitle, player)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Silently fail - this is a fallback mechanism, ICY metadata is primary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun processStreamTitle(streamTitle: String, player: Player) {
|
||||||
|
// Parse "Artist - Title" format
|
||||||
|
val parts = streamTitle.split(" - ", limit = 2)
|
||||||
|
val artist = if (parts.size == 2) parts[0].trim().ifEmpty { null } else null
|
||||||
|
val title = if (parts.size == 2) parts[1].trim().ifEmpty { null } else streamTitle.trim().ifEmpty { null }
|
||||||
|
|
||||||
|
if (artist.isNullOrBlank() && title.isNullOrBlank()) return
|
||||||
|
if (artist == lastRadioArtist && title == lastRadioTitle) return // Deduplicate
|
||||||
|
|
||||||
|
lastRadioArtist = artist
|
||||||
|
lastRadioTitle = title
|
||||||
|
|
||||||
|
// Update on main thread
|
||||||
|
widgetUpdateHandler.post {
|
||||||
|
val currentItemNow = player.currentMediaItem ?: return@post
|
||||||
|
val currentIndex = player.currentMediaItemIndex
|
||||||
|
if (currentIndex == C.INDEX_UNSET) return@post
|
||||||
|
|
||||||
|
val currentExtras = currentItemNow.mediaMetadata.extras
|
||||||
|
if (currentExtras?.getString("type") != Constants.MEDIA_TYPE_RADIO) return@post
|
||||||
|
|
||||||
|
// Double-check we still don't have embedded metadata (might have arrived since check)
|
||||||
|
val hasEmbeddedMetadata = !currentItemNow.mediaMetadata.artist.isNullOrBlank() ||
|
||||||
|
!currentItemNow.mediaMetadata.title.isNullOrBlank() ||
|
||||||
|
(currentExtras != null && !currentExtras.getString("radioArtist").isNullOrBlank()) ||
|
||||||
|
(currentExtras != null && !currentExtras.getString("radioTitle").isNullOrBlank())
|
||||||
|
if (hasEmbeddedMetadata) return@post
|
||||||
|
|
||||||
|
val metadataBuilder = currentItemNow.mediaMetadata.buildUpon()
|
||||||
|
val newExtras = Bundle(currentExtras ?: Bundle())
|
||||||
|
|
||||||
|
// Store individual values in extras for UI
|
||||||
|
artist?.let { newExtras.putString("radioArtist", it) }
|
||||||
|
title?.let { newExtras.putString("radioTitle", it) }
|
||||||
|
|
||||||
|
// Get station name (preserve if already set)
|
||||||
|
val stationName = currentExtras?.getString("stationName")
|
||||||
|
?: currentItemNow.mediaMetadata.title?.toString()
|
||||||
|
?: ""
|
||||||
|
if (stationName.isNotBlank()) {
|
||||||
|
newExtras.putString("stationName", stationName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format for notification/player: Title = "Artist - Song", Artist = "Station Name"
|
||||||
|
val formattedTitle = when {
|
||||||
|
!artist.isNullOrBlank() && !title.isNullOrBlank() -> "$artist - $title"
|
||||||
|
!title.isNullOrBlank() -> title
|
||||||
|
!artist.isNullOrBlank() -> artist
|
||||||
|
else -> stationName
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataBuilder.setTitle(formattedTitle)
|
||||||
|
if (stationName.isNotBlank()) {
|
||||||
|
metadataBuilder.setArtist(stationName)
|
||||||
|
}
|
||||||
|
metadataBuilder.setExtras(newExtras)
|
||||||
|
|
||||||
|
(player as? ExoPlayer)?.let { exo ->
|
||||||
|
exo.replaceMediaItem(currentIndex, currentItemNow.buildUpon()
|
||||||
|
.setMediaMetadata(metadataBuilder.build())
|
||||||
|
.build())
|
||||||
|
updateWidget(exo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
private fun attachEqualizerIfPossible(audioSessionId: Int): Boolean {
|
||||||
if (audioSessionId == 0 || audioSessionId == -1) return false
|
if (audioSessionId == 0 || audioSessionId == -1) return false
|
||||||
val attached = equalizerManager.attachToSession(audioSessionId)
|
val attached = equalizerManager.attachToSession(audioSessionId)
|
||||||
@@ -461,7 +734,7 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
customCommand: SessionCommand,
|
customCommand: SessionCommand,
|
||||||
args: Bundle
|
args: Bundle
|
||||||
): ListenableFuture<SessionResult> {
|
): ListenableFuture<SessionResult> {
|
||||||
Log.d(javaClass.toString(), "onCustomCommand")
|
Log.d(TAG, "onCustomCommand")
|
||||||
when (customCommand.customAction) {
|
when (customCommand.customAction) {
|
||||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
|
||||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
|
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
|
||||||
@@ -486,7 +759,7 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
controller: ControllerInfo,
|
controller: ControllerInfo,
|
||||||
mediaItems: List<MediaItem>
|
mediaItems: List<MediaItem>
|
||||||
): ListenableFuture<List<MediaItem>> {
|
): ListenableFuture<List<MediaItem>> {
|
||||||
Log.d(javaClass.toString(), "onAddMediaItems")
|
Log.d(TAG, "onAddMediaItems")
|
||||||
val updatedMediaItems = mediaItems.map { mediaItem ->
|
val updatedMediaItems = mediaItems.map { mediaItem ->
|
||||||
val mediaMetadata = mediaItem.mediaMetadata
|
val mediaMetadata = mediaItem.mediaMetadata
|
||||||
val newMetadata = mediaMetadata.buildUpon()
|
val newMetadata = mediaMetadata.buildUpon()
|
||||||
@@ -587,4 +860,5 @@ open class BaseMediaService : MediaLibraryService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||||
|
private const val RADIO_HEADER_CHECK_INTERVAL_SECONDS = 30L // Reduced frequency - only fallback when ICY fails
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.cappielloantonio.tempo.repository.SongRepository;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
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.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
@@ -183,11 +184,13 @@ public class MediaManager {
|
|||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int startIndex) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
|
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
try {
|
try {
|
||||||
if (mediaBrowserListenableFuture.isDone()) {
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
final MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
final List<MediaItem> items = MappingUtil.mapMediaItems(media);
|
||||||
|
|
||||||
new Handler(Looper.getMainLooper()).post(() -> {
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
justStarted.set(true);
|
justStarted.set(true);
|
||||||
browser.setMediaItems(items, startIndex, 0);
|
browser.setMediaItems(items, startIndex, 0);
|
||||||
@@ -196,28 +199,31 @@ public class MediaManager {
|
|||||||
Player.Listener timelineListener = new Player.Listener() {
|
Player.Listener timelineListener = new Player.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
|
|
||||||
int itemCount = browser.getMediaItemCount();
|
int itemCount = browser.getMediaItemCount();
|
||||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||||
browser.seekTo(startIndex, 0);
|
browser.seekTo(startIndex, 0);
|
||||||
browser.play();
|
browser.play();
|
||||||
browser.removeListener(this);
|
browser.removeListener(this);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Cannot start playback: itemCount=" + itemCount + ", startIndex=" + startIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
browser.addListener(timelineListener);
|
browser.addListener(timelineListener);
|
||||||
});
|
});
|
||||||
|
|
||||||
backgroundExecutor.execute(() -> {
|
backgroundExecutor.execute(() -> {
|
||||||
|
Log.d(TAG, "Background: enqueuing to database");
|
||||||
enqueueDatabase(media, true, 0);
|
enqueueDatabase(media, true, 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
Log.e(TAG, "Error executing startQueue logic: " + e.getMessage(), e);
|
Log.e(TAG, "Error in startQueue: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}, MoreExecutors.directExecutor());
|
}, MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
public static void startQueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, Child media) {
|
||||||
@@ -438,28 +444,33 @@ public class MediaManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public static void continuousPlay(MediaItem mediaItem) {
|
public static void continuousPlay(MediaItem mediaItem,
|
||||||
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
|
ListenableFuture<MediaBrowser> existingBrowserFuture) {
|
||||||
Preferences.setLastInstantMix();
|
if (mediaItem == null
|
||||||
|
|| !Preferences.isContinuousPlayEnabled()
|
||||||
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
|
|| !Preferences.isInstantMixUsable()) {
|
||||||
instantMix.observeForever(new Observer<List<Child>>() {
|
return;
|
||||||
@Override
|
|
||||||
public void onChanged(List<Child> media) {
|
|
||||||
if (media != null) {
|
|
||||||
Log.e(TAG, "continuous play");
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
public static void saveChronology(MediaItem mediaItem) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.subsonic
|
|||||||
import com.cappielloantonio.tempo.App
|
import com.cappielloantonio.tempo.App
|
||||||
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
||||||
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
||||||
|
import com.cappielloantonio.tempo.util.ClientCertManager
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -13,7 +14,7 @@ import java.util.Date
|
|||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class RetrofitClient(subsonic: Subsonic) {
|
class RetrofitClient(subsonic: Subsonic) {
|
||||||
var retrofit: Retrofit
|
val retrofit: Retrofit
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val gson = GsonBuilder()
|
val gson = GsonBuilder()
|
||||||
@@ -50,6 +51,7 @@ class RetrofitClient(subsonic: Subsonic) {
|
|||||||
.addInterceptor(cacheUtil.offlineInterceptor)
|
.addInterceptor(cacheUtil.offlineInterceptor)
|
||||||
// .addNetworkInterceptor(cacheUtil.onlineInterceptor)
|
// .addNetworkInterceptor(cacheUtil.onlineInterceptor)
|
||||||
.cache(getCache())
|
.cache(getCache())
|
||||||
|
.setupSsl()
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,4 +65,11 @@ class RetrofitClient(subsonic: Subsonic) {
|
|||||||
val cacheSize = 10 * 1024 * 1024
|
val cacheSize = 10 * 1024 * 1024
|
||||||
return Cache(App.getContext().cacheDir, cacheSize.toLong())
|
return Cache(App.getContext().cacheDir, cacheSize.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun OkHttpClient.Builder.setupSsl(): OkHttpClient.Builder {
|
||||||
|
ClientCertManager.sslSocketFactory?.let { sslSocketFactory ->
|
||||||
|
sslSocketFactory(sslSocketFactory, ClientCertManager.trustManager)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -24,8 +24,8 @@ public class SearchingClient {
|
|||||||
return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount);
|
return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Call<ApiResponse> search3(String query, int songCount, int albumCount, int artistCount) {
|
public Call<ApiResponse> search3(String query, int songCount, int songOffset, int albumCount, int albumOffset, int artistCount, int artistOffset) {
|
||||||
Log.d(TAG, "search3()");
|
Log.d(TAG, "search3()");
|
||||||
return searchingService.search3(subsonic.getParams(), query, songCount, albumCount, artistCount);
|
return searchingService.search3(subsonic.getParams(), query, songCount, songOffset, albumCount, albumOffset, artistCount, artistOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ public interface SearchingService {
|
|||||||
Call<ApiResponse> search2(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
|
Call<ApiResponse> search2(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
|
||||||
|
|
||||||
@GET("search3")
|
@GET("search3")
|
||||||
Call<ApiResponse> search3(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
|
Call<ApiResponse> search3(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("songOffset") int songOffset, @Query("albumCount") int albumCount, @Query("albumOffset") int albumOffset, @Query("artistCount") int artistCount, @Query("artistOffset") int artistOffset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,15 @@ public class SystemClient {
|
|||||||
|
|
||||||
public Call<ApiResponse> ping() {
|
public Call<ApiResponse> ping() {
|
||||||
Log.d(TAG, "ping()");
|
Log.d(TAG, "ping()");
|
||||||
|
int timeoutSeconds = Preferences.getNetworkPingTimeout();
|
||||||
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
||||||
if (Preferences.isInUseServerAddressLocal()) {
|
if (Preferences.isInUseServerAddressLocal()) {
|
||||||
pingCall.timeout()
|
pingCall.timeout()
|
||||||
.timeout(1, TimeUnit.SECONDS);
|
.timeout(timeoutSeconds, TimeUnit.SECONDS);
|
||||||
} else {
|
} else {
|
||||||
|
int finalTimeout = Math.min(timeoutSeconds * 2, 10);
|
||||||
pingCall.timeout()
|
pingCall.timeout()
|
||||||
.timeout(3, TimeUnit.SECONDS);
|
.timeout(finalTimeout, TimeUnit.SECONDS);
|
||||||
}
|
}
|
||||||
return pingCall;
|
return pingCall;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.subsonic.models
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@@ -10,5 +11,6 @@ class InternetRadioStation(
|
|||||||
var id: String? = null,
|
var id: String? = null,
|
||||||
var name: String? = null,
|
var name: String? = null,
|
||||||
var streamUrl: String? = null,
|
var streamUrl: String? = null,
|
||||||
|
@SerializedName("homePageUrl", alternate = ["homepageUrl"])
|
||||||
var homePageUrl: String? = null,
|
var homePageUrl: String? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.cappielloantonio.tempo.subsonic.models
|
package com.cappielloantonio.tempo.subsonic.models
|
||||||
|
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
class SimilarSongs {
|
class SimilarSongs {
|
||||||
|
@SerializedName("song")
|
||||||
var songs: List<Child>? = null
|
var songs: List<Child>? = null
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,8 @@ public class CacheUtil {
|
|||||||
|
|
||||||
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH);
|
||||||
if (!hasAppropriateTransport) {
|
if (!hasAppropriateTransport) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,27 @@ package com.cappielloantonio.tempo.ui.activity;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.navigation.ui.NavigationUI;
|
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.BuildConfig;
|
import com.cappielloantonio.tempo.BuildConfig;
|
||||||
@@ -31,8 +31,12 @@ import com.cappielloantonio.tempo.R;
|
|||||||
import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver;
|
import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver;
|
||||||
import com.cappielloantonio.tempo.databinding.ActivityMainBinding;
|
import com.cappielloantonio.tempo.databinding.ActivityMainBinding;
|
||||||
import com.cappielloantonio.tempo.github.utils.UpdateUtil;
|
import com.cappielloantonio.tempo.github.utils.UpdateUtil;
|
||||||
|
import com.cappielloantonio.tempo.navigation.NavigationController;
|
||||||
|
import com.cappielloantonio.tempo.navigation.NavigationHelper;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.ui.activity.base.BaseActivity;
|
import com.cappielloantonio.tempo.ui.activity.base.BaseActivity;
|
||||||
|
import com.cappielloantonio.tempo.ui.controller.BottomSheetController;
|
||||||
|
import com.cappielloantonio.tempo.ui.controller.BottomSheetHelper;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
||||||
@@ -45,6 +49,7 @@ import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
|||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
import com.google.android.material.color.DynamicColors;
|
import com.google.android.material.color.DynamicColors;
|
||||||
|
import com.google.android.material.navigation.NavigationView;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -60,14 +65,24 @@ public class MainActivity extends BaseActivity {
|
|||||||
private FragmentManager fragmentManager;
|
private FragmentManager fragmentManager;
|
||||||
private NavHostFragment navHostFragment;
|
private NavHostFragment navHostFragment;
|
||||||
private BottomNavigationView bottomNavigationView;
|
private BottomNavigationView bottomNavigationView;
|
||||||
|
private FrameLayout bottomNavigationViewFrame;
|
||||||
|
private DrawerLayout drawerLayout;
|
||||||
|
private NavigationView navigationView;
|
||||||
public NavController navController;
|
public NavController navController;
|
||||||
private BottomSheetBehavior bottomSheetBehavior;
|
private NavigationController navigationController;
|
||||||
|
private BottomSheetController bottomSheetController;
|
||||||
|
public BottomSheetBehavior bottomSheetBehavior;
|
||||||
|
public boolean isLandscape = false;
|
||||||
private AssetLinkNavigator assetLinkNavigator;
|
private AssetLinkNavigator assetLinkNavigator;
|
||||||
private AssetLinkUtil.AssetLink pendingAssetLink;
|
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||||
|
|
||||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||||
private Intent pendingDownloadPlaybackIntent;
|
private Intent pendingDownloadPlaybackIntent;
|
||||||
|
|
||||||
|
public ActivityMainBinding getBinding() {
|
||||||
|
return bind;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
SplashScreen.installSplashScreen(this);
|
SplashScreen.installSplashScreen(this);
|
||||||
@@ -85,6 +100,8 @@ public class MainActivity extends BaseActivity {
|
|||||||
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
||||||
connectivityStatusReceiverManager(true);
|
connectivityStatusReceiverManager(true);
|
||||||
|
|
||||||
|
isLandscape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
|
||||||
|
|
||||||
init();
|
init();
|
||||||
checkConnectionType();
|
checkConnectionType();
|
||||||
getOpenSubsonicExtensions();
|
getOpenSubsonicExtensions();
|
||||||
@@ -105,6 +122,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
pingServer();
|
pingServer();
|
||||||
|
toggleNavigationDrawerLockOnOrientationChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -131,7 +149,6 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
fragmentManager = getSupportFragmentManager();
|
|
||||||
|
|
||||||
initBottomSheet();
|
initBottomSheet();
|
||||||
initNavigation();
|
initNavigation();
|
||||||
@@ -141,51 +158,79 @@ public class MainActivity extends BaseActivity {
|
|||||||
} else {
|
} else {
|
||||||
goToLogin();
|
goToLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleNavigationDrawerLockOnOrientationChange();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BOTTOM SHEET/NAVIGATION
|
private void initNavigation() {
|
||||||
private void initBottomSheet() {
|
// We link the nav_graph.xml with our navigationController
|
||||||
bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.player_bottom_sheet));
|
NavHostFragment navHostFragment = (NavHostFragment) this
|
||||||
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
|
.getSupportFragmentManager()
|
||||||
fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit();
|
.findFragmentById(R.id.nav_host_fragment);
|
||||||
|
navController = Objects.requireNonNull(navHostFragment).getNavController();
|
||||||
|
/*
|
||||||
|
navController is currently global since some legacy code still invokes it directly
|
||||||
|
the MainActivity methods that use it must be converted to NavigationHelper methods
|
||||||
|
*/
|
||||||
|
|
||||||
checkBottomSheetAfterStateChanged();
|
// Helper
|
||||||
|
NavigationHelper navigationHelper =
|
||||||
|
new NavigationHelper(
|
||||||
|
findViewById(R.id.bottom_navigation),
|
||||||
|
findViewById(R.id.bottom_navigation_frame),
|
||||||
|
findViewById(R.id.drawer_layout),
|
||||||
|
findViewById(R.id.nav_view),
|
||||||
|
navHostFragment
|
||||||
|
);
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
navigationController = new NavigationController(navigationHelper);
|
||||||
|
navigationController.syncWithBottomSheetBehavior(bottomSheetBehavior, navController);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initBottomSheet() {
|
||||||
|
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||||
|
View bottomSheetView = findViewById(R.id.player_bottom_sheet);
|
||||||
|
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetView);
|
||||||
|
/*
|
||||||
|
bottomSheetBehavior is currently global since some legacy code still invokes it directly
|
||||||
|
the MainActivity methods that use it must be converted to BottomSheetHelper methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
BottomSheetHelper bottomSheetHelper =
|
||||||
|
new BottomSheetHelper(
|
||||||
|
bottomSheetBehavior,
|
||||||
|
bottomSheetView,
|
||||||
|
fragmentManager
|
||||||
|
);
|
||||||
|
|
||||||
|
// Controller
|
||||||
|
bottomSheetController = new BottomSheetController(bottomSheetHelper);
|
||||||
|
bottomSheetController.addCallback(bottomSheetCallback);
|
||||||
|
bottomSheetController.replaceFragment(R.id.player_bottom_sheet);
|
||||||
|
bottomSheetController.checkAfterStateChanged(mainViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomSheetInPeek(Boolean isVisible) {
|
public void setBottomSheetInPeek(Boolean isVisible) {
|
||||||
if (isVisible) {
|
bottomSheetController.setStateInPeek(isVisible);
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
} else {
|
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomSheetVisibility(boolean visibility) {
|
public void setBottomSheetVisibility(boolean visibility) {
|
||||||
if (visibility) {
|
bottomSheetController.setVisibility(visibility);
|
||||||
findViewById(R.id.player_bottom_sheet).setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
findViewById(R.id.player_bottom_sheet).setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkBottomSheetAfterStateChanged() {
|
|
||||||
final Handler handler = new Handler();
|
|
||||||
final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
|
||||||
handler.postDelayed(runnable, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void collapseBottomSheetDelayed() {
|
public void collapseBottomSheetDelayed() {
|
||||||
final Handler handler = new Handler();
|
bottomSheetController.collapseDelayed();
|
||||||
final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
handler.postDelayed(runnable, 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expandBottomSheet() {
|
public void expandBottomSheet() {
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
bottomSheetController.expand();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomSheetDraggableState(Boolean isDraggable) {
|
public void setBottomSheetDraggableState(Boolean isDraggable) {
|
||||||
bottomSheetBehavior.setDraggable(isDraggable);
|
bottomSheetController.setDraggable(isDraggable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
|
private final BottomSheetBehavior.BottomSheetCallback bottomSheetCallback =
|
||||||
@@ -198,7 +243,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case BottomSheetBehavior.STATE_HIDDEN:
|
case BottomSheetBehavior.STATE_HIDDEN:
|
||||||
resetMusicSession();
|
resetMusicSession(); // I can't put the callback inside BottomSheetHelper because of this line
|
||||||
break;
|
break;
|
||||||
case BottomSheetBehavior.STATE_COLLAPSED:
|
case BottomSheetBehavior.STATE_COLLAPSED:
|
||||||
if (playerBottomSheetFragment != null)
|
if (playerBottomSheetFragment != null)
|
||||||
@@ -215,17 +260,14 @@ public class MainActivity extends BaseActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onSlide(@NonNull View view, float slideOffset) {
|
public void onSlide(@NonNull View view, float slideOffset) {
|
||||||
animateBottomSheet(slideOffset);
|
animateBottomSheet(slideOffset);
|
||||||
animateBottomNavigation(slideOffset, navigationHeight);
|
if (!isLandscape) {
|
||||||
|
animateBottomNavigation(slideOffset, navigationHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void animateBottomSheet(float slideOffset) {
|
private void animateBottomSheet(float slideOffset) {
|
||||||
PlayerBottomSheetFragment playerBottomSheetFragment = (PlayerBottomSheetFragment) getSupportFragmentManager().findFragmentByTag("PlayerBottomSheet");
|
bottomSheetController.animate(slideOffset);
|
||||||
if (playerBottomSheetFragment != null) {
|
|
||||||
float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
|
|
||||||
playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset);
|
|
||||||
playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void animateBottomNavigation(float slideOffset, int navigationHeight) {
|
private void animateBottomNavigation(float slideOffset, int navigationHeight) {
|
||||||
@@ -240,36 +282,57 @@ public class MainActivity extends BaseActivity {
|
|||||||
bind.bottomNavigation.setTranslationY(slideY);
|
bind.bottomNavigation.setTranslationY(slideY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initNavigation() {
|
public void setBottomNavigationBarVisibility(boolean visibility) {
|
||||||
bottomNavigationView = findViewById(R.id.bottom_navigation);
|
navigationController.setNavbarVisibility(visibility);
|
||||||
navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment);
|
|
||||||
navController = Objects.requireNonNull(navHostFragment).getNavController();
|
|
||||||
|
|
||||||
/*
|
|
||||||
* In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet è aperto,
|
|
||||||
* lo chiudo
|
|
||||||
*/
|
|
||||||
navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
|
|
||||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED && (
|
|
||||||
destination.getId() == R.id.homeFragment ||
|
|
||||||
destination.getId() == R.id.libraryFragment ||
|
|
||||||
destination.getId() == R.id.downloadFragment)
|
|
||||||
) {
|
|
||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
NavigationUI.setupWithNavController(bottomNavigationView, navController);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBottomNavigationBarVisibility(boolean visibility) {
|
public void toggleBottomNavigationBarVisibilityOnOrientationChange() {
|
||||||
if (visibility) {
|
float displayDensity = getResources().getDisplayMetrics().density;
|
||||||
bottomNavigationView.setVisibility(View.VISIBLE);
|
// Ignore orientation change, bottom navbar always hidden
|
||||||
|
if (Preferences.getHideBottomNavbarOnPortrait()) {
|
||||||
|
navigationController.setNavbarVisibility(false);
|
||||||
|
bottomSheetController.setPeekHeight(56, displayDensity);
|
||||||
|
navigationController.setSystemBarsVisibility(this, !isLandscape);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLandscape) {
|
||||||
|
// Show app navbar + show system bars
|
||||||
|
bottomSheetController.setPeekHeight(136, displayDensity);
|
||||||
|
navigationController.setNavbarVisibility(true);
|
||||||
|
navigationController.setSystemBarsVisibility(this, true);
|
||||||
} else {
|
} else {
|
||||||
bottomNavigationView.setVisibility(View.GONE);
|
// Hide app navbar + hide system bars
|
||||||
|
bottomSheetController.setPeekHeight(56, displayDensity);
|
||||||
|
navigationController.setNavbarVisibility(false);
|
||||||
|
navigationController.setSystemBarsVisibility(this, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setNavigationDrawerLock(boolean locked) {
|
||||||
|
navigationController.setDrawerLock(locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNavigationDrawerLocked() {
|
||||||
|
return navigationController.isNavigationDrawerLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleNavigationDrawerLockOnOrientationChange() {
|
||||||
|
navigationController.toggleDrawerLockOnOrientation(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSystemBarsVisibility(boolean visibility) {
|
||||||
|
navigationController.setSystemBarsVisibility(this, visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
There are only 4 init functions that must exist up to here
|
||||||
|
1. init()
|
||||||
|
2. initNavigation()
|
||||||
|
3. initBottomSheet()
|
||||||
|
4. bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() { ... }
|
||||||
|
*/
|
||||||
|
|
||||||
private void initService() {
|
private void initService() {
|
||||||
MediaManager.check(getMediaBrowserListenableFuture());
|
MediaManager.check(getMediaBrowserListenableFuture());
|
||||||
|
|
||||||
@@ -304,7 +367,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void goToHome() {
|
private void goToHome() {
|
||||||
bottomNavigationView.setVisibility(View.VISIBLE);
|
setBottomNavigationBarVisibility(true);
|
||||||
|
|
||||||
if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) {
|
if (Objects.requireNonNull(navController.getCurrentDestination()).getId() == R.id.landingFragment) {
|
||||||
navController.navigate(R.id.action_landingFragment_to_homeFragment);
|
navController.navigate(R.id.action_landingFragment_to_homeFragment);
|
||||||
@@ -351,10 +414,11 @@ public class MainActivity extends BaseActivity {
|
|||||||
Preferences.setServer(null);
|
Preferences.setServer(null);
|
||||||
Preferences.setLocalAddress(null);
|
Preferences.setLocalAddress(null);
|
||||||
Preferences.setUser(null);
|
Preferences.setUser(null);
|
||||||
|
Preferences.setClientCert(null);
|
||||||
|
|
||||||
// TODO Enter all settings to be reset
|
// TODO Enter all settings to be reset
|
||||||
Preferences.setOpenSubsonic(false);
|
Preferences.setOpenSubsonic(false);
|
||||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
|
Preferences.setPlaybackSpeed(1.0f);
|
||||||
Preferences.setSkipSilenceMode(false);
|
Preferences.setSkipSilenceMode(false);
|
||||||
Preferences.setDataSavingMode(false);
|
Preferences.setDataSavingMode(false);
|
||||||
Preferences.setStarredSyncEnabled(false);
|
Preferences.setStarredSyncEnabled(false);
|
||||||
@@ -384,7 +448,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void pingServer() {
|
private void pingServer() {
|
||||||
if (Preferences.getToken() == null) return;
|
if (Preferences.getToken() == null && Preferences.getPassword() == null) return;
|
||||||
|
|
||||||
if (Preferences.isInUseServerAddressLocal()) {
|
if (Preferences.isInUseServerAddressLocal()) {
|
||||||
mainViewModel.ping().observe(this, subsonicResponse -> {
|
mainViewModel.ping().observe(this, subsonicResponse -> {
|
||||||
@@ -428,7 +492,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void getOpenSubsonicExtensions() {
|
private void getOpenSubsonicExtensions() {
|
||||||
if (Preferences.getToken() != null) {
|
if (Preferences.getToken() != null || Preferences.getPassword() != null) {
|
||||||
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
||||||
if (openSubsonicExtensions != null) {
|
if (openSubsonicExtensions != null) {
|
||||||
Preferences.setOpenSubsonicExtensions(openSubsonicExtensions);
|
Preferences.setOpenSubsonicExtensions(openSubsonicExtensions);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,6 +23,8 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
|
|||||||
|
|
||||||
private List<AlbumID3> albums;
|
private List<AlbumID3> albums;
|
||||||
|
|
||||||
|
private int sizePx = 400;
|
||||||
|
|
||||||
public AlbumAdapter(ClickCallback click) {
|
public AlbumAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.albums = Collections.emptyList();
|
this.albums = Collections.emptyList();
|
||||||
@@ -31,11 +34,20 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemLibraryAlbumBinding view = ItemLibraryAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemLibraryAlbumBinding view = ItemLibraryAlbumBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize(parent.getContext());
|
||||||
|
sizePx = TileSizeManager.getInstance().getTileSizePx(parent.getContext());
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.albumCoverImageView.getLayoutParams();
|
||||||
|
lp.width = sizePx;
|
||||||
|
lp.height = sizePx;
|
||||||
|
holder.item.albumCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
AlbumID3 album = albums.get(position);
|
AlbumID3 album = albums.get(position);
|
||||||
|
|
||||||
holder.item.albumNameLabel.setText(album.getName());
|
holder.item.albumNameLabel.setText(album.getName());
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -24,6 +25,7 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
|
|||||||
private final boolean mix;
|
private final boolean mix;
|
||||||
private final boolean bestOf;
|
private final boolean bestOf;
|
||||||
|
|
||||||
|
private int sizePx = 400;
|
||||||
private List<ArtistID3> artists;
|
private List<ArtistID3> artists;
|
||||||
|
|
||||||
public ArtistAdapter(ClickCallback click, Boolean mix, Boolean bestOf) {
|
public ArtistAdapter(ClickCallback click, Boolean mix, Boolean bestOf) {
|
||||||
@@ -37,11 +39,20 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemLibraryArtistBinding view = ItemLibraryArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemLibraryArtistBinding view = ItemLibraryArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize(parent.getContext());
|
||||||
|
sizePx = TileSizeManager.getInstance().getTileSizePx(parent.getContext());
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.artistCoverImageView.getLayoutParams();
|
||||||
|
lp.width = sizePx;
|
||||||
|
lp.height = sizePx;
|
||||||
|
holder.item.artistCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
ArtistID3 artist = artists.get(position);
|
ArtistID3 artist = artists.get(position);
|
||||||
|
|
||||||
holder.item.artistNameLabel.setText(artist.getName());
|
holder.item.artistNameLabel.setText(artist.getName());
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
|
|||||||
public void sort(String order) {
|
public void sort(String order) {
|
||||||
switch (order) {
|
switch (order) {
|
||||||
case Constants.ARTIST_ORDER_BY_NAME:
|
case Constants.ARTIST_ORDER_BY_NAME:
|
||||||
artists.sort(Comparator.comparing(ArtistID3::getName));
|
artists.sort(Comparator.comparing(ArtistID3::getName,String.CASE_INSENSITIVE_ORDER));
|
||||||
break;
|
break;
|
||||||
case Constants.ARTIST_ORDER_BY_RANDOM:
|
case Constants.ARTIST_ORDER_BY_RANDOM:
|
||||||
Collections.shuffle(artists);
|
Collections.shuffle(artists);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.SimilarArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.SimilarArtistID3;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,6 +23,8 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
|
|||||||
|
|
||||||
private List<SimilarArtistID3> artists;
|
private List<SimilarArtistID3> artists;
|
||||||
|
|
||||||
|
private int sizePx = 400;
|
||||||
|
|
||||||
public ArtistSimilarAdapter(ClickCallback click) {
|
public ArtistSimilarAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.artists = Collections.emptyList();
|
this.artists = Collections.emptyList();
|
||||||
@@ -31,11 +34,20 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemLibrarySimilarArtistBinding view = ItemLibrarySimilarArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemLibrarySimilarArtistBinding view = ItemLibrarySimilarArtistBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize(parent.getContext());
|
||||||
|
sizePx = TileSizeManager.getInstance().getTileSizePx(parent.getContext());
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.similarArtistCoverImageView.getLayoutParams();
|
||||||
|
lp.width = sizePx;
|
||||||
|
lp.height = sizePx;
|
||||||
|
holder.item.similarArtistCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
SimilarArtistID3 artist = artists.get(position);
|
SimilarArtistID3 artist = artists.get(position);
|
||||||
|
|
||||||
holder.item.artistNameLabel.setText(artist.getName());
|
holder.item.artistNameLabel.setText(artist.getName());
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
|
|||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,6 +25,9 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
|
|||||||
|
|
||||||
private List<Child> songs;
|
private List<Child> songs;
|
||||||
|
|
||||||
|
private int widthPx = 800;
|
||||||
|
private int heightPx = 400;
|
||||||
|
|
||||||
public DiscoverSongAdapter(ClickCallback click) {
|
public DiscoverSongAdapter(ClickCallback click) {
|
||||||
this.click = click;
|
this.click = click;
|
||||||
this.songs = Collections.emptyList();
|
this.songs = Collections.emptyList();
|
||||||
@@ -32,11 +37,21 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
ItemHomeDiscoverSongBinding view = ItemHomeDiscoverSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
ItemHomeDiscoverSongBinding view = ItemHomeDiscoverSongBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateDiscoverSize(parent.getContext());
|
||||||
|
widthPx = TileSizeManager.getInstance().getDiscoverWidthPx(parent.getContext());;
|
||||||
|
heightPx = TileSizeManager.getInstance().getDiscoverHeightPx(parent.getContext());;
|
||||||
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
ViewGroup.LayoutParams lp = holder.item.discoverSongCoverImageView.getLayoutParams();
|
||||||
|
lp.width = widthPx;
|
||||||
|
lp.height = heightPx;
|
||||||
|
holder.item.discoverSongCoverImageView.setLayoutParams(lp);
|
||||||
|
|
||||||
Child song = songs.get(position);
|
Child song = songs.get(position);
|
||||||
|
|
||||||
holder.item.titleDiscoverSongLabel.setText(song.getTitle());
|
holder.item.titleDiscoverSongLabel.setText(song.getTitle());
|
||||||
|
|||||||
@@ -42,8 +42,13 @@ public class InternetRadioStationAdapter extends RecyclerView.Adapter<InternetRa
|
|||||||
holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName());
|
holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName());
|
||||||
holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl());
|
holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl());
|
||||||
|
|
||||||
|
String imageId = internetRadioStation.getHomePageUrl();
|
||||||
|
if (imageId == null || imageId.isEmpty()) {
|
||||||
|
imageId = internetRadioStation.getStreamUrl();
|
||||||
|
}
|
||||||
|
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(holder.itemView.getContext(), internetRadioStation.getStreamUrl(), CustomGlideRequest.ResourceType.Radio)
|
.from(holder.itemView.getContext(), imageId, CustomGlideRequest.ResourceType.Radio)
|
||||||
.build()
|
.build()
|
||||||
.into(holder.item.internetRadioStationCoverImageView);
|
.into(holder.item.internetRadioStationCoverImageView);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
|
|||||||
|
|
||||||
FilterResults results = new FilterResults();
|
FilterResults results = new FilterResults();
|
||||||
results.values = filteredList;
|
results.values = filteredList;
|
||||||
|
results.count = filteredList.size();
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,9 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
|
|||||||
@Override
|
@Override
|
||||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
playlists.clear();
|
playlists.clear();
|
||||||
if (results.count > 0) playlists.addAll((List) results.values);
|
if (results.values != null) {
|
||||||
|
playlists.addAll((List<Playlist>) results.values);
|
||||||
|
}
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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()) {
|
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.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) {
|
if (album.getDiscTitles() != null) {
|
||||||
@@ -357,6 +359,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||||||
private boolean onLongClick() {
|
private boolean onLongClick() {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
||||||
|
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
|
||||||
|
|
||||||
click.onMediaLongClick(bundle);
|
click.onMediaLongClick(bundle);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.controller;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
public class BottomSheetController {
|
||||||
|
|
||||||
|
BottomSheetHelper helper;
|
||||||
|
|
||||||
|
public BottomSheetController(@NonNull BottomSheetHelper bottomSheetPlayerHelper) {
|
||||||
|
this.helper = bottomSheetPlayerHelper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expand() {
|
||||||
|
helper.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
helper.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStateInPeek(boolean isVisible) {
|
||||||
|
helper.setStateInPeek(isVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisibility(boolean visibility) {
|
||||||
|
helper.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallback(BottomSheetBehavior.BottomSheetCallback callback) {
|
||||||
|
helper.addCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceFragment(int playerBottomSheet) {
|
||||||
|
helper.replaceFragment(playerBottomSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAfterStateChanged(MainViewModel mainViewModel) {
|
||||||
|
helper.checkAfterStateChanged(mainViewModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collapseDelayed() {
|
||||||
|
helper.collapseDelayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDraggable(Boolean isDraggable) {
|
||||||
|
helper.setDraggable(isDraggable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
return helper.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void animate(float slideOffset) {
|
||||||
|
helper.animate(slideOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPeekHeight(int peekHeight, float displayDensity) {
|
||||||
|
helper.setPeekHeight(peekHeight, displayDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.controller;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
public class BottomSheetHelper {
|
||||||
|
|
||||||
|
BottomSheetBehavior<View> bottomSheetBehavior;
|
||||||
|
View bottomSheetView;
|
||||||
|
FragmentManager fragmentManager; // Of the entire activity
|
||||||
|
PlayerBottomSheetFragment playerBottomSheetFragment;
|
||||||
|
|
||||||
|
public void setState(int state) {
|
||||||
|
bottomSheetBehavior.setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BottomSheetHelper(@NonNull BottomSheetBehavior<View> bottomSheetBehavior,
|
||||||
|
@NonNull View bottomSheetView,
|
||||||
|
@NonNull FragmentManager fragmentManager) {
|
||||||
|
this.bottomSheetBehavior = bottomSheetBehavior;
|
||||||
|
this.bottomSheetView = bottomSheetView;
|
||||||
|
this.fragmentManager = fragmentManager;
|
||||||
|
this.playerBottomSheetFragment = new PlayerBottomSheetFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCallback(BottomSheetBehavior.BottomSheetCallback callback) {
|
||||||
|
bottomSheetBehavior.addBottomSheetCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStateInPeek(boolean isVisible) {
|
||||||
|
if (isVisible) {
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
} else {
|
||||||
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVisibility(boolean visibility) {
|
||||||
|
if (visibility) {
|
||||||
|
bottomSheetView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
bottomSheetView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceFragment(int playerBottomSheet) {
|
||||||
|
fragmentManager
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(
|
||||||
|
playerBottomSheet,
|
||||||
|
playerBottomSheetFragment,
|
||||||
|
"PlayerBottomSheet")
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkAfterStateChanged(MainViewModel mainViewModel) {
|
||||||
|
final Handler handler = new Handler();
|
||||||
|
final Runnable runnable = () -> setStateInPeek(mainViewModel.isQueueLoaded());
|
||||||
|
handler.postDelayed(runnable, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collapseDelayed() {
|
||||||
|
final Handler handler = new Handler();
|
||||||
|
final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
handler.postDelayed(runnable, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDraggable(Boolean isDraggable) {
|
||||||
|
bottomSheetBehavior.setDraggable((isDraggable));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getState() {
|
||||||
|
return bottomSheetBehavior.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void animate(float slideOffset) {
|
||||||
|
if (playerBottomSheetFragment != null) {
|
||||||
|
float condensedSlideOffset = Math.max(0.0f, Math.min(0.2f, slideOffset - 0.2f)) / 0.2f;
|
||||||
|
playerBottomSheetFragment.getPlayerHeader().setAlpha(1 - condensedSlideOffset);
|
||||||
|
playerBottomSheetFragment.getPlayerHeader().setVisibility(condensedSlideOffset > 0.99 ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPeekHeight(int peekHeight, float displayDensity) {
|
||||||
|
int newPeekPx = (int) (peekHeight * displayDensity);
|
||||||
|
bottomSheetBehavior.setPeekHeight(newPeekPx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,28 +19,30 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||||||
import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaylistChooserViewModel;
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
public class PlaylistChooserDialog extends DialogFragment implements ClickCallback {
|
||||||
private DialogPlaylistChooserBinding bind;
|
private DialogPlaylistChooserBinding bind;
|
||||||
private PlaylistChooserViewModel playlistChooserViewModel;
|
private PlaylistChooserViewModel playlistChooserViewModel;
|
||||||
|
|
||||||
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
||||||
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
||||||
bind = DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
bind = DialogPlaylistChooserBinding.inflate(getLayoutInflater());
|
||||||
|
|
||||||
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
playlistChooserViewModel = new ViewModelProvider(requireActivity()).get(PlaylistChooserViewModel.class);
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(getActivity())
|
bind.playlistDialogChooserVisibilitySwitch.setOnCheckedChangeListener(
|
||||||
|
(buttonView,
|
||||||
|
isChecked) -> playlistChooserViewModel.setIsPlaylistPublic(isChecked)
|
||||||
|
);
|
||||||
|
bind.playlistChooserDialogCreateButton.setOnClickListener(v -> launchPlaylistEditor());
|
||||||
|
bind.playlistChooserDialogCancelButton.setOnClickListener(v -> dismiss());
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext())
|
||||||
.setView(bind.getRoot())
|
.setView(bind.getRoot())
|
||||||
.setTitle(R.string.playlist_chooser_dialog_title)
|
.setTitle(R.string.playlist_chooser_dialog_title);
|
||||||
.setNeutralButton(R.string.playlist_chooser_dialog_neutral_button, (dialog, id) -> { })
|
return builder.create();
|
||||||
.setNegativeButton(R.string.playlist_chooser_dialog_negative_button, (dialog, id) -> dialog.cancel())
|
|
||||||
.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,25 +57,26 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
|||||||
|
|
||||||
initPlaylistView();
|
initPlaylistView();
|
||||||
setSongInfo();
|
setSongInfo();
|
||||||
setButtonAction();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSongInfo() {
|
private void setSongInfo() {
|
||||||
playlistChooserViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT));
|
playlistChooserViewModel.setSongsToAdd(requireArguments().getParcelableArrayList(Constants.TRACKS_OBJECT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setButtonAction() {
|
private void launchPlaylistEditor() {
|
||||||
androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog());
|
Bundle bundle = new Bundle();
|
||||||
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
|
bundle.putParcelableArrayList(
|
||||||
Bundle bundle = new Bundle();
|
Constants.TRACKS_OBJECT,
|
||||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, playlistChooserViewModel.getSongsToAdd());
|
playlistChooserViewModel.getSongsToAdd()
|
||||||
|
);
|
||||||
|
|
||||||
PlaylistEditorDialog dialog = new PlaylistEditorDialog(null);
|
PlaylistEditorDialog editorDialog = new PlaylistEditorDialog(null);
|
||||||
dialog.setArguments(bundle);
|
editorDialog.setArguments(bundle);
|
||||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
editorDialog.show(
|
||||||
|
requireActivity().getSupportFragmentManager(),
|
||||||
|
null);
|
||||||
|
|
||||||
Objects.requireNonNull(getDialog()).dismiss();
|
dismiss();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPlaylistView() {
|
private void initPlaylistView() {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ package com.cappielloantonio.tempo.ui.dialog;
|
|||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogRadioEditorBinding;
|
import com.cappielloantonio.tempo.databinding.DialogRadioEditorBinding;
|
||||||
import com.cappielloantonio.tempo.interfaces.RadioCallback;
|
import com.cappielloantonio.tempo.interfaces.RadioCallback;
|
||||||
@@ -21,7 +23,6 @@ import java.util.Objects;
|
|||||||
public class RadioEditorDialog extends DialogFragment {
|
public class RadioEditorDialog extends DialogFragment {
|
||||||
private DialogRadioEditorBinding bind;
|
private DialogRadioEditorBinding bind;
|
||||||
private RadioEditorViewModel radioEditorViewModel;
|
private RadioEditorViewModel radioEditorViewModel;
|
||||||
|
|
||||||
private final RadioCallback radioCallback;
|
private final RadioCallback radioCallback;
|
||||||
|
|
||||||
private String radioName;
|
private String radioName;
|
||||||
@@ -36,25 +37,26 @@ public class RadioEditorDialog extends DialogFragment {
|
|||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
bind = DialogRadioEditorBinding.inflate(getLayoutInflater());
|
bind = DialogRadioEditorBinding.inflate(getLayoutInflater());
|
||||||
|
|
||||||
radioEditorViewModel = new ViewModelProvider(requireActivity()).get(RadioEditorViewModel.class);
|
radioEditorViewModel = new ViewModelProvider(requireActivity()).get(RadioEditorViewModel.class);
|
||||||
|
|
||||||
|
setupObservers();
|
||||||
|
|
||||||
return new MaterialAlertDialogBuilder(requireContext())
|
return new MaterialAlertDialogBuilder(requireContext())
|
||||||
.setView(bind.getRoot())
|
.setView(bind.getRoot())
|
||||||
.setTitle(R.string.radio_editor_dialog_title)
|
.setTitle(R.string.radio_editor_dialog_title)
|
||||||
.setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> {
|
.setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> {
|
||||||
if (validateInput()) {
|
if (validateInput()) {
|
||||||
if (radioEditorViewModel.getRadioToEdit() == null) {
|
if (radioEditorViewModel.getRadioToEdit() == null) {
|
||||||
radioEditorViewModel.createRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL);
|
radioEditorViewModel.createRadio(radioName, radioStreamURL,
|
||||||
|
radioHomepageURL.isEmpty() ? null : radioHomepageURL);
|
||||||
} else {
|
} 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) -> {
|
.setNeutralButton(R.string.radio_editor_dialog_neutral_button, (dialog, id) -> {
|
||||||
radioEditorViewModel.deleteRadio();
|
radioEditorViewModel.deleteRadio();
|
||||||
dismissDialog();
|
|
||||||
})
|
})
|
||||||
.setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> {
|
.setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> {
|
||||||
dialog.cancel();
|
dialog.cancel();
|
||||||
@@ -62,6 +64,24 @@ public class RadioEditorDialog extends DialogFragment {
|
|||||||
.create();
|
.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
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
@@ -77,7 +97,6 @@ public class RadioEditorDialog extends DialogFragment {
|
|||||||
private void setParameterInfo() {
|
private void setParameterInfo() {
|
||||||
if (getArguments() != null && getArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT) != null) {
|
if (getArguments() != null && getArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT) != null) {
|
||||||
InternetRadioStation toEdit = requireArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT);
|
InternetRadioStation toEdit = requireArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT);
|
||||||
|
|
||||||
radioEditorViewModel.setRadioToEdit(toEdit);
|
radioEditorViewModel.setRadioToEdit(toEdit);
|
||||||
|
|
||||||
bind.internetRadioStationNameTextView.setText(toEdit.getName());
|
bind.internetRadioStationNameTextView.setText(toEdit.getName());
|
||||||
@@ -90,22 +109,21 @@ public class RadioEditorDialog extends DialogFragment {
|
|||||||
radioName = Objects.requireNonNull(bind.internetRadioStationNameTextView.getText()).toString().trim();
|
radioName = Objects.requireNonNull(bind.internetRadioStationNameTextView.getText()).toString().trim();
|
||||||
radioStreamURL = Objects.requireNonNull(bind.internetRadioStationStreamUrlTextView.getText()).toString().trim();
|
radioStreamURL = Objects.requireNonNull(bind.internetRadioStationStreamUrlTextView.getText()).toString().trim();
|
||||||
radioHomepageURL = Objects.requireNonNull(bind.internetRadioStationHomepageUrlTextView.getText()).toString().trim();
|
radioHomepageURL = Objects.requireNonNull(bind.internetRadioStationHomepageUrlTextView.getText()).toString().trim();
|
||||||
|
|
||||||
if (TextUtils.isEmpty(radioName)) {
|
if (TextUtils.isEmpty(radioName)) {
|
||||||
bind.internetRadioStationNameTextView.setError(getString(R.string.error_required));
|
bind.internetRadioStationNameTextView.setError(getString(R.string.error_required));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TextUtils.isEmpty(radioStreamURL)) {
|
if (TextUtils.isEmpty(radioStreamURL)) {
|
||||||
bind.internetRadioStationStreamUrlTextView.setError(getString(R.string.error_required));
|
bind.internetRadioStationStreamUrlTextView.setError(getString(R.string.error_required));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dismissDialog() {
|
private void dismissDialog() {
|
||||||
radioCallback.onDismiss();
|
if (radioCallback != null) {
|
||||||
|
radioCallback.onDismiss();
|
||||||
|
}
|
||||||
Objects.requireNonNull(getDialog()).dismiss();
|
Objects.requireNonNull(getDialog()).dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@ package com.cappielloantonio.tempo.ui.dialog;
|
|||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.security.KeyChain;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -32,11 +32,21 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
private String server;
|
private String server;
|
||||||
private String localAddress;
|
private String localAddress;
|
||||||
private boolean lowSecurity = false;
|
private boolean lowSecurity = false;
|
||||||
|
private String clientCertAlias;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
bind = DialogServerSignupBinding.inflate(getLayoutInflater());
|
bind = DialogServerSignupBinding.inflate(getLayoutInflater());
|
||||||
|
bind.clientCertTextView.setOnClickListener(v -> {
|
||||||
|
if (TextUtils.isEmpty(bind.clientCertTextView.getText())) {
|
||||||
|
KeyChain.choosePrivateKeyAlias(requireActivity(), alias -> {
|
||||||
|
bind.clientCertTextView.setText(alias);
|
||||||
|
}, null, null, null, null);
|
||||||
|
} else {
|
||||||
|
bind.clientCertTextView.setText(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
|
loginViewModel = new ViewModelProvider(requireActivity()).get(LoginViewModel.class);
|
||||||
|
|
||||||
@@ -74,6 +84,7 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress());
|
bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress());
|
||||||
bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress());
|
bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress());
|
||||||
bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity());
|
bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity());
|
||||||
|
bind.clientCertTextView.setText(loginViewModel.getServerToEdit().getClientCert());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
loginViewModel.setServerToEdit(null);
|
loginViewModel.setServerToEdit(null);
|
||||||
@@ -106,6 +117,7 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null;
|
server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null;
|
||||||
localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null;
|
localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null;
|
||||||
lowSecurity = bind.lowSecurityCheckbox.isChecked();
|
lowSecurity = bind.lowSecurityCheckbox.isChecked();
|
||||||
|
clientCertAlias = bind.clientCertTextView.getText() != null && !bind.clientCertTextView.getText().toString().trim().isBlank() ? bind.clientCertTextView.getText().toString().trim() : null;
|
||||||
|
|
||||||
if (TextUtils.isEmpty(serverName)) {
|
if (TextUtils.isEmpty(serverName)) {
|
||||||
bind.serverNameTextView.setError(getString(R.string.error_required));
|
bind.serverNameTextView.setError(getString(R.string.error_required));
|
||||||
@@ -137,6 +149,6 @@ public class ServerSignupDialog extends DialogFragment {
|
|||||||
|
|
||||||
private void saveServerPreference() {
|
private void saveServerPreference() {
|
||||||
String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString();
|
String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString();
|
||||||
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity));
|
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity, this.clientCertAlias));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.cappielloantonio.tempo.ui.dialog;
|
package com.cappielloantonio.tempo.ui.dialog;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|||||||
@@ -61,13 +61,47 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
private void setTrackInfo() {
|
private void setTrackInfo() {
|
||||||
genreLink = null;
|
genreLink = null;
|
||||||
yearLink = null;
|
yearLink = null;
|
||||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
|
||||||
bind.trakArtistInfoTextView.setText(
|
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||||
mediaMetadata.artist != null
|
boolean isRadio = Objects.equals(type, Constants.MEDIA_TYPE_RADIO);
|
||||||
? mediaMetadata.artist
|
|
||||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
if (isRadio) {
|
||||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||||
: "");
|
// MediaMetadata.title/artist are formatted for notification
|
||||||
|
String stationName = mediaMetadata.extras != null
|
||||||
|
? mediaMetadata.extras.getString("stationName",
|
||||||
|
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||||
|
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||||
|
|
||||||
|
String artist = mediaMetadata.extras != null
|
||||||
|
? mediaMetadata.extras.getString("radioArtist", "")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
String title = mediaMetadata.extras != null
|
||||||
|
? mediaMetadata.extras.getString("radioTitle", "")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Format: "Artist - Song" or fallback to title or station name
|
||||||
|
String mainTitle;
|
||||||
|
if (!android.text.TextUtils.isEmpty(artist) && !android.text.TextUtils.isEmpty(title)) {
|
||||||
|
mainTitle = artist + " - " + title;
|
||||||
|
} else if (!android.text.TextUtils.isEmpty(title)) {
|
||||||
|
mainTitle = title;
|
||||||
|
} else if (!android.text.TextUtils.isEmpty(artist)) {
|
||||||
|
mainTitle = artist;
|
||||||
|
} else {
|
||||||
|
mainTitle = stationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.trakTitleInfoTextView.setText(mainTitle);
|
||||||
|
bind.trakArtistInfoTextView.setText(stationName);
|
||||||
|
} else {
|
||||||
|
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||||
|
bind.trakArtistInfoTextView.setText(
|
||||||
|
mediaMetadata.artist != null
|
||||||
|
? mediaMetadata.artist
|
||||||
|
: "");
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaMetadata.extras != null) {
|
if (mediaMetadata.extras != null) {
|
||||||
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||||
@@ -90,6 +124,27 @@ public class TrackInfoDialog extends DialogFragment {
|
|||||||
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
|
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
|
||||||
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
|
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
|
||||||
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
||||||
|
|
||||||
|
// Handle radio-specific metadata
|
||||||
|
if (isRadio) {
|
||||||
|
String stationName = mediaMetadata.extras.getString("stationName", getString(R.string.label_placeholder));
|
||||||
|
String radioArtist = mediaMetadata.extras.getString("radioArtist", "");
|
||||||
|
String radioTitle = mediaMetadata.extras.getString("radioTitle", "");
|
||||||
|
|
||||||
|
// Show station name in station section
|
||||||
|
bind.stationInfoSector.setVisibility(android.view.View.VISIBLE);
|
||||||
|
bind.stationValueSector.setText(stationName);
|
||||||
|
|
||||||
|
// Use radio metadata for title/artist if available
|
||||||
|
if (!android.text.TextUtils.isEmpty(radioTitle)) {
|
||||||
|
titleValue = radioTitle;
|
||||||
|
}
|
||||||
|
if (!android.text.TextUtils.isEmpty(radioArtist)) {
|
||||||
|
artistValue = radioArtist;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bind.stationInfoSector.setVisibility(android.view.View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
||||||
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -34,6 +35,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -47,7 +49,8 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
private FragmentAlbumCatalogueBinding bind;
|
private FragmentAlbumCatalogueBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
||||||
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
private AlbumCatalogueAdapter albumAdapter;
|
private AlbumCatalogueAdapter albumAdapter;
|
||||||
private String currentSortOrder;
|
private String currentSortOrder;
|
||||||
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
|
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
|
||||||
@@ -90,6 +93,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
|
bind = FragmentAlbumCatalogueBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize( requireContext() );
|
||||||
|
spanCount = TileSizeManager.getInstance().getTileSpanCount( requireContext() );
|
||||||
|
tileSpacing = TileSizeManager.getInstance().getTileSpacing( requireContext() );
|
||||||
|
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initAlbumCatalogueView();
|
initAlbumCatalogueView();
|
||||||
initProgressLoader();
|
initProgressLoader();
|
||||||
@@ -133,8 +140,8 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initAlbumCatalogueView() {
|
private void initAlbumCatalogueView() {
|
||||||
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
bind.albumCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
|
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
||||||
|
|||||||
@@ -261,8 +261,10 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||||||
bind.albumOtherInfoButton.setOnClickListener(v -> {
|
bind.albumOtherInfoButton.setOnClickListener(v -> {
|
||||||
if (bind.albumDetailView.getVisibility() == View.GONE) {
|
if (bind.albumDetailView.getVisibility() == View.GONE) {
|
||||||
bind.albumDetailView.setVisibility(View.VISIBLE);
|
bind.albumDetailView.setVisibility(View.VISIBLE);
|
||||||
|
bind.albumNameLabel.setMaxLines(Integer.MAX_VALUE);
|
||||||
} else if (bind.albumDetailView.getVisibility() == View.VISIBLE) {
|
} else if (bind.albumDetailView.getVisibility() == View.VISIBLE) {
|
||||||
bind.albumDetailView.setVisibility(View.GONE);
|
bind.albumDetailView.setVisibility(View.GONE);
|
||||||
|
bind.albumNameLabel.setMaxLines(2);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -35,6 +36,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
|||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||||
|
|
||||||
@@ -51,6 +53,9 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private ArtistCatalogueAdapter artistAdapter;
|
private ArtistCatalogueAdapter artistAdapter;
|
||||||
|
|
||||||
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -66,6 +71,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
|
bind = FragmentArtistCatalogueBinding.inflate(inflater, container, false);
|
||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize( requireContext() );
|
||||||
|
spanCount = TileSizeManager.getInstance().getTileSpanCount( requireContext() );
|
||||||
|
tileSpacing = TileSizeManager.getInstance().getTileSpacing( requireContext() );
|
||||||
|
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initArtistCatalogueView();
|
initArtistCatalogueView();
|
||||||
|
|
||||||
@@ -108,8 +117,8 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initArtistCatalogueView() {
|
private void initArtistCatalogueView() {
|
||||||
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
bind.artistCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
bind.artistCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.artistCatalogueRecyclerView.setHasFixedSize(true);
|
bind.artistCatalogueRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
artistAdapter = new ArtistCatalogueAdapter(this);
|
artistAdapter = new ArtistCatalogueAdapter(this);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -9,12 +10,14 @@ import android.util.Log;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
@@ -32,12 +35,15 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
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.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
@@ -59,6 +65,9 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
activity = (MainActivity) getActivity();
|
activity = (MainActivity) getActivity();
|
||||||
@@ -68,6 +77,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
||||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateTileSize( requireContext() );
|
||||||
|
spanCount = TileSizeManager.getInstance().getTileSpanCount( requireContext() );
|
||||||
|
tileSpacing = TileSizeManager.getInstance().getTileSpacing( requireContext() );
|
||||||
|
|
||||||
init(view);
|
init(view);
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initArtistInfo();
|
initArtistInfo();
|
||||||
@@ -118,6 +131,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(artistPageViewModel.getArtist().getStarred() != null);
|
favoriteToggle.setChecked(artistPageViewModel.getArtist().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> artistPageViewModel.setFavorite(requireContext()));
|
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() {
|
private void initAppBar() {
|
||||||
@@ -135,13 +152,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
if (artistInfo == null) {
|
if (artistInfo == null) {
|
||||||
if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE);
|
if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography());
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (getContext() != null && bind != null) {
|
if (getContext() != null && bind != null) {
|
||||||
ArtistID3 currentArtist = artistPageViewModel.getArtist();
|
ArtistID3 currentArtist = artistPageViewModel.getArtist();
|
||||||
String primaryId = currentArtist.getCoverArtId() != null && !currentArtist.getCoverArtId().trim().isEmpty()
|
String primaryId = currentArtist.getCoverArtId() != null && !currentArtist.getCoverArtId().trim().isEmpty()
|
||||||
@@ -191,34 +201,67 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
.into(bind.artistBackdropImageView);
|
.into(bind.artistBackdropImageView);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bind != null) bind.bioTextView.setText(normalizedBio);
|
if (bind != null) {
|
||||||
|
String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography()).trim();
|
||||||
|
String lastFmUrl = artistInfo.getLastFmUrl();
|
||||||
|
|
||||||
if (bind != null) bind.bioMoreTextViewClickable.setOnClickListener(v -> {
|
if (normalizedBio.isEmpty()) {
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
bind.bioTextView.setVisibility(View.GONE);
|
||||||
intent.setData(Uri.parse(artistInfo.getLastFmUrl()));
|
} else {
|
||||||
startActivity(intent);
|
bind.bioTextView.setText(normalizedBio);
|
||||||
});
|
}
|
||||||
|
|
||||||
if (bind != null) bind.artistPageBioSector.setVisibility(View.VISIBLE);
|
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() {
|
private void initPlayButtons() {
|
||||||
bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> {
|
bind.artistPageShuffleButton.setOnClickListener(v -> artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
if (!songs.isEmpty()) {
|
@Override
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
public void onChanged(List<Child> songs) {
|
||||||
activity.setBottomSheetInPeek(true);
|
if (songs != null && !songs.isEmpty()) {
|
||||||
} else {
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show();
|
activity.setBottomSheetInPeek(true);
|
||||||
|
artistPageViewModel.getArtistShuffleList().removeObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
bind.artistPageRadioButton.setOnClickListener(v -> artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||||
if (songs != null && !songs.isEmpty()) {
|
@Override
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
public void onChanged(List<Child> songs) {
|
||||||
activity.setBottomSheetInPeek(true);
|
if (songs != null && !songs.isEmpty()) {
|
||||||
} else {
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_radio), Toast.LENGTH_SHORT).show();
|
activity.setBottomSheetInPeek(true);
|
||||||
|
artistPageViewModel.getArtistInstantMix().removeObserver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -243,8 +286,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initAlbumsView() {
|
private void initAlbumsView() {
|
||||||
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.albumsRecyclerView.setHasFixedSize(true);
|
bind.albumsRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
|
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
|
||||||
@@ -262,8 +305,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initSimilarArtistsView() {
|
private void initSimilarArtistsView() {
|
||||||
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
|
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.similarArtistsRecyclerView.setHasFixedSize(true);
|
bind.similarArtistsRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);
|
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||||
activity.setBottomSheetVisibility(true);
|
activity.setBottomSheetVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,26 @@ import com.cappielloantonio.tempo.R
|
|||||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||||
import com.cappielloantonio.tempo.service.BaseMediaService
|
import com.cappielloantonio.tempo.service.BaseMediaService
|
||||||
import com.cappielloantonio.tempo.service.MediaService
|
import com.cappielloantonio.tempo.service.MediaService
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||||
import com.cappielloantonio.tempo.util.Preferences
|
import com.cappielloantonio.tempo.util.Preferences
|
||||||
|
|
||||||
class EqualizerFragment : Fragment() {
|
class EqualizerFragment : Fragment() {
|
||||||
|
|
||||||
|
private lateinit var activity: MainActivity
|
||||||
private var equalizerManager: EqualizerManager? = null
|
private var equalizerManager: EqualizerManager? = null
|
||||||
private lateinit var eqBandsContainer: LinearLayout
|
private lateinit var eqBandsContainer: LinearLayout
|
||||||
private lateinit var eqSwitch: Switch
|
private lateinit var eqSwitch: Switch
|
||||||
private lateinit var resetButton: Button
|
private lateinit var resetButton: Button
|
||||||
private lateinit var safeSpace: Space
|
private lateinit var safeSpace: Space
|
||||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||||
|
|
||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
activity = requireActivity() as MainActivity
|
||||||
|
}
|
||||||
|
|
||||||
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
private val equalizerUpdatedReceiver = object : BroadcastReceiver() {
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
@@ -73,6 +81,8 @@ class EqualizerFragment : Fragment() {
|
|||||||
)
|
)
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
}
|
}
|
||||||
|
val showBottomBar = !Preferences.getHideBottomNavbarOnPortrait()
|
||||||
|
activity.setBottomNavigationBarVisibility(showBottomBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
@@ -32,6 +33,8 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
|||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.GenreCatalogueAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
|
import com.cappielloantonio.tempo.viewmodel.GenreCatalogueViewModel;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
@@ -42,6 +45,9 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
private GenreCatalogueAdapter genreCatalogueAdapter;
|
private GenreCatalogueAdapter genreCatalogueAdapter;
|
||||||
|
|
||||||
|
private int spanCount = 2;
|
||||||
|
private int tileSpacing = 20;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -56,6 +62,10 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
View view = bind.getRoot();
|
View view = bind.getRoot();
|
||||||
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
|
genreCatalogueViewModel = new ViewModelProvider(requireActivity()).get(GenreCatalogueViewModel.class);
|
||||||
|
|
||||||
|
TileSizeManager.getInstance().calculateGenreSize( requireContext() );
|
||||||
|
spanCount = TileSizeManager.getInstance().getGenreSpanCount( requireContext() );
|
||||||
|
tileSpacing = TileSizeManager.getInstance().getGenreSpacing( requireContext() );
|
||||||
|
|
||||||
init();
|
init();
|
||||||
initAppBar();
|
initAppBar();
|
||||||
initGenreCatalogueView();
|
initGenreCatalogueView();
|
||||||
@@ -97,8 +107,8 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initGenreCatalogueView() {
|
private void initGenreCatalogueView() {
|
||||||
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
|
bind.genreCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount));
|
||||||
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 16, false));
|
bind.genreCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, tileSpacing, false));
|
||||||
bind.genreCatalogueRecyclerView.setHasFixedSize(true);
|
bind.genreCatalogueRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
genreCatalogueAdapter = new GenreCatalogueAdapter(this);
|
genreCatalogueAdapter = new GenreCatalogueAdapter(this);
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class HomeFragment extends Fragment {
|
|||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||||
activity.setBottomSheetVisibility(true);
|
activity.setBottomSheetVisibility(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -62,6 +64,7 @@ import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
|||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.TileSizeManager;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
@@ -680,11 +683,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
private void initDiscoverSongSlideView() {
|
private void initDiscoverSongSlideView() {
|
||||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||||
|
|
||||||
bind.discoverSongViewPager.setOrientation(ViewPager2.ORIENTATION_HORIZONTAL);
|
bind.discoverSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||||
|
bind.discoverSongRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
discoverSongAdapter = new DiscoverSongAdapter(this);
|
discoverSongAdapter = new DiscoverSongAdapter(this);
|
||||||
bind.discoverSongViewPager.setAdapter(discoverSongAdapter);
|
bind.discoverSongRecyclerView.setAdapter(discoverSongAdapter);
|
||||||
bind.discoverSongViewPager.setOffscreenPageLimit(1);
|
|
||||||
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
||||||
MusicUtil.ratingFilter(songs);
|
MusicUtil.ratingFilter(songs);
|
||||||
|
|
||||||
@@ -697,8 +701,6 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
discoverSongAdapter.setItems(songs);
|
discoverSongAdapter.setItems(songs);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setSlideViewOffset(bind.discoverSongViewPager, 20, 16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSimilarSongView() {
|
private void initSimilarSongView() {
|
||||||
@@ -1253,20 +1255,25 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMediaClick(Bundle bundle) {
|
public void onMediaClick(Bundle bundle) {
|
||||||
if (bundle.containsKey(Constants.MEDIA_MIX)) {
|
if (bundle.containsKey(Constants.MEDIA_MIX)) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelable(Constants.TRACK_OBJECT));
|
Child track = bundle.getParcelable(Constants.TRACK_OBJECT);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
|
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
homeViewModel.getMediaInstantMix(getViewLifecycleOwner(), bundle.getParcelable(Constants.TRACK_OBJECT)).observe(getViewLifecycleOwner(), songs -> {
|
final boolean[] playbackStarted = {false};
|
||||||
MusicUtil.ratingFilter(songs);
|
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()) {
|
new Handler(Looper.getMainLooper()).postDelayed(() -> {
|
||||||
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
|
if (playbackStarted[0]) return;
|
||||||
}
|
|
||||||
});
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
|
playbackStarted[0] = true;
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) {
|
} else if (bundle.containsKey(Constants.MEDIA_CHRONOLOGY)) {
|
||||||
List<Child> media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT);
|
List<Child> media = bundle.getParcelableArrayList(Constants.TRACKS_OBJECT);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.session.MediaBrowser;
|
import androidx.media3.session.MediaBrowser;
|
||||||
@@ -16,8 +18,11 @@ import androidx.media3.session.SessionToken;
|
|||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding;
|
import com.cappielloantonio.tempo.databinding.FragmentLibraryBinding;
|
||||||
@@ -43,6 +48,7 @@ import java.util.Objects;
|
|||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class LibraryFragment extends Fragment implements ClickCallback {
|
public class LibraryFragment extends Fragment implements ClickCallback {
|
||||||
private static final String TAG = "LibraryFragment";
|
private static final String TAG = "LibraryFragment";
|
||||||
|
private static final String TOAST_MSG = "Long press to refresh" ;
|
||||||
|
|
||||||
private FragmentLibraryBinding bind;
|
private FragmentLibraryBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
@@ -81,13 +87,14 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
initArtistView();
|
initArtistView();
|
||||||
initGenreView();
|
initGenreView();
|
||||||
initPlaylistView();
|
initPlaylistView();
|
||||||
|
initSwipeToRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
initializeMediaBrowser();
|
initializeMediaBrowser();
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
activity.toggleBottomNavigationBarVisibilityOnOrientationChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -103,6 +110,11 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
|
bind.songCatalogueTextViewClickable.setOnClickListener(v -> {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString(Constants.MEDIA_ALL, Constants.MEDIA_ALL);
|
||||||
|
activity.navController.navigate(R.id.action_libraryFragment_to_songListPageFragment, bundle);
|
||||||
|
});
|
||||||
bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment));
|
bind.albumCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_albumCatalogueFragment));
|
||||||
bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment));
|
bind.artistCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_artistCatalogueFragment));
|
||||||
bind.genreCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_genreCatalogueFragment));
|
bind.genreCatalogueTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_libraryFragment_to_genreCatalogueFragment));
|
||||||
@@ -112,22 +124,41 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle);
|
activity.navController.navigate(R.id.action_libraryFragment_to_playlistCatalogueFragment, bundle);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Album
|
||||||
bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.albumCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshAlbumSample(getViewLifecycleOwner());
|
libraryViewModel.refreshAlbumSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.albumCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Artist
|
||||||
bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.artistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshArtistSample(getViewLifecycleOwner());
|
libraryViewModel.refreshArtistSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.artistCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Genre
|
||||||
bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.genreCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshGenreSample(getViewLifecycleOwner());
|
libraryViewModel.refreshGenreSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.genreCatalogueSampleTextViewRefreshable.setOnClickListener(v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Playlist
|
||||||
bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
bind.playlistCatalogueSampleTextViewRefreshable.setOnLongClickListener(view -> {
|
||||||
libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner());
|
libraryViewModel.refreshPlaylistSample(getViewLifecycleOwner());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
bind.playlistCatalogueSampleTextViewRefreshable.setOnClickListener( v ->
|
||||||
|
Toast.makeText(requireContext(), TOAST_MSG, Toast.LENGTH_SHORT).show()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initAppBar() {
|
private void initAppBar() {
|
||||||
@@ -304,4 +335,20 @@ public class LibraryFragment extends Fragment implements ClickCallback {
|
|||||||
private void initializeMediaBrowser() {
|
private void initializeMediaBrowser() {
|
||||||
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void initSwipeToRefresh() {
|
||||||
|
bind.swipeLibraryToRefresh.setOnRefreshListener(() -> {
|
||||||
|
pullToRefresh();
|
||||||
|
bind.swipeLibraryToRefresh.setRefreshing(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pullToRefresh() {
|
||||||
|
LifecycleOwner lifecycleOwner = getViewLifecycleOwner();
|
||||||
|
libraryViewModel.refreshAlbumSample(lifecycleOwner);
|
||||||
|
libraryViewModel.refreshGenreSample(lifecycleOwner);
|
||||||
|
libraryViewModel.refreshArtistSample(lifecycleOwner);
|
||||||
|
libraryViewModel.refreshPlaylistSample(lifecycleOwner);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
|||||||
@Override
|
@Override
|
||||||
public void onServerClick(Bundle bundle) {
|
public void onServerClick(Bundle bundle) {
|
||||||
Server server = bundle.getParcelable("server_object");
|
Server server = bundle.getParcelable("server_object");
|
||||||
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity());
|
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity(), server.getClientCert());
|
||||||
|
|
||||||
SystemRepository systemRepository = new SystemRepository();
|
SystemRepository systemRepository = new SystemRepository();
|
||||||
systemRepository.checkUserCredential(new SystemCallback() {
|
systemRepository.checkUserCredential(new SystemCallback() {
|
||||||
@@ -142,13 +142,14 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
|||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity) {
|
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity, String clientCert) {
|
||||||
Preferences.setServerId(serverId);
|
Preferences.setServerId(serverId);
|
||||||
Preferences.setServer(server);
|
Preferences.setServer(server);
|
||||||
Preferences.setLocalAddress(localAddress);
|
Preferences.setLocalAddress(localAddress);
|
||||||
Preferences.setUser(user);
|
Preferences.setUser(user);
|
||||||
Preferences.setPassword(password);
|
Preferences.setPassword(password);
|
||||||
Preferences.setLowSecurity(isLowSecurity);
|
Preferences.setLowSecurity(isLowSecurity);
|
||||||
|
Preferences.setClientCert(clientCert);
|
||||||
|
|
||||||
App.getSubsonicClientInstance(true);
|
App.getSubsonicClientInstance(true);
|
||||||
}
|
}
|
||||||
@@ -161,6 +162,7 @@ public class LoginFragment extends Fragment implements ClickCallback {
|
|||||||
Preferences.setToken(null);
|
Preferences.setToken(null);
|
||||||
Preferences.setSalt(null);
|
Preferences.setSalt(null);
|
||||||
Preferences.setLowSecurity(false);
|
Preferences.setLowSecurity(false);
|
||||||
|
Preferences.setClientCert(null);
|
||||||
|
|
||||||
App.getSubsonicClientInstance(true);
|
App.getSubsonicClientInstance(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -173,25 +174,54 @@ public class PlayerBottomSheetFragment extends Fragment {
|
|||||||
playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId"));
|
playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId"));
|
||||||
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
|
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
|
||||||
|
|
||||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
String type = mediaMetadata.extras.getString("type");
|
||||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
|
||||||
mediaMetadata.artist != null
|
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||||
? mediaMetadata.artist
|
// For radio: keep header consistent with full player
|
||||||
: Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
String stationName = mediaMetadata.extras.getString(
|
||||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
"stationName",
|
||||||
: "");
|
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : ""
|
||||||
|
);
|
||||||
|
|
||||||
|
String artist = mediaMetadata.extras.getString("radioArtist", "");
|
||||||
|
String title = mediaMetadata.extras.getString("radioTitle", "");
|
||||||
|
|
||||||
|
String mainTitle;
|
||||||
|
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||||
|
mainTitle = artist + " - " + title;
|
||||||
|
} else if (!TextUtils.isEmpty(title)) {
|
||||||
|
mainTitle = title;
|
||||||
|
} else if (!TextUtils.isEmpty(artist)) {
|
||||||
|
mainTitle = artist;
|
||||||
|
} else {
|
||||||
|
mainTitle = stationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mainTitle);
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(stationName);
|
||||||
|
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||||
|
} else {
|
||||||
|
// Default (music, podcast, etc.)
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||||
|
mediaMetadata.artist != null
|
||||||
|
? mediaMetadata.artist
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
||||||
|
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
||||||
|
mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), "")
|
||||||
|
? View.VISIBLE
|
||||||
|
: View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);
|
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);
|
||||||
|
|
||||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setVisibility(mediaMetadata.extras.getString("title") != null && !Objects.equals(mediaMetadata.extras.getString("title"), "") ? View.VISIBLE : View.GONE);
|
|
||||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setVisibility(
|
|
||||||
(mediaMetadata.extras.getString("artist") != null && !Objects.equals(mediaMetadata.extras.getString("artist"), ""))
|
|
||||||
|| (Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null)
|
|
||||||
? View.VISIBLE
|
|
||||||
: View.GONE);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ import android.content.ServiceConnection;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
@@ -32,6 +35,10 @@ import androidx.media3.session.SessionToken;
|
|||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.NavOptions;
|
import androidx.navigation.NavOptions;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
import androidx.transition.ChangeBounds;
|
||||||
|
import androidx.transition.Slide;
|
||||||
|
import androidx.transition.TransitionManager;
|
||||||
|
import androidx.transition.TransitionSet;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
@@ -39,6 +46,7 @@ import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBindi
|
|||||||
import com.cappielloantonio.tempo.service.EqualizerManager;
|
import com.cappielloantonio.tempo.service.EqualizerManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
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.RatingDialog;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
||||||
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
||||||
@@ -54,7 +62,6 @@ import com.google.android.material.elevation.SurfaceColors;
|
|||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -213,12 +220,53 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setMetadata(MediaMetadata mediaMetadata) {
|
private void setMetadata(MediaMetadata mediaMetadata) {
|
||||||
|
String type = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type") : null;
|
||||||
|
|
||||||
|
if (Objects.equals(type, Constants.MEDIA_TYPE_RADIO)) {
|
||||||
|
// For radio: always read from extras first (radioArtist, radioTitle, stationName)
|
||||||
|
// MediaMetadata.title/artist are formatted for notification
|
||||||
|
String stationName = mediaMetadata.extras != null
|
||||||
|
? mediaMetadata.extras.getString("stationName",
|
||||||
|
mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "")
|
||||||
|
: mediaMetadata.artist != null ? String.valueOf(mediaMetadata.artist) : "";
|
||||||
|
|
||||||
|
String artist = mediaMetadata.extras != null
|
||||||
|
? mediaMetadata.extras.getString("radioArtist", "")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
String title = mediaMetadata.extras != null
|
||||||
|
? mediaMetadata.extras.getString("radioTitle", "")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
// Format: "Artist - Song" or fallback to title or station name
|
||||||
|
String mainTitle;
|
||||||
|
if (!TextUtils.isEmpty(artist) && !TextUtils.isEmpty(title)) {
|
||||||
|
mainTitle = artist + " - " + title;
|
||||||
|
} else if (!TextUtils.isEmpty(title)) {
|
||||||
|
mainTitle = title;
|
||||||
|
} else if (!TextUtils.isEmpty(artist)) {
|
||||||
|
mainTitle = artist;
|
||||||
|
} else {
|
||||||
|
mainTitle = stationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
playerMediaTitleLabel.setText(mainTitle);
|
||||||
|
playerArtistNameLabel.setText(stationName);
|
||||||
|
|
||||||
|
playerMediaTitleLabel.setSelected(true);
|
||||||
|
playerArtistNameLabel.setSelected(true);
|
||||||
|
|
||||||
|
playerMediaTitleLabel.setVisibility(!TextUtils.isEmpty(mainTitle) ? View.VISIBLE : View.GONE);
|
||||||
|
playerArtistNameLabel.setVisibility(!TextUtils.isEmpty(stationName) ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
updateAssetLinkChips(mediaMetadata);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
|
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
|
||||||
playerArtistNameLabel.setText(
|
playerArtistNameLabel.setText(
|
||||||
mediaMetadata.artist != null
|
mediaMetadata.artist != null
|
||||||
? String.valueOf(mediaMetadata.artist)
|
? String.valueOf(mediaMetadata.artist)
|
||||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
|
||||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
playerMediaTitleLabel.setSelected(true);
|
playerMediaTitleLabel.setSelected(true);
|
||||||
@@ -235,41 +283,80 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
||||||
|
boolean isLocal = false;
|
||||||
|
|
||||||
|
if (mediaBrowserListenableFuture != null && mediaBrowserListenableFuture.isDone()) {
|
||||||
|
try {
|
||||||
|
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||||
|
if (browser != null && browser.getCurrentMediaItem() != null) {
|
||||||
|
android.net.Uri currentUri = browser.getCurrentMediaItem().requestMetadata.mediaUri;
|
||||||
|
if (currentUri != null) {
|
||||||
|
String scheme = currentUri.getScheme();
|
||||||
|
isLocal = "content".equals(scheme) || "file".equals(scheme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("DEBUG_PLAYER", "Error getting browser for UI update", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaMetadata.extras != null) {
|
if (mediaMetadata.extras != null) {
|
||||||
String extension = mediaMetadata.extras.getString("suffix", getString(R.string.player_unknown_format));
|
String extension = mediaMetadata.extras.getString("suffix", getString(R.string.player_unknown_format));
|
||||||
String bitrate = mediaMetadata.extras.getInt("bitrate", 0) != 0 ? mediaMetadata.extras.getInt("bitrate", 0) + "kbps" : "Original";
|
int rawBitrate = mediaMetadata.extras.getInt("bitrate", 0);
|
||||||
String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? new DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : "";
|
String bitrate = rawBitrate != 0 ? rawBitrate + "kbps" : "Original";
|
||||||
|
String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ?
|
||||||
|
new java.text.DecimalFormat("0.#").format(mediaMetadata.extras.getInt("samplingRate", 0) / 1000.0) + "kHz" : "";
|
||||||
String bitDepth = mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + "b" : "";
|
String bitDepth = mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + "b" : "";
|
||||||
|
|
||||||
playerMediaExtension.setText(extension);
|
playerMediaExtension.setText(extension);
|
||||||
|
|
||||||
if (bitrate.equals("Original")) {
|
if (bitrate.equals("Original") && !isLocal) {
|
||||||
playerMediaBitrate.setVisibility(View.GONE);
|
playerMediaBitrate.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
List<String> mediaQualityItems = new ArrayList<>();
|
List<String> items = new ArrayList<>();
|
||||||
|
if (!bitrate.trim().isEmpty()) items.add(bitrate);
|
||||||
if (!bitrate.trim().isEmpty()) mediaQualityItems.add(bitrate);
|
if (!bitDepth.trim().isEmpty()) items.add(bitDepth);
|
||||||
if (!bitDepth.trim().isEmpty()) mediaQualityItems.add(bitDepth);
|
if (!samplingRate.trim().isEmpty()) items.add(samplingRate);
|
||||||
if (!samplingRate.trim().isEmpty()) mediaQualityItems.add(samplingRate);
|
String mediaQuality = TextUtils.join(" • ", items);
|
||||||
|
|
||||||
String mediaQuality = TextUtils.join(" • ", mediaQualityItems);
|
playerMediaBitrate.setVisibility(Preferences.getBitrateVisible() ? View.VISIBLE : View.GONE);
|
||||||
playerMediaBitrate.setVisibility(View.VISIBLE);
|
playerMediaBitrate.setText(isLocal ? mediaQuality : mediaQuality);
|
||||||
playerMediaBitrate.setText(mediaQuality);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw");
|
|
||||||
boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0");
|
if (!isLocal) {
|
||||||
|
boolean isTranscodingExtension = !MusicUtil.getTranscodingFormatPreference().equals("raw");
|
||||||
|
boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0");
|
||||||
|
if (isTranscodingExtension || isTranscodingBitrate) {
|
||||||
|
playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")");
|
||||||
|
playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ?
|
||||||
|
MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested));
|
||||||
|
}
|
||||||
|
|
||||||
if (isTranscodingExtension || isTranscodingBitrate) {
|
|
||||||
playerMediaExtension.setText(MusicUtil.getTranscodingFormatPreference() + " (" + getString(R.string.player_transcoding) + ")");
|
|
||||||
playerMediaBitrate.setText(!MusicUtil.getBitratePreference().equals("0") ? MusicUtil.getBitratePreference() + "kbps" : getString(R.string.player_transcoding_requested));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerTrackInfo.setOnClickListener(view -> {
|
playerTrackInfo.setOnClickListener(view -> {
|
||||||
TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata);
|
TrackInfoDialog dialog = new TrackInfoDialog(mediaMetadata);
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
playerMediaExtension.setOnClickListener( v -> toggleBitrateVisibility() );
|
||||||
|
playerMediaBitrate.setOnClickListener(v -> toggleBitrateVisibility() );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleBitrateVisibility() {
|
||||||
|
ViewGroup parent = (ViewGroup) playerMediaBitrate.getParent();
|
||||||
|
|
||||||
|
TransitionSet transition = new TransitionSet()
|
||||||
|
.addTransition(new Slide(Gravity.START))
|
||||||
|
.addTransition(new ChangeBounds())
|
||||||
|
.setDuration(500)
|
||||||
|
.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||||
|
TransitionManager.beginDelayedTransition(parent, transition);
|
||||||
|
|
||||||
|
playerMediaBitrate.setVisibility(Preferences.getBitrateVisible() ? View.GONE : View.VISIBLE);
|
||||||
|
Preferences.setBitrateVisible(!Preferences.getBitrateVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
||||||
@@ -413,10 +500,10 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
bind.getRoot().setShowNextButton(true);
|
bind.getRoot().setShowNextButton(true);
|
||||||
bind.getRoot().setShowFastForwardButton(false);
|
bind.getRoot().setShowFastForwardButton(false);
|
||||||
bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL | RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE);
|
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.player_skip_silence_toggle_button).setVisibility(View.GONE);
|
||||||
bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.VISIBLE);
|
bind.getRoot().findViewById(R.id.button_favorite).setVisibility(View.VISIBLE);
|
||||||
resetPlaybackParameters(mediaBrowser);
|
setPlaybackParameters(mediaBrowser);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,33 +609,12 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
|
|
||||||
private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) {
|
private void initPlaybackSpeedButton(MediaBrowser mediaBrowser) {
|
||||||
playbackSpeedButton.setOnClickListener(view -> {
|
playbackSpeedButton.setOnClickListener(view -> {
|
||||||
float currentSpeed = Preferences.getPlaybackSpeed();
|
PlaybackSpeedDialog dialog = new PlaybackSpeedDialog();
|
||||||
|
dialog.setPlaybackSpeedListener(speed -> {
|
||||||
if (currentSpeed == Constants.MEDIA_PLAYBACK_SPEED_080) {
|
mediaBrowser.setPlaybackParameters(new PlaybackParameters(speed));
|
||||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
playbackSpeedButton.setText(getString(R.string.player_playback_speed, speed));
|
||||||
playbackSpeedButton.setText(getString(R.string.player_playback_speed, Constants.MEDIA_PLAYBACK_SPEED_100));
|
});
|
||||||
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
|
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
skipSilenceToggleButton.setOnClickListener(view -> {
|
skipSilenceToggleButton.setOnClickListener(view -> {
|
||||||
@@ -600,7 +666,7 @@ public class PlayerControllerFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void resetPlaybackParameters(MediaBrowser mediaBrowser) {
|
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
|
// TODO Resettare lo skip del silenzio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import android.os.Handler;
|
|||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
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.text.style.ForegroundColorSpan;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -51,6 +53,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
private Runnable syncLyricsRunnable;
|
private Runnable syncLyricsRunnable;
|
||||||
private String currentLyrics;
|
private String currentLyrics;
|
||||||
private LyricsList currentLyricsList;
|
private LyricsList currentLyricsList;
|
||||||
|
private Integer lastLineIdx;
|
||||||
private String currentDescription;
|
private String currentDescription;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -109,6 +112,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
currentLyrics = null;
|
currentLyrics = null;
|
||||||
currentLyricsList = null;
|
currentLyricsList = null;
|
||||||
currentDescription = null;
|
currentDescription = null;
|
||||||
|
lastLineIdx = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initOverlay() {
|
private void initOverlay() {
|
||||||
@@ -162,6 +166,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
|
|
||||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||||
currentLyricsList = lyricsList;
|
currentLyricsList = lyricsList;
|
||||||
|
lastLineIdx = null;
|
||||||
updatePanelContent();
|
updatePanelContent();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -194,7 +199,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||||
|
|
||||||
if (hasStructuredLyrics(currentLyricsList)) {
|
if (hasStructuredLyrics(currentLyricsList)) {
|
||||||
setSyncLirics(currentLyricsList);
|
setSyncLyrics(currentLyricsList);
|
||||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||||
@@ -241,14 +246,14 @@ public class PlayerLyricsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
@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) {
|
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
|
||||||
if (lines != null) {
|
if (lines != null) {
|
||||||
for (Line line : lines) {
|
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());
|
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||||
|
|
||||||
if (hasStructuredLyrics(lyricsList)) {
|
if (hasStructuredLyrics(lyricsList)) {
|
||||||
StringBuilder lyricsBuilder = new StringBuilder();
|
|
||||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||||
|
if (lines == null || lines.isEmpty()) {
|
||||||
if (lines == null || lines.isEmpty()) return;
|
return;
|
||||||
|
|
||||||
for (Line line : lines) {
|
|
||||||
lyricsBuilder.append(line.getValue().trim()).append("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Line toHighlight = lines.stream().filter(line -> line != null && line.getStart() != null && line.getStart() < timestamp).reduce((first, second) -> second).orElse(null);
|
// Find the index of the currently playing line
|
||||||
|
int curIdx = 0;
|
||||||
if (toHighlight != null) {
|
for (; curIdx < lines.size(); ++curIdx) {
|
||||||
String lyrics = lyricsBuilder.toString();
|
Integer start = lines.get(curIdx).getStart();
|
||||||
Spannable spannableString = new SpannableString(lyrics);
|
if (start != null && start > timestamp) {
|
||||||
|
curIdx--; // Found the first line that starts after the current timestamp
|
||||||
int startingPosition = getStartPosition(lines, toHighlight);
|
break;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getStartPosition(List<Line> lines, Line toHighlight) {
|
// Only update if the highlighted line has changed
|
||||||
int start = 0;
|
if (lastLineIdx != null && curIdx == lastLineIdx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastLineIdx = curIdx;
|
||||||
|
|
||||||
for (Line line : lines) {
|
StringBuilder lyricsBuilder = new StringBuilder();
|
||||||
if (line != toHighlight) {
|
for (Line line : lines) {
|
||||||
start = start + line.getValue().length() + 1;
|
lyricsBuilder.append(line.getValue().trim()).append("\n\n");
|
||||||
} else {
|
}
|
||||||
break;
|
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) {
|
private int getScroll(int startIndex) {
|
||||||
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);
|
|
||||||
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
Layout layout = bind.nowPlayingSongLyricsTextView.getLayout();
|
||||||
if (layout == null) return 0;
|
if (layout == null) return 0;
|
||||||
|
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ public class PlaylistCatalogueFragment extends Fragment implements ClickCallback
|
|||||||
|
|
||||||
popup.setOnMenuItemClickListener(menuItem -> {
|
popup.setOnMenuItemClickListener(menuItem -> {
|
||||||
if (menuItem.getItemId() == R.id.menu_playlist_sort_name) {
|
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;
|
return true;
|
||||||
} else if (menuItem.getItemId() == R.id.menu_playlist_sort_random) {
|
} 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,8 +216,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
||||||
Collections.shuffle(songs);
|
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> shuffledSongs = new java.util.ArrayList<>(songs);
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
java.util.Collections.shuffle(shuffledSongs);
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, shuffledSongs, 0);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,32 +228,33 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
private void initBackCover() {
|
private void initBackCover() {
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
||||||
if (bind != null && songs != null && !songs.isEmpty()) {
|
if (bind != null && songs != null && !songs.isEmpty()) {
|
||||||
Collections.shuffle(songs);
|
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> randomSongs = new java.util.ArrayList<>(songs);
|
||||||
|
java.util.Collections.shuffle(randomSongs);
|
||||||
|
|
||||||
// Pic top-left
|
// Pic top-left
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), !songs.isEmpty() ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), !randomSongs.isEmpty() ? randomSongs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
||||||
.into(bind.playlistCoverImageViewTopLeft);
|
.into(bind.playlistCoverImageViewTopLeft);
|
||||||
|
|
||||||
// Pic top-right
|
// Pic top-right
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 1 ? songs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 1 ? randomSongs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
||||||
.into(bind.playlistCoverImageViewTopRight);
|
.into(bind.playlistCoverImageViewTopRight);
|
||||||
|
|
||||||
// Pic bottom-left
|
// Pic bottom-left
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 2 ? songs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 2 ? randomSongs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
||||||
.into(bind.playlistCoverImageViewBottomLeft);
|
.into(bind.playlistCoverImageViewBottomLeft);
|
||||||
|
|
||||||
// Pic bottom-right
|
// Pic bottom-right
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 3 ? songs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 3 ? randomSongs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
||||||
.into(bind.playlistCoverImageViewBottomRight);
|
.into(bind.playlistCoverImageViewBottomRight);
|
||||||
@@ -271,6 +273,11 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
songHorizontalAdapter.setItems(songs);
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
if (songs != null) {
|
||||||
|
bind.playlistSongCountLabel.setText(getString(R.string.playlist_song_count, songs.size()));
|
||||||
|
long totalDuration = songs.stream().mapToLong(s -> s.getDuration() != null ? s.getDuration() : 0).sum();
|
||||||
|
bind.playlistDurationLabel.setText(getString(R.string.playlist_duration, MusicUtil.getReadableDurationString(totalDuration, false)));
|
||||||
|
}
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -291,6 +298,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaLongClick(Bundle bundle) {
|
public void onMediaLongClick(Bundle bundle) {
|
||||||
|
bundle.putString(Constants.PLAYLIST_ID, playlistPageViewModel.getPlaylist().getId());
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,16 +26,20 @@ import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper;
|
|||||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||||
|
import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class SearchFragment extends Fragment implements ClickCallback {
|
public class SearchFragment extends Fragment implements ClickCallback {
|
||||||
@@ -49,6 +53,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
private ArtistAdapter artistAdapter;
|
private ArtistAdapter artistAdapter;
|
||||||
private AlbumAdapter albumAdapter;
|
private AlbumAdapter albumAdapter;
|
||||||
private SongHorizontalAdapter songHorizontalAdapter;
|
private SongHorizontalAdapter songHorizontalAdapter;
|
||||||
|
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
@@ -126,6 +131,12 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
|
|
||||||
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
||||||
|
|
||||||
|
bind.allsongsview.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
|
bind.allsongsview.setHasFixedSize(true);
|
||||||
|
|
||||||
|
playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this);
|
||||||
|
bind.allsongsview.setAdapter(playlistHorizontalAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSearchView() {
|
private void initSearchView() {
|
||||||
@@ -216,13 +227,23 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
|
|
||||||
public void search(String query) {
|
public void search(String query) {
|
||||||
searchViewModel.setQuery(query);
|
searchViewModel.setQuery(query);
|
||||||
|
bind.allSongs.setText(this.getView().getContext().getString(R.string.search_all_songs_loading));
|
||||||
|
playlistHorizontalAdapter.setItems(Collections.emptyList());
|
||||||
bind.searchBar.setText(query);
|
bind.searchBar.setText(query);
|
||||||
bind.searchView.hide();
|
bind.searchView.hide();
|
||||||
performSearch(query);
|
performSearch(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateUI(List<Playlist> allSongs) {
|
||||||
|
if (!allSongs.isEmpty()) {
|
||||||
|
playlistHorizontalAdapter.setItems(allSongs);
|
||||||
|
} else {
|
||||||
|
playlistHorizontalAdapter.setItems(Collections.emptyList());
|
||||||
|
}
|
||||||
|
bind.allSongs.setText(this.getView().getContext().getString(R.string.search_all_songs_play,String.valueOf(allSongs.getFirst().getName())));
|
||||||
|
}
|
||||||
private void performSearch(String query) {
|
private void performSearch(String query) {
|
||||||
searchViewModel.search3(query).observe(getViewLifecycleOwner(), result -> {
|
searchViewModel.search3(this, query).observe(getViewLifecycleOwner(), result -> {
|
||||||
if (bind != null) {
|
if (bind != null) {
|
||||||
if (result.getArtists() != null) {
|
if (result.getArtists() != null) {
|
||||||
bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE);
|
bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
@@ -281,6 +302,19 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistClick(Bundle bundle) {
|
||||||
|
PlaylistWithSongs playlistWithSongs = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
||||||
|
if (playlistWithSongs != null) {
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, playlistWithSongs.getEntries(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaylistLongClick(Bundle bundle) {
|
||||||
|
Navigation.findNavController(requireView()).navigate(R.id.playlistBottomSheetDialog, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAlbumClick(Bundle bundle) {
|
public void onAlbumClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle);
|
||||||
|
|||||||
@@ -0,0 +1,603 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.ServiceConnection;
|
||||||
|
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;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate;
|
||||||
|
import androidx.core.os.LocaleListCompat;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.BuildConfig;
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
||||||
|
import com.cappielloantonio.tempo.interfaces.ScanCallback;
|
||||||
|
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.DeleteDownloadStorageDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
||||||
|
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
||||||
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.UIUtil;
|
||||||
|
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
public class SettingsContainerFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
private static final String TAG = "SettingsFragment";
|
||||||
|
private MainActivity activity;
|
||||||
|
|
||||||
|
private SettingViewModel settingViewModel;
|
||||||
|
|
||||||
|
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||||
|
|
||||||
|
private MediaService.LocalBinder mediaServiceBinder;
|
||||||
|
private boolean isServiceBound = false;
|
||||||
|
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
equalizerResultLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
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 -> {
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||||
|
Intent data = result.getData();
|
||||||
|
if (data != null) {
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null) {
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
);
|
||||||
|
|
||||||
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
activity = (MainActivity) getActivity();
|
||||||
|
|
||||||
|
View view = super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class);
|
||||||
|
|
||||||
|
if (view != null) {
|
||||||
|
getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
activity.setBottomNavigationBarVisibility(false);
|
||||||
|
activity.setBottomSheetVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
|
checkSystemEqualizer();
|
||||||
|
checkCacheStorage();
|
||||||
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
|
||||||
|
setStreamingCacheSize();
|
||||||
|
setAppLanguage();
|
||||||
|
setVersion();
|
||||||
|
setNetorkPingTimeoutBase();
|
||||||
|
|
||||||
|
actionLogout();
|
||||||
|
actionScan();
|
||||||
|
actionSyncStarredAlbums();
|
||||||
|
actionSyncStarredTracks();
|
||||||
|
actionSyncStarredArtists();
|
||||||
|
actionChangeStreamingCacheStorage();
|
||||||
|
actionChangeDownloadStorage();
|
||||||
|
actionSetDownloadDirectory();
|
||||||
|
actionDeleteDownloadStorage();
|
||||||
|
actionKeepScreenOn();
|
||||||
|
actionAutoDownloadLyrics();
|
||||||
|
actionMiniPlayerHeart();
|
||||||
|
|
||||||
|
bindMediaService();
|
||||||
|
actionAppEqualizer();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||||
|
setPreferencesFromResource(R.xml.global_preferences, rootKey);
|
||||||
|
ListPreference themePreference = findPreference(Preferences.THEME);
|
||||||
|
if (themePreference != null) {
|
||||||
|
themePreference.setOnPreferenceChangeListener(
|
||||||
|
(preference, newValue) -> {
|
||||||
|
String themeOption = (String) newValue;
|
||||||
|
ThemeHelper.applyTheme(themeOption);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkSystemEqualizer() {
|
||||||
|
Preference equalizer = findPreference("system_equalizer");
|
||||||
|
|
||||||
|
if (equalizer == null) return;
|
||||||
|
|
||||||
|
Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
|
||||||
|
|
||||||
|
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||||
|
equalizer.setOnPreferenceClickListener(preference -> {
|
||||||
|
equalizerResultLauncher.launch(intent);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
equalizer.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkCacheStorage() {
|
||||||
|
Preference storage = findPreference("streaming_cache_storage");
|
||||||
|
|
||||||
|
if (storage == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
} else {
|
||||||
|
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkStorage() {
|
||||||
|
Preference storage = findPreference("download_storage");
|
||||||
|
|
||||||
|
if (storage == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
} else {
|
||||||
|
int pref = Preferences.getDownloadStoragePreference();
|
||||||
|
if (pref == 0) {
|
||||||
|
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||||
|
} else if (pref == 1) {
|
||||||
|
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
} else {
|
||||||
|
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception exception) {
|
||||||
|
storage.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkDownloadDirectory() {
|
||||||
|
Preference storage = findPreference("download_storage");
|
||||||
|
Preference directory = findPreference("set_download_directory");
|
||||||
|
|
||||||
|
if (directory == null) return;
|
||||||
|
|
||||||
|
String current = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (current != null) {
|
||||||
|
if (storage != null) storage.setVisible(false);
|
||||||
|
directory.setVisible(true);
|
||||||
|
directory.setIcon(R.drawable.ic_close);
|
||||||
|
directory.setTitle(R.string.settings_clear_download_folder);
|
||||||
|
directory.setSummary(current);
|
||||||
|
} else {
|
||||||
|
if (storage != null) storage.setVisible(true);
|
||||||
|
if (Preferences.getDownloadStoragePreference() == 2) {
|
||||||
|
directory.setVisible(true);
|
||||||
|
directory.setIcon(R.drawable.ic_folder);
|
||||||
|
directory.setTitle(R.string.settings_set_download_folder);
|
||||||
|
directory.setSummary(R.string.settings_choose_download_folder);
|
||||||
|
} else {
|
||||||
|
directory.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
if (streamingCachePreference != null) {
|
||||||
|
streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider<ListPreference>() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public CharSequence provideSummary(@NonNull ListPreference preference) {
|
||||||
|
CharSequence entry = preference.getEntry();
|
||||||
|
|
||||||
|
if (entry == null) return null;
|
||||||
|
|
||||||
|
long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024);
|
||||||
|
|
||||||
|
return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAppLanguage() {
|
||||||
|
ListPreference localePref = (ListPreference) findPreference("language");
|
||||||
|
|
||||||
|
Map<String, String> locales = UIUtil.getLangPreferenceDropdownEntries(requireContext());
|
||||||
|
|
||||||
|
CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]);
|
||||||
|
CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]);
|
||||||
|
|
||||||
|
localePref.setEntries(entries);
|
||||||
|
localePref.setEntryValues(entryValues);
|
||||||
|
|
||||||
|
String value = localePref.getValue();
|
||||||
|
if ("default".equals(value)) {
|
||||||
|
localePref.setSummary(requireContext().getString(R.string.settings_system_language));
|
||||||
|
} else {
|
||||||
|
localePref.setSummary(Locale.forLanguageTag(value).getDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
localePref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if ("default".equals(newValue)) {
|
||||||
|
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
|
||||||
|
preference.setSummary(requireContext().getString(R.string.settings_system_language));
|
||||||
|
} else {
|
||||||
|
LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue);
|
||||||
|
AppCompatDelegate.setApplicationLocales(appLocale);
|
||||||
|
preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVersion() {
|
||||||
|
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionLogout() {
|
||||||
|
findPreference("logout").setOnPreferenceClickListener(preference -> {
|
||||||
|
activity.quit();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionScan() {
|
||||||
|
findPreference("scan_library").setOnPreferenceClickListener(preference -> {
|
||||||
|
settingViewModel.launchScan(new ScanCallback() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception exception) {
|
||||||
|
findPreference("scan_library").setSummary(exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(boolean isScanning, long count) {
|
||||||
|
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||||
|
if (isScanning) getScanStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredTracks() {
|
||||||
|
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredSyncDialog dialog = new StarredSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredAlbums() {
|
||||||
|
findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSyncStarredArtists() {
|
||||||
|
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
|
||||||
|
((SwitchPreference)preference).setChecked(false);
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionChangeStreamingCacheStorage() {
|
||||||
|
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
||||||
|
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPositiveClick() {
|
||||||
|
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNegativeClick() {
|
||||||
|
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionChangeDownloadStorage() {
|
||||||
|
findPreference("download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
|
DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPositiveClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNegativeClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNeutralClick() {
|
||||||
|
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionSetDownloadDirectory() {
|
||||||
|
Preference pref = findPreference("set_download_directory");
|
||||||
|
if (pref != null) {
|
||||||
|
pref.setOnPreferenceClickListener(preference -> {
|
||||||
|
String current = Preferences.getDownloadDirectoryUri();
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
Preferences.setDownloadDirectoryUri(null);
|
||||||
|
Preferences.setDownloadStoragePreference(0);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show();
|
||||||
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
} else {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
directoryPickerLauncher.launch(intent);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionDeleteDownloadStorage() {
|
||||||
|
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
|
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionMiniPlayerHeart() {
|
||||||
|
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionAutoDownloadLyrics() {
|
||||||
|
SwitchPreference preference = findPreference("auto_download_lyrics");
|
||||||
|
if (preference == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
||||||
|
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getScanStatus() {
|
||||||
|
settingViewModel.getScanStatus(new ScanCallback() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception exception) {
|
||||||
|
findPreference("scan_library").setSummary(exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(boolean isScanning, long count) {
|
||||||
|
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||||
|
if (isScanning) getScanStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionKeepScreenOn() {
|
||||||
|
findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
if (newValue instanceof Boolean) {
|
||||||
|
if ((Boolean) newValue) {
|
||||||
|
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
} else {
|
||||||
|
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
|
@Override
|
||||||
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||||
|
mediaServiceBinder = (MediaService.LocalBinder) service;
|
||||||
|
isServiceBound = true;
|
||||||
|
checkEqualizerBands();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServiceDisconnected(ComponentName name) {
|
||||||
|
mediaServiceBinder = null;
|
||||||
|
isServiceBound = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void bindMediaService() {
|
||||||
|
Intent intent = new Intent(requireActivity(), MediaService.class);
|
||||||
|
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
|
||||||
|
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||||
|
isServiceBound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkEqualizerBands() {
|
||||||
|
if (mediaServiceBinder != null) {
|
||||||
|
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
|
||||||
|
short numBands = eqManager.getNumberOfBands();
|
||||||
|
Preference appEqualizer = findPreference("app_equalizer");
|
||||||
|
if (appEqualizer != null) {
|
||||||
|
appEqualizer.setVisible(numBands > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void actionAppEqualizer() {
|
||||||
|
Preference appEqualizer = findPreference("app_equalizer");
|
||||||
|
if (appEqualizer != null) {
|
||||||
|
appEqualizer.setOnPreferenceClickListener(preference -> {
|
||||||
|
NavController navController = NavHostFragment.findNavController(this);
|
||||||
|
NavOptions navOptions = new NavOptions.Builder()
|
||||||
|
.setLaunchSingleTop(true)
|
||||||
|
.setPopUpTo(R.id.equalizerFragment, true)
|
||||||
|
.build();
|
||||||
|
activity.setBottomNavigationBarVisibility(true);
|
||||||
|
activity.setBottomSheetVisibility(true);
|
||||||
|
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
if (isServiceBound) {
|
||||||
|
requireActivity().unbindService(serviceConnection);
|
||||||
|
isServiceBound = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,125 +1,61 @@
|
|||||||
package com.cappielloantonio.tempo.ui.fragment;
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
import android.app.Activity;
|
import static com.google.android.material.internal.ViewUtils.hideKeyboard;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.ServiceConnection;
|
|
||||||
import android.media.audiofx.AudioEffect;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.OptIn;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.appcompat.app.AppCompatDelegate;
|
|
||||||
import androidx.core.os.LocaleListCompat;
|
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
|
||||||
import androidx.media3.common.util.UnstableApi;
|
|
||||||
import androidx.navigation.NavController;
|
|
||||||
import androidx.navigation.NavOptions;
|
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
|
||||||
import androidx.preference.ListPreference;
|
|
||||||
import androidx.preference.Preference;
|
|
||||||
import androidx.preference.PreferenceCategory;
|
|
||||||
import androidx.preference.PreferenceFragmentCompat;
|
|
||||||
import androidx.preference.SwitchPreference;
|
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.BuildConfig;
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding;
|
||||||
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
|
||||||
import com.cappielloantonio.tempo.interfaces.ScanCallback;
|
|
||||||
import com.cappielloantonio.tempo.service.EqualizerManager;
|
|
||||||
import com.cappielloantonio.tempo.service.MediaService;
|
|
||||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
|
|
||||||
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.util.UIUtil;
|
|
||||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
|
||||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
public class SettingsFragment extends Fragment {
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
|
||||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
|
||||||
private static final String TAG = "SettingsFragment";
|
|
||||||
|
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SettingViewModel settingViewModel;
|
private FragmentSettingsBinding bind;
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
|
||||||
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
|
||||||
|
|
||||||
private MediaService.LocalBinder mediaServiceBinder;
|
|
||||||
private boolean isServiceBound = false;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
equalizerResultLauncher = registerForActivityResult(
|
activity = (MainActivity) getActivity();
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
|
||||||
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 -> {
|
|
||||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
||||||
Intent data = result.getData();
|
|
||||||
if (data != null) {
|
|
||||||
Uri uri = data.getData();
|
|
||||||
if (uri != null) {
|
|
||||||
requireContext().getContentResolver().takePersistableUriPermission(
|
|
||||||
uri,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
||||||
);
|
|
||||||
|
|
||||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
|
||||||
ExternalAudioReader.refreshCache();
|
|
||||||
Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show();
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
activity = (MainActivity) getActivity();
|
bind = FragmentSettingsBinding.inflate(inflater,container,false);
|
||||||
|
View view = bind.getRoot();
|
||||||
|
|
||||||
View view = super.onCreateView(inflater, container, savedInstanceState);
|
initAppBar();
|
||||||
settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class);
|
|
||||||
|
|
||||||
if (view != null) {
|
|
||||||
getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
|
// Add the PreferenceFragment only the first time
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
SettingsContainerFragment prefFragment = new SettingsContainerFragment();
|
||||||
|
|
||||||
|
// Use the child fragment manager so the PreferenceFragment is scoped to this fragment
|
||||||
|
getChildFragmentManager()
|
||||||
|
.beginTransaction()
|
||||||
|
.replace(R.id.settings_container, prefFragment)
|
||||||
|
.setReorderingAllowed(true) // optional but recommended
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -127,454 +63,25 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||||||
super.onStart();
|
super.onStart();
|
||||||
activity.setBottomNavigationBarVisibility(false);
|
activity.setBottomNavigationBarVisibility(false);
|
||||||
activity.setBottomSheetVisibility(false);
|
activity.setBottomSheetVisibility(false);
|
||||||
}
|
activity.setNavigationDrawerLock(true);
|
||||||
|
activity.setSystemBarsVisibility(!activity.isLandscape);
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
checkSystemEqualizer();
|
|
||||||
checkCacheStorage();
|
|
||||||
checkStorage();
|
|
||||||
checkDownloadDirectory();
|
|
||||||
|
|
||||||
setStreamingCacheSize();
|
|
||||||
setAppLanguage();
|
|
||||||
setVersion();
|
|
||||||
|
|
||||||
actionLogout();
|
|
||||||
actionScan();
|
|
||||||
actionSyncStarredAlbums();
|
|
||||||
actionSyncStarredTracks();
|
|
||||||
actionSyncStarredArtists();
|
|
||||||
actionChangeStreamingCacheStorage();
|
|
||||||
actionChangeDownloadStorage();
|
|
||||||
actionSetDownloadDirectory();
|
|
||||||
actionDeleteDownloadStorage();
|
|
||||||
actionKeepScreenOn();
|
|
||||||
actionAutoDownloadLyrics();
|
|
||||||
actionMiniPlayerHeart();
|
|
||||||
|
|
||||||
bindMediaService();
|
|
||||||
actionAppEqualizer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
activity.setBottomSheetVisibility(true);
|
activity.setBottomSheetVisibility(true);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if (activity.isLandscape) {
|
||||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
activity.setNavigationDrawerLock(false);
|
||||||
setPreferencesFromResource(R.xml.global_preferences, rootKey);
|
} else if (Preferences.getEnableDrawerOnPortrait()) {
|
||||||
ListPreference themePreference = findPreference(Preferences.THEME);
|
activity.setNavigationDrawerLock(false);
|
||||||
if (themePreference != null) {
|
|
||||||
themePreference.setOnPreferenceChangeListener(
|
|
||||||
(preference, newValue) -> {
|
|
||||||
String themeOption = (String) newValue;
|
|
||||||
ThemeHelper.applyTheme(themeOption);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkSystemEqualizer() {
|
private void initAppBar() {
|
||||||
Preference equalizer = findPreference("system_equalizer");
|
bind.settingsToolbar.setNavigationOnClickListener(v -> {
|
||||||
|
activity.navController.navigateUp();
|
||||||
if (equalizer == null) return;
|
|
||||||
|
|
||||||
Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL);
|
|
||||||
|
|
||||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
|
||||||
equalizer.setOnPreferenceClickListener(preference -> {
|
|
||||||
equalizerResultLauncher.launch(intent);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
equalizer.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkCacheStorage() {
|
|
||||||
Preference storage = findPreference("streaming_cache_storage");
|
|
||||||
|
|
||||||
if (storage == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
} else {
|
|
||||||
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkStorage() {
|
|
||||||
Preference storage = findPreference("download_storage");
|
|
||||||
|
|
||||||
if (storage == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
} else {
|
|
||||||
int pref = Preferences.getDownloadStoragePreference();
|
|
||||||
if (pref == 0) {
|
|
||||||
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
|
||||||
} else if (pref == 1) {
|
|
||||||
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
|
||||||
} else {
|
|
||||||
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
|
||||||
storage.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkDownloadDirectory() {
|
|
||||||
Preference storage = findPreference("download_storage");
|
|
||||||
Preference directory = findPreference("set_download_directory");
|
|
||||||
|
|
||||||
if (directory == null) return;
|
|
||||||
|
|
||||||
String current = Preferences.getDownloadDirectoryUri();
|
|
||||||
if (current != null) {
|
|
||||||
if (storage != null) storage.setVisible(false);
|
|
||||||
directory.setVisible(true);
|
|
||||||
directory.setIcon(R.drawable.ic_close);
|
|
||||||
directory.setTitle(R.string.settings_clear_download_folder);
|
|
||||||
directory.setSummary(current);
|
|
||||||
} else {
|
|
||||||
if (storage != null) storage.setVisible(true);
|
|
||||||
if (Preferences.getDownloadStoragePreference() == 2) {
|
|
||||||
directory.setVisible(true);
|
|
||||||
directory.setIcon(R.drawable.ic_folder);
|
|
||||||
directory.setTitle(R.string.settings_set_download_folder);
|
|
||||||
directory.setSummary(R.string.settings_choose_download_folder);
|
|
||||||
} else {
|
|
||||||
directory.setVisible(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setStreamingCacheSize() {
|
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
|
||||||
|
|
||||||
if (streamingCachePreference != null) {
|
|
||||||
streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider<ListPreference>() {
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public CharSequence provideSummary(@NonNull ListPreference preference) {
|
|
||||||
CharSequence entry = preference.getEntry();
|
|
||||||
|
|
||||||
if (entry == null) return null;
|
|
||||||
|
|
||||||
long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024);
|
|
||||||
|
|
||||||
return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setAppLanguage() {
|
|
||||||
ListPreference localePref = (ListPreference) findPreference("language");
|
|
||||||
|
|
||||||
Map<String, String> locales = UIUtil.getLangPreferenceDropdownEntries(requireContext());
|
|
||||||
|
|
||||||
CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]);
|
|
||||||
CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]);
|
|
||||||
|
|
||||||
localePref.setEntries(entries);
|
|
||||||
localePref.setEntryValues(entryValues);
|
|
||||||
|
|
||||||
String value = localePref.getValue();
|
|
||||||
if ("default".equals(value)) {
|
|
||||||
localePref.setSummary(requireContext().getString(R.string.settings_system_language));
|
|
||||||
} else {
|
|
||||||
localePref.setSummary(Locale.forLanguageTag(value).getDisplayName());
|
|
||||||
}
|
|
||||||
|
|
||||||
localePref.setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if ("default".equals(newValue)) {
|
|
||||||
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
|
|
||||||
preference.setSummary(requireContext().getString(R.string.settings_system_language));
|
|
||||||
} else {
|
|
||||||
LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue);
|
|
||||||
AppCompatDelegate.setApplicationLocales(appLocale);
|
|
||||||
preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName());
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setVersion() {
|
|
||||||
findPreference("version").setSummary(BuildConfig.VERSION_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionLogout() {
|
|
||||||
findPreference("logout").setOnPreferenceClickListener(preference -> {
|
|
||||||
activity.quit();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionScan() {
|
|
||||||
findPreference("scan_library").setOnPreferenceClickListener(preference -> {
|
|
||||||
settingViewModel.launchScan(new ScanCallback() {
|
|
||||||
@Override
|
|
||||||
public void onError(Exception exception) {
|
|
||||||
findPreference("scan_library").setSummary(exception.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSuccess(boolean isScanning, long count) {
|
|
||||||
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
|
||||||
if (isScanning) getScanStatus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSyncStarredTracks() {
|
|
||||||
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
StarredSyncDialog dialog = new StarredSyncDialog(() -> {
|
|
||||||
((SwitchPreference)preference).setChecked(false);
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSyncStarredAlbums() {
|
|
||||||
findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> {
|
|
||||||
((SwitchPreference)preference).setChecked(false);
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSyncStarredArtists() {
|
|
||||||
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
|
|
||||||
((SwitchPreference)preference).setChecked(false);
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionChangeStreamingCacheStorage() {
|
|
||||||
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
|
||||||
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPositiveClick() {
|
|
||||||
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNegativeClick() {
|
|
||||||
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionChangeDownloadStorage() {
|
|
||||||
findPreference("download_storage").setOnPreferenceClickListener(preference -> {
|
|
||||||
DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPositiveClick() {
|
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNegativeClick() {
|
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNeutralClick() {
|
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
|
||||||
checkDownloadDirectory();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionSetDownloadDirectory() {
|
|
||||||
Preference pref = findPreference("set_download_directory");
|
|
||||||
if (pref != null) {
|
|
||||||
pref.setOnPreferenceClickListener(preference -> {
|
|
||||||
String current = Preferences.getDownloadDirectoryUri();
|
|
||||||
|
|
||||||
if (current != null) {
|
|
||||||
Preferences.setDownloadDirectoryUri(null);
|
|
||||||
Preferences.setDownloadStoragePreference(0);
|
|
||||||
ExternalAudioReader.refreshCache();
|
|
||||||
Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show();
|
|
||||||
checkStorage();
|
|
||||||
checkDownloadDirectory();
|
|
||||||
} else {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
|
||||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
|
||||||
directoryPickerLauncher.launch(intent);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionDeleteDownloadStorage() {
|
|
||||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
|
||||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
|
||||||
dialog.show(activity.getSupportFragmentManager(), null);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionMiniPlayerHeart() {
|
|
||||||
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
|
|
||||||
if (preference == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
|
|
||||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionAutoDownloadLyrics() {
|
|
||||||
SwitchPreference preference = findPreference("auto_download_lyrics");
|
|
||||||
if (preference == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
|
||||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void getScanStatus() {
|
|
||||||
settingViewModel.getScanStatus(new ScanCallback() {
|
|
||||||
@Override
|
|
||||||
public void onError(Exception exception) {
|
|
||||||
findPreference("scan_library").setSummary(exception.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSuccess(boolean isScanning, long count) {
|
|
||||||
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
|
||||||
if (isScanning) getScanStatus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionKeepScreenOn() {
|
|
||||||
findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> {
|
|
||||||
if (newValue instanceof Boolean) {
|
|
||||||
if ((Boolean) newValue) {
|
|
||||||
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
} else {
|
|
||||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
|
||||||
@Override
|
|
||||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
|
||||||
mediaServiceBinder = (MediaService.LocalBinder) service;
|
|
||||||
isServiceBound = true;
|
|
||||||
checkEqualizerBands();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onServiceDisconnected(ComponentName name) {
|
|
||||||
mediaServiceBinder = null;
|
|
||||||
isServiceBound = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private void bindMediaService() {
|
|
||||||
Intent intent = new Intent(requireActivity(), MediaService.class);
|
|
||||||
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
|
|
||||||
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
|
||||||
isServiceBound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkEqualizerBands() {
|
|
||||||
if (mediaServiceBinder != null) {
|
|
||||||
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
|
|
||||||
short numBands = eqManager.getNumberOfBands();
|
|
||||||
Preference appEqualizer = findPreference("app_equalizer");
|
|
||||||
if (appEqualizer != null) {
|
|
||||||
appEqualizer.setVisible(numBands > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void actionAppEqualizer() {
|
|
||||||
Preference appEqualizer = findPreference("app_equalizer");
|
|
||||||
if (appEqualizer != null) {
|
|
||||||
appEqualizer.setOnPreferenceClickListener(preference -> {
|
|
||||||
NavController navController = NavHostFragment.findNavController(this);
|
|
||||||
NavOptions navOptions = new NavOptions.Builder()
|
|
||||||
.setLaunchSingleTop(true)
|
|
||||||
.setPopUpTo(R.id.equalizerFragment, true)
|
|
||||||
.build();
|
|
||||||
activity.setBottomNavigationBarVisibility(true);
|
|
||||||
activity.setBottomSheetVisibility(true);
|
|
||||||
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
if (isServiceBound) {
|
|
||||||
requireActivity().unbindService(serviceConnection);
|
|
||||||
isServiceBound = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
|
songListPageViewModel.invalidateSongList();
|
||||||
|
|
||||||
if (requireArguments().getString(Constants.MEDIA_RECENTLY_PLAYED) != null) {
|
if (requireArguments().getString(Constants.MEDIA_RECENTLY_PLAYED) != null) {
|
||||||
songListPageViewModel.title = Constants.MEDIA_RECENTLY_PLAYED;
|
songListPageViewModel.title = Constants.MEDIA_RECENTLY_PLAYED;
|
||||||
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_recently_played);
|
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_recently_played);
|
||||||
@@ -146,6 +148,10 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
songListPageViewModel.title = Constants.MEDIA_STARRED;
|
songListPageViewModel.title = Constants.MEDIA_STARRED;
|
||||||
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_starred);
|
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_starred);
|
||||||
bind.pageTitleLabel.setText(R.string.song_list_page_starred);
|
bind.pageTitleLabel.setText(R.string.song_list_page_starred);
|
||||||
|
} else if (requireArguments().getString(Constants.MEDIA_ALL) != null) {
|
||||||
|
songListPageViewModel.title = Constants.MEDIA_ALL;
|
||||||
|
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_all);
|
||||||
|
bind.pageTitleLabel.setText(R.string.song_list_page_all);
|
||||||
} else if (requireArguments().getString(Constants.MEDIA_DOWNLOADED) != null) {
|
} else if (requireArguments().getString(Constants.MEDIA_DOWNLOADED) != null) {
|
||||||
songListPageViewModel.title = Constants.MEDIA_DOWNLOADED;
|
songListPageViewModel.title = Constants.MEDIA_DOWNLOADED;
|
||||||
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_downloaded);
|
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_downloaded);
|
||||||
@@ -302,6 +308,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
case Constants.MEDIA_BY_ARTIST:
|
case Constants.MEDIA_BY_ARTIST:
|
||||||
case Constants.MEDIA_BY_GENRES:
|
case Constants.MEDIA_BY_GENRES:
|
||||||
case Constants.MEDIA_STARRED:
|
case Constants.MEDIA_STARRED:
|
||||||
|
case Constants.MEDIA_ALL:
|
||||||
bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, children.size()));
|
bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, children.size()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -316,6 +323,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
case Constants.MEDIA_BY_ARTIST:
|
case Constants.MEDIA_BY_ARTIST:
|
||||||
case Constants.MEDIA_BY_GENRES:
|
case Constants.MEDIA_BY_GENRES:
|
||||||
case Constants.MEDIA_STARRED:
|
case Constants.MEDIA_STARRED:
|
||||||
|
case Constants.MEDIA_ALL:
|
||||||
bind.songListSortImageView.setVisibility(View.VISIBLE);
|
bind.songListSortImageView.setVisibility(View.VISIBLE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -367,4 +375,4 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||||||
private void setMediaBrowserListenableFuture() {
|
private void setMediaBrowserListenableFuture() {
|
||||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.navigation.fragment.NavHostFragment;
|
|||||||
|
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
import com.cappielloantonio.tempo.interfaces.MediaCallback;
|
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.service.MediaManager;
|
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.AlbumBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -61,7 +59,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private List<Child> currentAlbumTracks = Collections.emptyList();
|
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||||
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||||
|
|
||||||
|
private boolean isFirstBatch = true;
|
||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private static final String TAG = "AlbumBottomSheetDialog";
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
@@ -114,33 +115,41 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
|
|
||||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||||
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
|
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
|
||||||
favoriteToggle.setOnClickListener(v -> {
|
favoriteToggle.setOnClickListener(v -> albumBottomSheetViewModel.setFavorite(requireContext()));
|
||||||
albumBottomSheetViewModel.setFavorite(requireContext());
|
|
||||||
});
|
|
||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
playRadio.setOnClickListener(v -> {
|
playRadio.setOnClickListener(v -> {
|
||||||
AlbumRepository albumRepository = new AlbumRepository();
|
MainActivity activity = (MainActivity) getActivity();
|
||||||
albumRepository.getInstantMix(album, 20, new MediaCallback() {
|
if (activity == null) return;
|
||||||
@Override
|
|
||||||
public void onError(Exception exception) {
|
|
||||||
exception.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
ListenableFuture<MediaBrowser> activityBrowserFuture = activity.getMediaBrowserListenableFuture();
|
||||||
public void onLoadMedia(List<?> media) {
|
if (activityBrowserFuture == null) return;
|
||||||
MusicUtil.ratingFilter((ArrayList<Child>) media);
|
|
||||||
|
|
||||||
if (!media.isEmpty()) {
|
isFirstBatch = true;
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, (ArrayList<Child>) media, 0);
|
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
dismissBottomSheet();
|
MediaManager.enqueue(activityBrowserFuture, media, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
TextView playRandom = view.findViewById(R.id.play_random_text_view);
|
TextView playRandom = view.findViewById(R.id.play_random_text_view);
|
||||||
playRandom.setOnClickListener(v -> {
|
playRandom.setOnClickListener(v -> {
|
||||||
AlbumRepository albumRepository = new AlbumRepository();
|
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);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
Bundle bundle = new Bundle();
|
||||||
Bundle bundle = new Bundle();
|
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
|
||||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(songs));
|
|
||||||
|
|
||||||
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||||
dialog.setArguments(bundle);
|
dialog.setArguments(bundle);
|
||||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
@@ -291,4 +298,5 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private void refreshShares() {
|
private void refreshShares() {
|
||||||
homeViewModel.refreshShares(requireActivity());
|
homeViewModel.refreshShares(requireActivity());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import com.cappielloantonio.tempo.viewmodel.ArtistBottomSheetViewModel;
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||||
private static final String TAG = "AlbumBottomSheetDialog";
|
private static final String TAG = "AlbumBottomSheetDialog";
|
||||||
@@ -38,6 +39,8 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
|||||||
|
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
|
||||||
|
private boolean isFirstBatch = true;
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
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);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
playRadio.setOnClickListener(v -> {
|
playRadio.setOnClickListener(v -> {
|
||||||
ArtistRepository artistRepository = new ArtistRepository();
|
MainActivity activity = (MainActivity) getActivity();
|
||||||
|
if (activity == null) return;
|
||||||
|
|
||||||
artistRepository.getInstantMix(artist, 20).observe(getViewLifecycleOwner(), songs -> {
|
ListenableFuture<MediaBrowser> activityBrowserFuture = activity.getMediaBrowserListenableFuture();
|
||||||
// navidrome may return null for this
|
if (activityBrowserFuture == null) return;
|
||||||
if (songs == null)
|
|
||||||
return;
|
|
||||||
MusicUtil.ratingFilter(songs);
|
|
||||||
|
|
||||||
if (!songs.isEmpty()) {
|
isFirstBatch = true;
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
|
||||||
|
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 artistRepository = new ArtistRepository();
|
||||||
artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> {
|
artistRepository.getRandomSong(artist, 50).observe(getViewLifecycleOwner(), songs -> {
|
||||||
MusicUtil.ratingFilter(songs);
|
MusicUtil.ratingFilter(songs);
|
||||||
|
|
||||||
if (!songs.isEmpty()) {
|
if (!songs.isEmpty()) {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||||
|
|
||||||
dismissBottomSheet();
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), getString(R.string.artist_error_retrieving_tracks), Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -139,4 +147,5 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
|||||||
private void releaseMediaBrowser() {
|
private void releaseMediaBrowser() {
|
||||||
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog;
|
||||||
|
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import androidx.media3.session.MediaBrowser;
|
||||||
|
import androidx.media3.session.SessionToken;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaManager;
|
||||||
|
import com.cappielloantonio.tempo.service.MediaService;
|
||||||
|
import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs;
|
||||||
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
public class PlaylistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener {
|
||||||
|
private PlaylistWithSongs playlist;
|
||||||
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private static final String TAG = "PlaylistBottomSheetDialog";
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.bottom_sheet_playlist_dialog, container, false);
|
||||||
|
|
||||||
|
playlist = requireArguments().getParcelable(Constants.PLAYLIST_OBJECT);
|
||||||
|
|
||||||
|
init(view);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
|
||||||
|
initializeMediaBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
releaseMediaBrowser();
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(View view) {
|
||||||
|
ImageView coverPlaylist = view.findViewById(R.id.playlist_cover_image_view);
|
||||||
|
|
||||||
|
CustomGlideRequest.Builder
|
||||||
|
.from(view.getContext(), playlist.getCoverArtId(), CustomGlideRequest.ResourceType.Playlist)
|
||||||
|
.build()
|
||||||
|
.into(coverPlaylist);
|
||||||
|
|
||||||
|
TextView titlePlaylist = view.findViewById(R.id.playlist_title_text_view);
|
||||||
|
titlePlaylist.setText(playlist.getName());
|
||||||
|
|
||||||
|
titlePlaylist.setSelected(true);
|
||||||
|
|
||||||
|
TextView countPlaylist = view.findViewById(R.id.playlist_count_text_view);
|
||||||
|
countPlaylist.setText(view.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false)));
|
||||||
|
|
||||||
|
TextView playNext = view.findViewById(R.id.play_next_text_view);
|
||||||
|
playNext.setOnClickListener(v -> {
|
||||||
|
MediaManager.enqueue(mediaBrowserListenableFuture, playlist.getEntries(), true);
|
||||||
|
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||||
|
dismissBottomSheet();
|
||||||
|
});
|
||||||
|
|
||||||
|
TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view);
|
||||||
|
addToQueue.setOnClickListener(v -> {
|
||||||
|
MediaManager.enqueue(mediaBrowserListenableFuture, playlist.getEntries(), false);
|
||||||
|
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
||||||
|
dismissBottomSheet();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissBottomSheet() {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeMediaBrowser() {
|
||||||
|
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseMediaBrowser() {
|
||||||
|
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.content.ClipboardManager;
|
|||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.android.material.chip.ChipGroup;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import androidx.media3.common.MediaItem;
|
|
||||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -67,7 +66,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private AssetLinkUtil.AssetLink currentAlbumLink;
|
private AssetLinkUtil.AssetLink currentAlbumLink;
|
||||||
private AssetLinkUtil.AssetLink currentArtistLink;
|
private AssetLinkUtil.AssetLink currentArtistLink;
|
||||||
|
|
||||||
|
private boolean isFirstBatch = true;
|
||||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||||
|
private static final String TAG = "SongBottomSheetDialog";
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
@@ -143,20 +144,34 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
|
|
||||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||||
playRadio.setOnClickListener(v -> {
|
playRadio.setOnClickListener(v -> {
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, song);
|
MainActivity activity = (MainActivity) getActivity();
|
||||||
((MainActivity) requireActivity()).setBottomSheetInPeek(true);
|
if (activity == null) return;
|
||||||
|
|
||||||
songBottomSheetViewModel.getInstantMix(getViewLifecycleOwner(), song).observe(getViewLifecycleOwner(), songs -> {
|
ListenableFuture<MediaBrowser> activityBrowserFuture = activity.getMediaBrowserListenableFuture();
|
||||||
MusicUtil.ratingFilter(songs);
|
if (activityBrowserFuture == null) {
|
||||||
|
Log.e(TAG, "MediaBrowser Future is null in MainActivity");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (songs == null) {
|
isFirstBatch = true;
|
||||||
dismissBottomSheet();
|
Toast.makeText(requireContext(), R.string.bottom_sheet_generating_instant_mix, Toast.LENGTH_SHORT).show();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!songs.isEmpty()) {
|
songBottomSheetViewModel.getInstantMix(activity, song).observe(activity, media -> {
|
||||||
MediaManager.enqueue(mediaBrowserListenableFuture, songs, true);
|
|
||||||
dismissBottomSheet();
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -214,6 +229,34 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
});
|
});
|
||||||
|
|
||||||
updateDownloadButtons();
|
updateDownloadButtons();
|
||||||
|
|
||||||
|
String playlistId = requireArguments().getString(Constants.PLAYLIST_ID);
|
||||||
|
int itemPosition = requireArguments().getInt(Constants.ITEM_POSITION, -1);
|
||||||
|
|
||||||
|
TextView removeFromPlaylist = view.findViewById(R.id.remove_from_playlist_text_view);
|
||||||
|
if (playlistId != null && itemPosition != -1) {
|
||||||
|
removeFromPlaylist.setVisibility(View.VISIBLE);
|
||||||
|
removeFromPlaylist.setOnClickListener(v -> {
|
||||||
|
songBottomSheetViewModel.removeFromPlaylist(playlistId, itemPosition, new com.cappielloantonio.tempo.repository.PlaylistRepository.AddToPlaylistCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_success, Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure() {
|
||||||
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_failure, Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAllSkipped() {
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> {
|
||||||
@@ -327,16 +370,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
chip.setVisibility(View.VISIBLE);
|
chip.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
chip.setOnClickListener(v -> {
|
chip.setOnClickListener(v -> {
|
||||||
if (assetLink != null) {
|
((MainActivity) requireActivity()).openAssetLink(assetLink);
|
||||||
((MainActivity) requireActivity()).openAssetLink(assetLink);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
chip.setOnLongClickListener(v -> {
|
chip.setOnLongClickListener(v -> {
|
||||||
if (assetLink != null) {
|
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,4 +436,5 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||||||
private void refreshShares() {
|
private void refreshShares() {
|
||||||
homeViewModel.refreshShares(requireActivity());
|
homeViewModel.refreshShares(requireActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.cappielloantonio.tempo.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.security.KeyChain
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import okhttp3.internal.platform.Platform
|
||||||
|
import java.net.Socket
|
||||||
|
import java.security.KeyManagementException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.Principal
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.HttpsURLConnection
|
||||||
|
import javax.net.ssl.SSLContext
|
||||||
|
import javax.net.ssl.SSLSocketFactory
|
||||||
|
import javax.net.ssl.X509KeyManager
|
||||||
|
|
||||||
|
object ClientCertManager {
|
||||||
|
|
||||||
|
private const val TAG = "ClientCertManager"
|
||||||
|
|
||||||
|
val trustManager = Platform.get().platformTrustManager()
|
||||||
|
var sslSocketFactory: SSLSocketFactory? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun setupSslSocketFactory(context: Context) {
|
||||||
|
sslSocketFactory = createSslSocketFactory(context)
|
||||||
|
sslSocketFactory?.let {
|
||||||
|
// HttpsURLConnection is used both by:
|
||||||
|
// - Glide: in IPv6StringLoader
|
||||||
|
// - ExoPlayer: in DefaultHttpDataSource
|
||||||
|
HttpsURLConnection.setDefaultSSLSocketFactory(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSslSocketFactory(context: Context): SSLSocketFactory? {
|
||||||
|
return try {
|
||||||
|
val clientKeyManager = object : X509KeyManager {
|
||||||
|
override fun getClientAliases(keyType: String?, issuers: Array<Principal>?) = null
|
||||||
|
|
||||||
|
override fun chooseClientAlias(
|
||||||
|
keyType: Array<String>?,
|
||||||
|
issuers: Array<Principal>?,
|
||||||
|
socket: Socket?
|
||||||
|
): String? {
|
||||||
|
val clientCert = Preferences.getClientCert() ?: return null
|
||||||
|
val server = Preferences.getServer() ?: return null
|
||||||
|
return if (server.toUri().host == socket?.inetAddress?.hostName) {
|
||||||
|
clientCert
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getServerAliases(keyType: String?, issuers: Array<Principal>?) = null
|
||||||
|
|
||||||
|
override fun chooseServerAlias(
|
||||||
|
keyType: String?,
|
||||||
|
issuers: Array<Principal>?,
|
||||||
|
socket: Socket?
|
||||||
|
) = null
|
||||||
|
|
||||||
|
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
|
||||||
|
val clientCert = Preferences.getClientCert()
|
||||||
|
return if (alias == clientCert && clientCert != null) {
|
||||||
|
KeyChain.getCertificateChain(
|
||||||
|
context,
|
||||||
|
clientCert
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPrivateKey(alias: String?): PrivateKey? {
|
||||||
|
val clientCert = Preferences.getClientCert()
|
||||||
|
return if (alias == clientCert && clientCert != null) {
|
||||||
|
KeyChain.getPrivateKey(
|
||||||
|
context,
|
||||||
|
clientCert
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sslContext = SSLContext.getInstance("TLS")
|
||||||
|
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
|
||||||
|
sslContext.socketFactory
|
||||||
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
|
Log.e(TAG, "Failed setting mTLS", e)
|
||||||
|
null
|
||||||
|
} catch (e: KeyManagementException) {
|
||||||
|
Log.e(TAG, "Failed setting mTLS", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ object Constants {
|
|||||||
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
||||||
const val GENRE_OBJECT = "GENRE_OBJECT"
|
const val GENRE_OBJECT = "GENRE_OBJECT"
|
||||||
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
||||||
|
const val PLAYLIST_ID = "PLAYLIST_ID"
|
||||||
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
||||||
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
||||||
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
||||||
@@ -61,13 +62,6 @@ object Constants {
|
|||||||
const val MEDIA_TYPE_VIDEO = "video"
|
const val MEDIA_TYPE_VIDEO = "video"
|
||||||
const val MEDIA_TYPE_RADIO = "radio"
|
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_RECENTLY_PLAYED = "MEDIA_RECENTLY_PLAYED"
|
||||||
const val MEDIA_MOST_PLAYED = "MEDIA_MOST_PLAYED"
|
const val MEDIA_MOST_PLAYED = "MEDIA_MOST_PLAYED"
|
||||||
const val MEDIA_RECENTLY_ADDED = "MEDIA_RECENTLY_ADDED"
|
const val MEDIA_RECENTLY_ADDED = "MEDIA_RECENTLY_ADDED"
|
||||||
@@ -76,6 +70,7 @@ object Constants {
|
|||||||
const val MEDIA_BY_ARTIST = "MEDIA_BY_ARTIST"
|
const val MEDIA_BY_ARTIST = "MEDIA_BY_ARTIST"
|
||||||
const val MEDIA_BY_YEAR = "MEDIA_BY_YEAR"
|
const val MEDIA_BY_YEAR = "MEDIA_BY_YEAR"
|
||||||
const val MEDIA_STARRED = "MEDIA_STARRED"
|
const val MEDIA_STARRED = "MEDIA_STARRED"
|
||||||
|
const val MEDIA_ALL = "MEDIA_ALL"
|
||||||
const val MEDIA_DOWNLOADED = "MEDIA_DOWNLOADED"
|
const val MEDIA_DOWNLOADED = "MEDIA_DOWNLOADED"
|
||||||
const val MEDIA_FROM_ALBUM = "MEDIA_FROM_ALBUM"
|
const val MEDIA_FROM_ALBUM = "MEDIA_FROM_ALBUM"
|
||||||
const val MEDIA_MIX = "MEDIA_MIX"
|
const val MEDIA_MIX = "MEDIA_MIX"
|
||||||
@@ -133,4 +128,7 @@ object Constants {
|
|||||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF"
|
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_ONE = "android.media3.session.demo.REPEAT_ONE"
|
||||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
||||||
}
|
enum class SeedType {
|
||||||
|
ARTIST, ALBUM, TRACK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import java.net.CookieHandler;
|
|||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
import java.net.CookiePolicy;
|
import java.net.CookiePolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@@ -78,12 +80,33 @@ public final class DownloadUtil {
|
|||||||
return httpDataSourceFactory;
|
return httpDataSourceFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static synchronized DataSource.Factory getHttpDataSourceFactoryForRadio() {
|
||||||
|
CookieManager cookieManager = new CookieManager();
|
||||||
|
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
|
||||||
|
CookieHandler.setDefault(cookieManager);
|
||||||
|
|
||||||
|
// Create a factory with ICY metadata support for radio streams
|
||||||
|
Map<String, String> defaultRequestProperties = new HashMap<>();
|
||||||
|
defaultRequestProperties.put("Icy-MetaData", "1");
|
||||||
|
defaultRequestProperties.put("User-Agent", "Tempus/1.0");
|
||||||
|
|
||||||
|
return new DefaultHttpDataSource
|
||||||
|
.Factory()
|
||||||
|
.setAllowCrossProtocolRedirects(true)
|
||||||
|
.setDefaultRequestProperties(defaultRequestProperties);
|
||||||
|
}
|
||||||
|
|
||||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
||||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||||
return dataSourceFactory;
|
return dataSourceFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static synchronized DataSource.Factory getUpstreamDataSourceFactoryForRadio(Context context) {
|
||||||
|
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactoryForRadio());
|
||||||
|
return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||||
|
}
|
||||||
|
|
||||||
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
||||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||||
.setCache(getStreamingCache(context))
|
.setCache(getStreamingCache(context))
|
||||||
|
|||||||
@@ -20,10 +20,15 @@ class DynamicMediaSourceFactory(
|
|||||||
) : MediaSource.Factory {
|
) : MediaSource.Factory {
|
||||||
|
|
||||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||||
val mediaType: String? = mediaItem.mediaMetadata.extras?.getString("type", "")
|
// Detect radio streams in a backwards-compatible way.
|
||||||
|
// Older Tempus versions tagged radio items via MediaMetadata extras
|
||||||
|
// (`type == MEDIA_TYPE_RADIO`), while newer upstream changes use an
|
||||||
|
// "ir-" mediaId prefix. Support BOTH so radio works after rebases.
|
||||||
|
val mediaType = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||||
|
val isRadio = mediaType == Constants.MEDIA_TYPE_RADIO || mediaItem.mediaId.startsWith("ir-")
|
||||||
|
|
||||||
val streamingCacheSize = Preferences.getStreamingCacheSize()
|
val streamingCacheSize = Preferences.getStreamingCacheSize()
|
||||||
val bypassCache = mediaType == Constants.MEDIA_TYPE_RADIO
|
val bypassCache = isRadio
|
||||||
|
|
||||||
val useUpstream = when {
|
val useUpstream = when {
|
||||||
streamingCacheSize.toInt() == 0 -> true
|
streamingCacheSize.toInt() == 0 -> true
|
||||||
@@ -32,7 +37,10 @@ class DynamicMediaSourceFactory(
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
val dataSourceFactory: DataSource.Factory = if (useUpstream) {
|
val dataSourceFactory: DataSource.Factory = if (bypassCache) {
|
||||||
|
// For radio streams, use a DataSourceFactory with ICY metadata support
|
||||||
|
DownloadUtil.getUpstreamDataSourceFactoryForRadio(context)
|
||||||
|
} else if (useUpstream) {
|
||||||
DownloadUtil.getUpstreamDataSourceFactory(context)
|
DownloadUtil.getUpstreamDataSourceFactory(context)
|
||||||
} else {
|
} else {
|
||||||
DownloadUtil.getCacheDataSourceFactory(context)
|
DownloadUtil.getCacheDataSourceFactory(context)
|
||||||
@@ -46,8 +54,16 @@ class DynamicMediaSourceFactory(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
|
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
|
||||||
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
val progressiveFactory = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||||
.createMediaSource(mediaItem)
|
|
||||||
|
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,8 +1,10 @@
|
|||||||
package com.cappielloantonio.tempo.util;
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.OptIn;
|
import androidx.annotation.OptIn;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
@@ -15,6 +17,7 @@ import androidx.media3.common.HeartRating;
|
|||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||||
import com.cappielloantonio.tempo.model.Download;
|
import com.cappielloantonio.tempo.model.Download;
|
||||||
|
import com.cappielloantonio.tempo.provider.AlbumArtContentProvider;
|
||||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||||
@@ -23,6 +26,7 @@ import com.google.common.collect.ImmutableList;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@OptIn(markerClass = UnstableApi.class)
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
public class MappingUtil {
|
public class MappingUtil {
|
||||||
@@ -45,7 +49,7 @@ public class MappingUtil {
|
|||||||
Uri artworkUri = null;
|
Uri artworkUri = null;
|
||||||
|
|
||||||
if (coverArtId != null) {
|
if (coverArtId != null) {
|
||||||
artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, Preferences.getImageSize()));
|
artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
@@ -205,18 +209,34 @@ public class MappingUtil {
|
|||||||
|
|
||||||
public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) {
|
public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) {
|
||||||
Uri uri = Uri.parse(internetRadioStation.getStreamUrl());
|
Uri uri = Uri.parse(internetRadioStation.getStreamUrl());
|
||||||
|
Uri artworkUri = null;
|
||||||
|
String homePageUrl = internetRadioStation.getHomePageUrl();
|
||||||
|
String coverArtId = null;
|
||||||
|
|
||||||
|
if (homePageUrl != null && !homePageUrl.isEmpty() && MusicUtil.isImageUrl(homePageUrl)) {
|
||||||
|
String encodedUrl = Base64.encodeToString(homePageUrl.getBytes(StandardCharsets.UTF_8),
|
||||||
|
Base64.URL_SAFE | Base64.NO_WRAP);
|
||||||
|
coverArtId = "ir_" + encodedUrl;
|
||||||
|
artworkUri = AlbumArtContentProvider.contentUri(coverArtId);
|
||||||
|
}
|
||||||
|
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putString("id", internetRadioStation.getId());
|
bundle.putString("id", internetRadioStation.getId());
|
||||||
bundle.putString("title", internetRadioStation.getName());
|
bundle.putString("title", internetRadioStation.getName());
|
||||||
|
bundle.putString("stationName", internetRadioStation.getName());
|
||||||
bundle.putString("uri", uri.toString());
|
bundle.putString("uri", uri.toString());
|
||||||
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
||||||
|
bundle.putString("coverArtId", coverArtId);
|
||||||
|
if (homePageUrl != null) {
|
||||||
|
bundle.putString("homepageUrl", homePageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
return new MediaItem.Builder()
|
return new MediaItem.Builder()
|
||||||
.setMediaId(internetRadioStation.getId())
|
.setMediaId(internetRadioStation.getId())
|
||||||
.setMediaMetadata(
|
.setMediaMetadata(
|
||||||
new MediaMetadata.Builder()
|
new MediaMetadata.Builder()
|
||||||
.setTitle(internetRadioStation.getName())
|
.setTitle(internetRadioStation.getName())
|
||||||
|
.setArtworkUri(artworkUri)
|
||||||
.setExtras(bundle)
|
.setExtras(bundle)
|
||||||
.setIsBrowsable(false)
|
.setIsBrowsable(false)
|
||||||
.setIsPlayable(true)
|
.setIsPlayable(true)
|
||||||
@@ -235,7 +255,7 @@ public class MappingUtil {
|
|||||||
|
|
||||||
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
|
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
|
||||||
Uri uri = getUri(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 bundle = new Bundle();
|
||||||
bundle.putString("id", podcastEpisode.getId());
|
bundle.putString("id", podcastEpisode.getId());
|
||||||
@@ -286,13 +306,24 @@ public class MappingUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(Child media) {
|
private static Uri getUri(Child media) {
|
||||||
|
// Check if it's in our local SQL Database
|
||||||
|
DownloadRepository repo = new DownloadRepository();
|
||||||
|
Download localDownload = repo.getDownload(media.getId());
|
||||||
|
|
||||||
|
if (localDownload != null && localDownload.getDownloadUri() != null && !localDownload.getDownloadUri().isEmpty()) {
|
||||||
|
Log.d(TAG, "Playing local file for: " + media.getTitle());
|
||||||
|
return Uri.parse(localDownload.getDownloadUri());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy check for external directory, i think this was broken/buggy
|
||||||
if (Preferences.getDownloadDirectoryUri() != null) {
|
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||||
Uri local = ExternalAudioReader.getUri(media);
|
Uri local = ExternalAudioReader.getUri(media);
|
||||||
return local != null ? local : MusicUtil.getStreamUri(media.getId());
|
if (local != null) return local;
|
||||||
}
|
}
|
||||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
|
||||||
? getDownloadUri(media.getId())
|
// Fallback to streaming
|
||||||
: MusicUtil.getStreamUri(media.getId());
|
Log.d(TAG, "No local file found. Streaming: " + media.getTitle());
|
||||||
|
return MusicUtil.getStreamUri(media.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
||||||
@@ -316,4 +347,4 @@ public class MappingUtil {
|
|||||||
}
|
}
|
||||||
ExternalAudioReader.getRefreshEvents().observe(owner, event -> onRefresh.run());
|
ExternalAudioReader.getRefreshEvents().observe(owner, event -> onRefresh.run());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ public class MusicUtil {
|
|||||||
private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
|
private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
|
||||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+");
|
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();
|
Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
|
||||||
|
|
||||||
StringBuilder uri = new StringBuilder();
|
StringBuilder uri = new StringBuilder();
|
||||||
@@ -52,12 +52,18 @@ public class MusicUtil {
|
|||||||
if (params.containsKey("c") && params.get("c") != null)
|
if (params.containsKey("c") && params.get("c") != null)
|
||||||
uri.append("&c=").append(params.get("c"));
|
uri.append("&c=").append(params.get("c"));
|
||||||
|
|
||||||
|
String selectedBitrate = getBitratePreference();
|
||||||
|
String selectedFormat = getTranscodingFormatPreference();
|
||||||
|
Log.i(TAG, "DEBUG: Requesting Format: " + selectedFormat + " at Bitrate: " + selectedBitrate);
|
||||||
|
|
||||||
if (!Preferences.isServerPrioritized())
|
if (!Preferences.isServerPrioritized())
|
||||||
uri.append("&maxBitRate=").append(getBitratePreference());
|
uri.append("&maxBitRate=").append(getBitratePreference());
|
||||||
if (!Preferences.isServerPrioritized())
|
if (!Preferences.isServerPrioritized())
|
||||||
uri.append("&format=").append(getTranscodingFormatPreference());
|
uri.append("&format=").append(getTranscodingFormatPreference());
|
||||||
if (Preferences.askForEstimateContentLength())
|
if (Preferences.askForEstimateContentLength())
|
||||||
uri.append("&estimateContentLength=true");
|
uri.append("&estimateContentLength=true");
|
||||||
|
if (timeOffset > 0)
|
||||||
|
uri.append("&timeOffset=").append(timeOffset);
|
||||||
|
|
||||||
uri.append("&id=").append(id);
|
uri.append("&id=").append(id);
|
||||||
|
|
||||||
@@ -66,8 +72,22 @@ public class MusicUtil {
|
|||||||
return Uri.parse(uri.toString());
|
return Uri.parse(uri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Uri getStreamUri(String id) {
|
||||||
|
return getStreamUri(id, 0);
|
||||||
|
}
|
||||||
|
|
||||||
public static Uri updateStreamUri(Uri uri) {
|
public static Uri updateStreamUri(Uri uri) {
|
||||||
|
if (uri == null) return null;
|
||||||
|
|
||||||
|
String scheme = uri.getScheme();
|
||||||
|
// If it is local (content:// or file://), return it IMMEDIATELY.
|
||||||
|
// This prevents the code below from appending &maxBitRate to a local path.
|
||||||
|
if (scheme != null && (scheme.equals("content") || scheme.equals("file"))) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
String s = uri.toString();
|
String s = uri.toString();
|
||||||
|
|
||||||
Matcher m1 = BITRATE_PATTERN.matcher(s);
|
Matcher m1 = BITRATE_PATTERN.matcher(s);
|
||||||
s = m1.replaceAll("");
|
s = m1.replaceAll("");
|
||||||
Matcher m2 = FORMAT_PATTERN.matcher(s);
|
Matcher m2 = FORMAT_PATTERN.matcher(s);
|
||||||
@@ -151,7 +171,6 @@ public class MusicUtil {
|
|||||||
return Uri.parse(uri.toString());
|
return Uri.parse(uri.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static String getReadableDurationString(Long duration, boolean millis) {
|
public static String getReadableDurationString(Long duration, boolean millis) {
|
||||||
long lenght = duration != null ? duration : 0;
|
long lenght = duration != null ? duration : 0;
|
||||||
|
|
||||||
@@ -297,13 +316,17 @@ public class MusicUtil {
|
|||||||
|
|
||||||
if (network == null || networkCapabilities == null) return "raw";
|
if (network == null || networkCapabilities == null) return "raw";
|
||||||
|
|
||||||
|
String format;
|
||||||
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||||
return Preferences.getAudioTranscodeFormatWifi();
|
format = Preferences.getAudioTranscodeFormatWifi();
|
||||||
|
Log.d(TAG, "DEBUG: Using WIFI Format: " + format);
|
||||||
} else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
} else if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||||
return Preferences.getAudioTranscodeFormatMobile();
|
format = Preferences.getAudioTranscodeFormatMobile();
|
||||||
|
Log.d(TAG, "DEBUG: Using MOBILE Format: " + format);
|
||||||
} else {
|
} else {
|
||||||
return Preferences.getAudioTranscodeFormatWifi();
|
format = Preferences.getAudioTranscodeFormatWifi();
|
||||||
}
|
}
|
||||||
|
return format;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getBitratePreferenceForDownload() {
|
public static String getBitratePreferenceForDownload() {
|
||||||
@@ -354,4 +377,15 @@ public class MusicUtil {
|
|||||||
|
|
||||||
toFilter.addAll(filtered);
|
toFilter.addAll(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isImageUrl(String url) {
|
||||||
|
if (url == null || url.isEmpty())
|
||||||
|
return false;
|
||||||
|
String path = url.toLowerCase().trim().split("\\?")[0];
|
||||||
|
|
||||||
|
return path.endsWith(".jpg") || path.endsWith(".jpeg") ||
|
||||||
|
path.endsWith(".png") || path.endsWith(".webp") ||
|
||||||
|
path.endsWith(".gif") || path.endsWith(".bmp") ||
|
||||||
|
path.endsWith(".svg");
|
||||||
|
}
|
||||||
}
|
}
|
||||||