Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a06ab77b42 | ||
|
|
04a6176bfd | ||
|
|
1f4464e089 | ||
|
|
9d01d2057a | ||
|
|
ad440c490a | ||
|
|
acdcfff9ac | ||
|
|
8c7a25cbd0 | ||
|
|
bdca5e16ed | ||
|
|
f091b3d248 | ||
|
|
18cd84f820 | ||
|
|
281ebf8263 | ||
|
|
2854ac6354 | ||
|
|
16d25a1f1d | ||
|
|
5d3ca8acfa | ||
|
|
0689272046 | ||
|
|
17372fc4d0 | ||
|
|
44679855cd | ||
|
|
78e7032903 | ||
|
|
8d8087f2d6 | ||
|
|
5b6a4fab62 | ||
|
|
fdc41b299c | ||
|
|
82c22ed247 | ||
|
|
48ce3a2a4f | ||
|
|
b93acc6563 | ||
|
|
9c088a7e88 | ||
|
|
de2f1067a7 | ||
|
|
1c21546461 | ||
|
|
a4121e8d49 | ||
|
|
4cc4cc7363 | ||
|
|
c5ef274916 | ||
|
|
2c53f36a18 | ||
|
|
6c637dcbcb | ||
|
|
89fa38f5a0 | ||
|
|
87f8bdc618 | ||
|
|
903fde4bdc | ||
|
|
3824dd882c | ||
|
|
602bab6414 | ||
|
|
d891e429b6 | ||
|
|
ebefd77027 | ||
|
|
57f34affd9 | ||
|
|
50b5ab38bc | ||
|
|
9f61d70fca | ||
|
|
a97a2d5b50 | ||
|
|
8d73a2cd36 | ||
|
|
ca5a0698bb | ||
|
|
04f34e03d1 | ||
|
|
233bc9987e | ||
|
|
f0e418687e | ||
|
|
e87b658447 | ||
|
|
19c985c9e4 | ||
|
|
4ab122a9d7 | ||
|
|
ff0c42d14c | ||
|
|
d1e247f9e2 | ||
|
|
f1d19142fa | ||
|
|
45793c343a | ||
|
|
aa4249842d | ||
|
|
126663f1e5 | ||
|
|
ec19e8c401 | ||
|
|
717f95a04a | ||
|
|
ccce01a61b | ||
|
|
a7682d7656 | ||
|
|
84de93a4f1 | ||
|
|
78c4c89eca | ||
|
|
328beaff90 | ||
|
|
9d5d89d648 | ||
|
|
cd8b06f544 | ||
|
|
47a0def06c | ||
|
|
1c2f1aa061 | ||
|
|
30281e8f2d | ||
|
|
3c58e6fbb2 | ||
|
|
99a399b4d7 | ||
|
|
1da0a0b810 | ||
|
|
539920965e | ||
|
|
9a64eeabe6 | ||
|
|
791190f681 | ||
|
|
c03fca8039 | ||
|
|
620fba0a14 | ||
|
|
1357c5c062 | ||
|
|
682f63ef38 | ||
|
|
24864637f9 | ||
|
|
3ba19be4d9 | ||
|
|
cce6456951 | ||
|
|
fda586c4d8 | ||
|
|
57be72d5d4 | ||
|
|
c2354d4d42 | ||
|
|
a940af934c | ||
|
|
5891ec800c | ||
|
|
7259a82b67 | ||
|
|
c2b6d7eed5 | ||
|
|
2335bf2095 | ||
|
|
8bb6c02e46 | ||
|
|
47380a79a5 | ||
|
|
a187ba1e75 | ||
|
|
3eb9b2fb5c | ||
|
|
a547e19361 | ||
|
|
f4722fa0a8 | ||
|
|
ee738bc4c7 | ||
|
|
a22883fdde | ||
|
|
2acf11023a | ||
|
|
9736890e3c | ||
|
|
e790bf3eb6 | ||
|
|
e1d63a9eef | ||
|
|
134a1605ad | ||
|
|
1b45036963 | ||
|
|
0ba12c3d84 | ||
|
|
8cc3356b14 | ||
|
|
9d439b726b | ||
|
|
d4c0e30fd1 | ||
|
|
bb23d7e866 | ||
|
|
7321ef46f2 | ||
|
|
2fe2c2b28b | ||
|
|
a9318ec5d0 | ||
|
|
eb29dc2fb2 | ||
|
|
287e4a2b10 | ||
|
|
b7d56c2d70 | ||
|
|
5a6d101bdf | ||
|
|
969f0b5b21 | ||
|
|
14939d20fd | ||
|
|
35af1f9038 | ||
|
|
b79cfa4af0 | ||
|
|
e81e1a5356 | ||
|
|
cc0e264a17 | ||
|
|
a83495f353 | ||
|
|
be4346b3d1 | ||
|
|
2e29e9537a | ||
|
|
bc0adfe8e0 | ||
|
|
5261ca317b | ||
|
|
bf4ff3f1f9 | ||
|
|
cd195dbba0 | ||
|
|
7ec78991a5 | ||
|
|
e1c5a60805 | ||
|
|
f74813ef69 | ||
|
|
040558198e | ||
|
|
5ab68e4a98 | ||
|
|
aa8fac43a6 | ||
|
|
905bb3e3c5 | ||
|
|
d810010090 | ||
|
|
52ba783a90 | ||
|
|
d72855e160 | ||
|
|
82a9f00173 | ||
|
|
3f5749f7e1 | ||
|
|
cd2ab36351 | ||
|
|
a6688f897a | ||
|
|
64658dda1f | ||
|
|
d9f701d9d3 | ||
|
|
60fee3c77c | ||
|
|
b89086c5be | ||
|
|
d3dd236054 | ||
|
|
2e3330b63f | ||
|
|
e604c9ba86 | ||
|
|
2bf39d846e | ||
|
|
06066f1f66 | ||
|
|
7c0d44680f | ||
|
|
d4cb6c5c9a | ||
|
|
fab18c130e | ||
|
|
bd753f4489 | ||
|
|
e43a2b6fe5 | ||
|
|
c62d2ace4d | ||
|
|
6d403f808c | ||
|
|
1223062388 | ||
|
|
b0ddd5388b | ||
|
|
92f79a8e3d | ||
|
|
473d7e4e9c | ||
|
|
7ca0415274 | ||
|
|
cf7feacdc0 | ||
|
|
4740028a44 | ||
|
|
fe2c163aaa | ||
|
|
c6f08d9cec | ||
|
|
59b40df9ef | ||
|
|
81726baa08 | ||
|
|
af92a7b11a | ||
|
|
0578745bee | ||
|
|
e24063e460 | ||
|
|
4f1b1b603e | ||
|
|
7279c62944 | ||
|
|
a59d46f884 | ||
|
|
10285b308d | ||
|
|
8e2c5d1fee | ||
|
|
f59a360eb7 | ||
|
|
accf5fddc2 | ||
|
|
cc61d1cd48 | ||
|
|
6a16159cf0 | ||
|
|
eaf2710054 | ||
|
|
31d91f7215 | ||
|
|
f854f49686 | ||
|
|
9e8870a86a | ||
|
|
a0040c52a0 | ||
|
|
65f6347faf | ||
|
|
85fa2f768e | ||
|
|
cc5abd150a | ||
|
|
1ed6ac6cff | ||
|
|
cc6cb077b4 | ||
|
|
4be0acf76c | ||
|
|
7d843390db | ||
|
|
5c5316055c | ||
|
|
f1a179e7f8 | ||
|
|
0377c5e939 | ||
|
|
614ce8b466 | ||
|
|
9e87b53bc9 | ||
|
|
08023026b4 | ||
|
|
1bbcf6c790 | ||
|
|
698ca3b22b | ||
|
|
b2d875ac98 | ||
|
|
02eef97171 | ||
|
|
26a5fb029a | ||
|
|
c38c7c3deb | ||
|
|
8ed0a4642b | ||
|
|
6cfa04d368 | ||
|
|
af98f8d1a9 | ||
|
|
469204daac | ||
|
|
8943faf44c | ||
|
|
fea6366d84 | ||
|
|
92ac2e5684 | ||
|
|
06a52afa18 | ||
|
|
87f6db9e79 | ||
|
|
5fa46cc49b | ||
|
|
4da967910a | ||
|
|
3b18f39948 | ||
|
|
c9e0581815 | ||
|
|
b0fcc31f7b | ||
|
|
e98c9483c8 | ||
|
|
98a45b6059 | ||
|
|
6e070dfef0 | ||
|
|
910cce90f5 | ||
|
|
1a70ccd8f4 | ||
|
|
7830657fe1 | ||
|
|
0351ccfc95 | ||
|
|
10af6fb4ce | ||
|
|
61ec15e696 | ||
|
|
d21bd475a1 | ||
|
|
7e34f6ee64 | ||
|
|
7cfefe76cc | ||
|
|
4585533740 | ||
|
|
07c1760c39 | ||
|
|
24d4e67872 | ||
|
|
bec840620c | ||
|
|
2fa4ddf874 | ||
|
|
e16f88cb73 | ||
|
|
f79b05cb67 | ||
|
|
ed7c572578 | ||
|
|
e891214831 |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: eddyizm
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
64
.github/workflows/github_release.yml
vendored
64
.github/workflows/github_release.yml
vendored
@@ -3,7 +3,7 @@ name: Github Release Workflow
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -35,12 +35,18 @@ jobs:
|
||||
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||
|
||||
- name: Build APK
|
||||
- name: Build All APKs
|
||||
id: build
|
||||
run: bash ./gradlew assembleTempoRelease
|
||||
run: |
|
||||
# Build release variants
|
||||
bash ./gradlew assembleTempoRelease
|
||||
bash ./gradlew assembleNotquitemyRelease
|
||||
# Build debug variants
|
||||
bash ./gradlew assembleTempoDebug
|
||||
bash ./gradlew assembleNotquitemyDebug
|
||||
|
||||
- name: Sign APK
|
||||
id: sign_apk
|
||||
- name: Sign Tempo Release APKs
|
||||
id: sign_tempo_release
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/tempo/release
|
||||
@@ -51,11 +57,17 @@ jobs:
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||
|
||||
- name: Make artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Sign NotQuiteMy Release APKs
|
||||
id: sign_notquitemy_release
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
name: app-release-signed
|
||||
path: ${{steps.sign_apk.outputs.signedReleaseFile}}
|
||||
releaseDirectory: app/build/outputs/apk/notquitemy/release
|
||||
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
|
||||
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
@@ -67,12 +79,40 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Upload APK
|
||||
- name: Upload Release APKs
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
|
||||
asset_path: ${{steps.sign_tempo_release.outputs.signedReleaseFile}}
|
||||
asset_name: app-tempo-release.apk
|
||||
asset_content_type: application/zip
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
- name: Upload NotQuiteMy Release APK
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{steps.sign_notquitemy_release.outputs.signedReleaseFile}}
|
||||
asset_name: app-notquitemy-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
- name: Upload Debug APKs as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug-apks
|
||||
path: |
|
||||
app/build/outputs/apk/tempo/debug/
|
||||
app/build/outputs/apk/notquitemy/debug/
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload Release APKs as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apks
|
||||
path: |
|
||||
${{steps.sign_tempo_release.outputs.signedReleaseFile}}
|
||||
${{steps.sign_notquitemy_release.outputs.signedReleaseFile}}
|
||||
retention-days: 30
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -14,4 +14,8 @@
|
||||
.cxx
|
||||
/.idea/
|
||||
.env
|
||||
.vscode/settings.json
|
||||
.vscode/settings.json
|
||||
# release / debug files
|
||||
tempus-release-key.jks
|
||||
app/tempo/
|
||||
app/notquitemy/
|
||||
|
||||
121
CHANGELOG.md
121
CHANGELOG.md
@@ -2,6 +2,127 @@
|
||||
|
||||
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
|
||||
|
||||
## [3.17.0](https://github.com/eddyizm/tempo/releases/tag/v3.17.0) (2025-10-10)
|
||||
## What's Changed
|
||||
* chore: adding screenshot and docs for 4 icons/buttons in player control by @eddyizm in https://github.com/eddyizm/tempo/pull/162
|
||||
* Update Polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/160
|
||||
* feat: Make all objects in Tempo references for quick access by @le-firehawk in https://github.com/eddyizm/tempo/pull/158
|
||||
* fix: Glide module incorrectly encoding IPv6 addresses by @le-firehawk in https://github.com/eddyizm/tempo/pull/159
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.6...v3.17.0
|
||||
|
||||
## [3.16.6](https://github.com/eddyizm/tempo/releases/tag/v3.16.6) (2025-10-08)
|
||||
## What's Changed
|
||||
* chore(i18n): Update Spanish translation by @jaime-grj in https://github.com/eddyizm/tempo/pull/151
|
||||
* fix: Re-add new equalizer settings that got lost by @jaime-grj in https://github.com/eddyizm/tempo/pull/153
|
||||
* chore: removed play variant by @eddyizm in https://github.com/eddyizm/tempo/pull/155
|
||||
* fix: updating release workflow to account for the 32/64 bit builds an… by @eddyizm in https://github.com/eddyizm/tempo/pull/156
|
||||
* feat: Show sampling rate and bit depth in downloads by @jaime-grj in https://github.com/eddyizm/tempo/pull/154
|
||||
* fix: Replace hardcoded strings in SettingsFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/152
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.16.0...v3.16.6
|
||||
|
||||
## [3.16.0](https://github.com/eddyizm/tempo/releases/tag/v3.16.0) (2025-10-07)
|
||||
## What's Changed
|
||||
* chore: add sha256 fingerprint for validation by @eddyizm in https://github.com/eddyizm/tempo/commit/3c58e6fbb2157a804853259dfadbbffe3b6793b5
|
||||
* fix: Prevent crash when getting artist radio and song list is null by @jaime-grj in https://github.com/eddyizm/tempo/pull/117
|
||||
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/125
|
||||
* fix: Update search query validation to require at least 2 characters instead of 3 by @jaime-grj in https://github.com/eddyizm/tempo/pull/124
|
||||
* feat: download starred artists. by @eddyizm in https://github.com/eddyizm/tempo/pull/137
|
||||
* feat: Enable downloading of song lyrics for offline viewing by @le-firehawk in https://github.com/eddyizm/tempo/pull/99
|
||||
* fix: Lag during startup when local url is not available by @SinTan1729 in https://github.com/eddyizm/tempo/pull/110
|
||||
* chore: add link to discussion page in settings by @eddyizm in https://github.com/eddyizm/tempo/pull/143
|
||||
* feat: Notification heart rating by @eddyizm in https://github.com/eddyizm/tempo/pull/140
|
||||
* chore: Unify and update polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/146
|
||||
* chore: added sha256 signing key for verification by @eddyizm in https://github.com/eddyizm/tempo/pull/147
|
||||
* feat: Support user-defined download directory for media by @le-firehawk in https://github.com/eddyizm/tempo/pull/21
|
||||
* feat: Added support for skipping duplicates by @SinTan1729 in https://github.com/eddyizm/tempo/pull/135
|
||||
* feat: Add home screen music playback widget and some updates in Turkish localization by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/98
|
||||
|
||||
## New Contributors
|
||||
* @SinTan1729 made their first contribution in https://github.com/eddyizm/tempo/pull/110
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.15.0...v3.16.0
|
||||
|
||||
## [3.15.0](https://github.com/eddyizm/tempo/releases/tag/v3.15.0) (2025-09-23)
|
||||
## What's Changed
|
||||
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/84
|
||||
* chore: Update RU locale by @ArchiDevil in https://github.com/eddyizm/tempo/pull/87
|
||||
* chore: Update Korean translations by @kongwoojin in https://github.com/eddyizm/tempo/pull/97
|
||||
* fix: only plays the first song on an album by @eddyizm in https://github.com/eddyizm/tempo/pull/81
|
||||
* fix: handle null and not crash when disconnecting chromecast by @eddyizm in https://github.com/eddyizm/tempo/pull/81
|
||||
* feat: Built-in audio equalizer by @jaime-grj in https://github.com/eddyizm/tempo/pull/94
|
||||
* fix: Resolve playback issues with live radio MPEG & HLS streams by @jaime-grj in https://github.com/eddyizm/tempo/pull/89
|
||||
* chore: Updates to polish translation by @skajmer in https://github.com/eddyizm/tempo/pull/105
|
||||
* feat: added 32bit build and debug build for testing. Removed unused f… by @eddyizm in https://github.com/eddyizm/tempo/pull/108
|
||||
* feat: Mark currently playing song with play/pause button by @jaime-grj in https://github.com/eddyizm/tempo/pull/107
|
||||
* fix: add listener to track playlist click/change by @eddyizm in https://github.com/eddyizm/tempo/pull/113
|
||||
* feat: Tap anywhere on the song item to toggle playback by @jaime-grj in https://github.com/eddyizm/tempo/pull/112
|
||||
|
||||
## New Contributors
|
||||
* @ArchiDevil made their first contribution in https://github.com/eddyizm/tempo/pull/87
|
||||
* @kongwoojin made their first contribution in https://github.com/eddyizm/tempo/pull/97
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.14.8...v3.15.0
|
||||
|
||||
|
||||
## [3.14.8](https://github.com/eddyizm/tempo/releases/tag/v3.14.8) (2025-08-30)
|
||||
## What's Changed
|
||||
* fix: Use correct SearchView widget to avoid crash in AlbumListPageFragment by @jaime-grj in https://github.com/eddyizm/tempo/pull/76
|
||||
* chore(i18n): Update Spanish (es-ES) and English translations by @jaime-grj in https://github.com/eddyizm/tempo/pull/77
|
||||
* style: Center subtitle text in empty_download_layout in fragment_download.xml when there is more than one line by @jaime-grj in https://github.com/eddyizm/tempo/pull/78
|
||||
* fix: Disable "sync starred tracks/albums" switches when Cancel is clicked in warning dialog, use proper view for "Sync starred albums" dialog by @jaime-grj in https://github.com/eddyizm/tempo/pull/79
|
||||
* bug fixes, chores, docs v3.14.8 by @eddyizm in https://github.com/eddyizm/tempo/pull/80
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.14.1...v3.14.8
|
||||
|
||||
## [3.14.1](https://github.com/eddyizm/tempo/releases/tag/v3.14.1) (2025-08-30)
|
||||
## What's Changed
|
||||
* feat: rating dialog added to album page by @eddyizm in https://github.com/eddyizm/tempo/pull/52
|
||||
* style: Add song rating bar in landscape player controller layout by @jaime-grj in https://github.com/eddyizm/tempo/pull/57
|
||||
* feat: setting to show/hide 5 star rating on playerview by @eddyizm in https://github.com/eddyizm/tempo/pull/59
|
||||
* chore: setting-to-hide-song-rating by @eddyizm in https://github.com/eddyizm/tempo/pull/60
|
||||
* fix: catches null value and prepares bundle appropriately adding sing… by @eddyizm in https://github.com/eddyizm/tempo/pull/64
|
||||
* fix: artist filtering in library view browse artist resolves #45 by @eddyizm in https://github.com/eddyizm/tempo/pull/69
|
||||
* chore: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/70
|
||||
* feat: adds sync starred albums functionality #66 by @eddyizm in https://github.com/eddyizm/tempo/pull/73
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.13.0...v3.14.1
|
||||
|
||||
## [3.13.0](https://github.com/eddyizm/tempo/releases/tag/v3.13.0) (2025-08-23)
|
||||
## What's Changed
|
||||
* style: Change position and size of rating container by @jaime-grj in https://github.com/eddyizm/tempo/pull/44
|
||||
* feat: Add Turkish localization (values-tr) by @mucahit-kaya in https://github.com/eddyizm/tempo/pull/50
|
||||
* chore: adding a note/not fully baked label to the sync user play queue setting by @eddyizm in https://github.com/eddyizm/tempo/commit/8ed0a4642bd0cd637c65e3115142596331fa7ef7
|
||||
* fix: moved hardcoded italian save text to string template, updated with english and italian language xmls by @eddyizm in https://github.com/eddyizm/tempo/commit/26a5fb029a07752c9c0db0d08a89afd638772579
|
||||
|
||||
|
||||
## New Contributors
|
||||
* @mucahit-kaya made their first contribution in https://github.com/eddyizm/tempo/pull/50
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.12.0...v3.13.0
|
||||
|
||||
## [3.12.0](https://github.com/eddyizm/tempo/releases/tag/v3.12.0) (2025-08-15)
|
||||
### What's Changed
|
||||
* [chore]: add German translations for track info and home section strings (#29) by @BreadWare92 in https://github.com/eddyizm/tempo/pull/31
|
||||
* [chore]: increased "Offline mode" text size, changed its color in dark theme by @jaime-grj in https://github.com/eddyizm/tempo/pull/33
|
||||
* [chore]: Translations for sections by @skajmer in https://github.com/eddyizm/tempo/pull/30
|
||||
* [chore]: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/36
|
||||
* [fix]: Show placeholder string in TrackInfoDialog fields when there is no data by @jaime-grj in https://github.com/eddyizm/tempo/pull/37
|
||||
* [feat]: added transcoding codec and bitrate info to PlayerControllerFragment, replace hardcoded strings by @jaime-grj in https://github.com/eddyizm/tempo/pull/38
|
||||
* [chore]: Update French localization by @benoit-smith in https://github.com/eddyizm/tempo/pull/39
|
||||
* [feat]: show rating on song view by @eddyizm in https://github.com/eddyizm/tempo/pull/40
|
||||
|
||||
### New Contributors
|
||||
* @BreadWare92 made their first contribution in https://github.com/eddyizm/tempo/pull/31
|
||||
* @skajmer made their first contribution in https://github.com/eddyizm/tempo/pull/30
|
||||
* @benoit-smith made their first contribution in https://github.com/eddyizm/tempo/pull/36
|
||||
|
||||
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.11.2...v3.12.0
|
||||
|
||||
## [3.11.2](https://github.com/eddyizm/tempo/releases/tag/v3.11.2) (2025-08-09)
|
||||
|
||||
|
||||
|
||||
49
README.md
49
README.md
@@ -24,9 +24,28 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
|
||||
|
||||
## Fork
|
||||
|
||||
sha256 signing key fingerprint
|
||||
`B7:85:01:B9:34:D0:4E:0A:CA:8D:94:AF:D6:72:6A:4D:1D:CE:65:79:7F:1D:41:71:0F:64:3C:29:00:EB:1D:1D`
|
||||
|
||||
This fork is my attempt to keep development moving forward and merge in PR's that have been sitting for a while in the main repo. Thankful to @CappielloAntonio for the amazing app and hopefully we can continue to build on top of it. I will only be releasing on github and if I am not able to merge back to the main repo, I plan to rename the app to be able to publish it to fdroid and possibly google play? We will see.
|
||||
|
||||
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
|
||||
### Releases
|
||||
|
||||
Please note the two variants in the release assets include release/debug and 32/64 bit flavors.
|
||||
|
||||
`app-tempo` <- The github release with all the android auto/chromecast features
|
||||
|
||||
`app-notquitemy*` <- The f-droid release that goes without any of the google stuff. It was last released at 3.8.1 from the original repo. Since I don't have access to that original repo, I am releasing the apk's here on github.
|
||||
|
||||
As mentioned above, I am working towards a rebrand to get into app stores with a new name an icon.
|
||||
|
||||
Moved details to [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
Fork [**sponsorship here**](https://ko-fi.com/eddyizm).
|
||||
|
||||
## Usage
|
||||
|
||||
[Documentation](USAGE.md) (work in progress)
|
||||
|
||||
## Features
|
||||
- **Subsonic Integration**: Tempo seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
|
||||
@@ -40,22 +59,10 @@ Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANG
|
||||
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
|
||||
- **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.
|
||||
- **Multiple Libraries**: Tempo handles multi-library setups gracefully. They are displayed as Library folders.
|
||||
|
||||
<p align="center">
|
||||
<img src="mockup/feat/1_screenshot.png" width=200>
|
||||
<img src="mockup/feat/2_screenshot.png" width=200>
|
||||
<img src="mockup/feat/3_screenshot.png" width=200>
|
||||
<img src="mockup/feat/4_screenshot.png" width=200>
|
||||
<img src="mockup/feat/5_screenshot.png" width=200>
|
||||
<img src="mockup/feat/6_screenshot.png" width=200>
|
||||
<img src="mockup/feat/7_screenshot.png" width=200>
|
||||
<img src="mockup/feat/8_screenshot.png" width=200>
|
||||
</p>
|
||||
|
||||
## Sponsors
|
||||
Tempo is an open-source project developed and maintained solely by me. I would like to express my heartfelt thanks to all the users who have shown their love and support for Tempo. Your contributions and encouragement mean a lot to me, and they help drive the development and improvement of the app.
|
||||
|
||||
If you would like to sponsor the project and show your support, you can make a donation or contribution by visiting the [**sponsorship page**](https://www.buymeacoffee.com/a.cappiello). Your generosity will help cover the costs of development and further enhancements.
|
||||
## Credits
|
||||
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (3.9.0)
|
||||
|
||||
## Screenshot
|
||||
|
||||
@@ -91,6 +98,16 @@ If you would like to sponsor the project and show your support, you can make a d
|
||||
<img src="mockup/dark/8_screenshot.png" width=200>
|
||||
</p>
|
||||
|
||||
## Contributing
|
||||
|
||||
Please fork and open PR's against the development branch. Make sure your PR builds successfully.
|
||||
|
||||
If there is an UI change, please include a before/after screenshot and a short video/gif if that helps elaborating the fix/feature in the PR.
|
||||
|
||||
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.
|
||||
|
||||
## License
|
||||
|
||||
Tempo 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.
|
||||
|
||||
166
USAGE.md
Normal file
166
USAGE.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Tempo Usage Guide
|
||||
[<- back home](README.md)
|
||||
|
||||
## Table of Contents
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Server Configuration](#server-configuration)
|
||||
- [Main Features](#main-features)
|
||||
- [Navigation](#navigation)
|
||||
- [Playback Controls](#playback-controls)
|
||||
- [Favorites](#favorites)
|
||||
- [Playlist Management](#playlist-management)
|
||||
- [Android Auto](#android-auto)
|
||||
- [Settings](#settings)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
**Important Notice**: This app is a Subsonic-compatible client and does not provide any music content itself. To use this application, you must have:
|
||||
|
||||
- An active Subsonic API server (or compatible service) already set up
|
||||
- Valid login credentials for your Subsonic server
|
||||
- Music content uploaded and organized on your server
|
||||
|
||||
### Verified backends
|
||||
This app works with any service that implements the Subsonic API, including:
|
||||
- [LMS - Lightweight Music Server](https://github.com/epoupon/lms) - *personal fave and my backend*
|
||||
- [Navidrome](https://www.navidrome.org/)
|
||||
- [Gonic](https://github.com/sentriz/gonic)
|
||||
|
||||
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Installation
|
||||
1. Download the APK from the [Releases](https://github.com/eddyizm/tempo/releases) section
|
||||
2. Enable "Install from unknown sources" in your Android settings
|
||||
3. Install the application
|
||||
|
||||
### First Launch
|
||||
1. Open the application
|
||||
2. You will be prompted to configure your server connection
|
||||
3. Grant necessary permissions for media playback and background operation
|
||||
|
||||
## Server Configuration
|
||||
|
||||
### Initial Setup
|
||||
**IN PROGRESS**
|
||||
1. Enter your server URL (e.g., `https://your-subsonic-server.com`)
|
||||
2. Provide your username and password
|
||||
3. Test the connection to ensure proper configuration
|
||||
|
||||
### Advanced Settings
|
||||
**TODO**
|
||||
|
||||
## Main Features
|
||||
|
||||
### Library View
|
||||
|
||||
**Multi-library**
|
||||
|
||||
Tempo 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.
|
||||
|
||||
You can create multiple users , one for each library, and save each of them in Tempo app.
|
||||
|
||||
### Now Playing Screen
|
||||
|
||||
On the main player control screen, tapping on the artwork will reveal a small collection of 4 buttons/icons.
|
||||
<p align="left">
|
||||
<img src="mockup/usage/player_icons.png" width=159>
|
||||
</p>
|
||||
|
||||
*marked the icons with numbers for clarity*
|
||||
|
||||
1. Downloads the track (there is a notification if the android screen but not a pop toast currently )
|
||||
2. Adds track to playlist - pops up playlist dialog.
|
||||
3. Adds tracks to the queue via instant mix function
|
||||
4. Saves play queue (if the feature is enabled in the settings)
|
||||
* if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right)
|
||||
|
||||
|
||||
## Navigation
|
||||
|
||||
### Bottom Navigation Bar
|
||||
**IN PROGRESS**
|
||||
- **Home**: Recently played and server recommendations
|
||||
- **Library**: Your server's complete music collection
|
||||
- **Download**: Locally downloaded files from server
|
||||
|
||||
## Playback Controls
|
||||
|
||||
### Streaming Controls
|
||||
**TODO**
|
||||
|
||||
### Advanced Controls
|
||||
**TODO**
|
||||
|
||||
## Favorites
|
||||
|
||||
### Favorites (aka heart aka star) to albums and artists
|
||||
- Long pressing on an album gives you access to heart/unheart an album
|
||||
|
||||
<p align="center">
|
||||
<img src="mockup/usage/fave_album.png" width=376>
|
||||
</p>
|
||||
|
||||
- Long pressing on an artist cover gets you the same access to to heart/unheart an album
|
||||
|
||||
<p align="center">
|
||||
<img src="mockup/usage/fave_artist.png" width=376>
|
||||
</p>
|
||||
|
||||
|
||||
## Playlist Management
|
||||
|
||||
### Server Playlists
|
||||
**TODO**
|
||||
|
||||
### Creating Playlists
|
||||
**TODO**
|
||||
|
||||
## Settings
|
||||
|
||||
|
||||
## Android Auto
|
||||
|
||||
### Enabling on your head unit
|
||||
- You have to enable Android Auto developer options, which are different from actual Android dev options. Then you have to enable "Unknown sources" in Android Auto, otherwise the app won't appear as it isn't downloaded from Play Store. (screenshots needed)
|
||||
|
||||
|
||||
### Server Settings
|
||||
**IN PROGRESS**
|
||||
- Manage multiple server connections
|
||||
- Configure sync intervals
|
||||
- Set data usage limits for streaming
|
||||
|
||||
### Audio Settings
|
||||
**IN PROGRESS**
|
||||
- Streaming quality settings
|
||||
- Offline caching preferences
|
||||
|
||||
### Appearance
|
||||
**TODO**
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Connection Issues
|
||||
|
||||
**TODO**
|
||||
|
||||
### Common Issues
|
||||
|
||||
**TODO**
|
||||
|
||||
### Support
|
||||
For additional help:
|
||||
- Question? Start a [Discussion](https://github.com/eddyizm/tempo/discussions)
|
||||
- Open an [issue](https://github.com/eddyizm/tempo/issues) if you don't find a discussion solving your issue.
|
||||
- Consult your Subsonic server's documentation
|
||||
|
||||
---
|
||||
|
||||
*Note: This app requires a pre-existing Subsonic-compatible server with music content.*
|
||||
@@ -10,9 +10,8 @@ android {
|
||||
minSdkVersion 24
|
||||
targetSdk 35
|
||||
|
||||
versionCode 27
|
||||
versionName '3.11.2'
|
||||
|
||||
versionCode 36
|
||||
versionName '3.17.14'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
|
||||
javaCompileOptions {
|
||||
@@ -23,8 +22,21 @@ android {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
//noinspection ChromeOsAbiSupport
|
||||
include 'armeabi-v7a', 'arm64-v8a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
flavorDimensions += "default"
|
||||
|
||||
productFlavors {
|
||||
@@ -38,10 +50,6 @@ android {
|
||||
applicationId "com.cappielloantonio.notquitemy.tempo"
|
||||
}
|
||||
|
||||
play {
|
||||
dimension = "default"
|
||||
applicationId "com.cappielloantonio.play.tempo"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -51,6 +59,11 @@ android {
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -98,7 +111,7 @@ dependencies {
|
||||
implementation 'androidx.media3:media3-ui:1.5.1'
|
||||
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
|
||||
tempoImplementation 'androidx.media3:media3-cast:1.5.1'
|
||||
playImplementation 'androidx.media3:media3-cast:1.5.1'
|
||||
|
||||
|
||||
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
|
||||
annotationProcessor 'androidx.room:room-compiler:2.6.1'
|
||||
@@ -112,4 +125,4 @@ java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
1151
app/schemas/com.cappielloantonio.tempo.database.AppDatabase/12.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,16 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="asset"
|
||||
android:scheme="tempo" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
@@ -73,5 +83,20 @@
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".widget.WidgetProvider4x1"
|
||||
android:exported="false"
|
||||
android:label="@string/widget_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_info"/>
|
||||
</receiver>
|
||||
|
||||
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
|
||||
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
|
||||
import com.cappielloantonio.tempo.database.dao.DownloadDao;
|
||||
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
|
||||
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
|
||||
import com.cappielloantonio.tempo.database.dao.QueueDao;
|
||||
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
|
||||
@@ -20,6 +21,7 @@ import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
|
||||
import com.cappielloantonio.tempo.model.Chronology;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.model.Favorite;
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
import com.cappielloantonio.tempo.model.Queue;
|
||||
import com.cappielloantonio.tempo.model.RecentSearch;
|
||||
import com.cappielloantonio.tempo.model.Server;
|
||||
@@ -28,9 +30,9 @@ import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||
|
||||
@UnstableApi
|
||||
@Database(
|
||||
version = 11,
|
||||
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
|
||||
autoMigrations = {@AutoMigration(from = 10, to = 11)}
|
||||
version = 12,
|
||||
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)}
|
||||
)
|
||||
@TypeConverters({DateConverters.class})
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
@@ -62,4 +64,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||
public abstract SessionMediaItemDao sessionMediaItemDao();
|
||||
|
||||
public abstract PlaylistDao playlistDao();
|
||||
|
||||
public abstract LyricsDao lyricsDao();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ public interface DownloadDao {
|
||||
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||
LiveData<List<Download>> getAll();
|
||||
|
||||
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
|
||||
List<Download> getAllSync();
|
||||
|
||||
@Query("SELECT * FROM download WHERE id = :id")
|
||||
Download getOne(String id);
|
||||
|
||||
@@ -30,6 +33,9 @@ public interface DownloadDao {
|
||||
@Query("DELETE FROM download WHERE id = :id")
|
||||
void delete(String id);
|
||||
|
||||
@Query("DELETE FROM download WHERE id IN (:ids)")
|
||||
void deleteByIds(List<String> ids);
|
||||
|
||||
@Query("DELETE FROM download")
|
||||
void deleteAll();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.cappielloantonio.tempo.database.dao;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
|
||||
@Dao
|
||||
public interface LyricsDao {
|
||||
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||
LyricsCache getOne(String songId);
|
||||
|
||||
@Query("SELECT * FROM lyrics_cache WHERE song_id = :songId")
|
||||
LiveData<LyricsCache> observeOne(String songId);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
void insert(LyricsCache lyricsCache);
|
||||
|
||||
@Query("DELETE FROM lyrics_cache WHERE song_id = :songId")
|
||||
void delete(String songId);
|
||||
}
|
||||
@@ -4,14 +4,18 @@ import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.GlideBuilder;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.load.DecodeFormat;
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
||||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
@GlideModule
|
||||
public class CustomGlideModule extends AppGlideModule {
|
||||
@Override
|
||||
@@ -20,4 +24,9 @@ public class CustomGlideModule extends AppGlideModule {
|
||||
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cache", diskCacheSize));
|
||||
builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
registry.replace(String.class, InputStream.class, new IPv6StringLoader.Factory());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cappielloantonio.tempo.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.Log;
|
||||
@@ -16,6 +17,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
@@ -109,9 +111,21 @@ public class CustomGlideRequest {
|
||||
return uri.toString();
|
||||
}
|
||||
|
||||
public static void loadAlbumArtBitmap(Context context,
|
||||
String coverId,
|
||||
int size,
|
||||
CustomTarget<Bitmap> target) {
|
||||
String url = createUrl(coverId, size);
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(url)
|
||||
.apply(createRequestOptions(context, coverId, ResourceType.Album))
|
||||
.into(target);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final RequestManager requestManager;
|
||||
private Object item;
|
||||
private String item;
|
||||
|
||||
private Builder(Context context, String item, ResourceType type) {
|
||||
this.requestManager = Glide.with(context);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.cappielloantonio.tempo.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
public class IPv6StringLoader implements ModelLoader<String, InputStream> {
|
||||
private static final int DEFAULT_TIMEOUT_MS = 2500;
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull String model) {
|
||||
return model.startsWith("http://") || model.startsWith("https://");
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) {
|
||||
if (!handles(model)) {
|
||||
return null;
|
||||
}
|
||||
return new LoadData<>(new ObjectKey(model), new IPv6StreamFetcher(model));
|
||||
}
|
||||
|
||||
private static class IPv6StreamFetcher implements DataFetcher<InputStream> {
|
||||
private final String model;
|
||||
private InputStream stream;
|
||||
private HttpURLConnection connection;
|
||||
|
||||
IPv6StreamFetcher(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
|
||||
try {
|
||||
URL url = new URL(model);
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(DEFAULT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(DEFAULT_TIMEOUT_MS);
|
||||
connection.setUseCaches(true);
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
|
||||
if (connection.getResponseCode() / 100 != 2) {
|
||||
callback.onLoadFailed(new IOException("Request failed with status code: " + connection.getResponseCode()));
|
||||
return;
|
||||
}
|
||||
|
||||
stream = connection.getInputStream();
|
||||
callback.onDataReady(stream);
|
||||
} catch (IOException e) {
|
||||
callback.onLoadFailed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
// HttpURLConnection does not provide a direct cancel mechanism.
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<InputStream> getDataClass() {
|
||||
return InputStream.class;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return DataSource.REMOTE;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<String, InputStream> {
|
||||
@NonNull
|
||||
@Override
|
||||
public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
|
||||
return new IPv6StringLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,18 +8,18 @@ import androidx.room.PrimaryKey
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
@Entity(tableName = "chronology")
|
||||
class Chronology(@PrimaryKey override val id: String) : Child(id) {
|
||||
class Chronology(
|
||||
@PrimaryKey override val id: String,
|
||||
@ColumnInfo(name = "timestamp")
|
||||
var timestamp: Long = System.currentTimeMillis()
|
||||
|
||||
var timestamp: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "server")
|
||||
var server: String? = null
|
||||
|
||||
var server: String? = null,
|
||||
) : Child(id) {
|
||||
constructor(mediaItem: MediaItem) : this(mediaItem.mediaMetadata.extras!!.getString("id")!!) {
|
||||
parentId = mediaItem.mediaMetadata.extras!!.getString("parentId")
|
||||
isDir = mediaItem.mediaMetadata.extras!!.getBoolean("isDir")
|
||||
|
||||
@@ -10,19 +10,17 @@ import kotlinx.parcelize.Parcelize
|
||||
@Keep
|
||||
@Parcelize
|
||||
@Entity(tableName = "download")
|
||||
class Download(@PrimaryKey override val id: String) : Child(id) {
|
||||
class Download(
|
||||
@PrimaryKey override val id: String,
|
||||
@ColumnInfo(name = "playlist_id")
|
||||
var playlistId: String? = null
|
||||
|
||||
var playlistId: String? = null,
|
||||
@ColumnInfo(name = "playlist_name")
|
||||
var playlistName: String? = null
|
||||
|
||||
var playlistName: String? = null,
|
||||
@ColumnInfo(name = "download_state", defaultValue = "1")
|
||||
var downloadState: Int = 0
|
||||
|
||||
var downloadState: Int = 0,
|
||||
@ColumnInfo(name = "download_uri", defaultValue = "")
|
||||
var downloadUri: String? = null
|
||||
|
||||
var downloadUri: String? = null,
|
||||
) : Child(id) {
|
||||
constructor(child: Child) : this(child.id) {
|
||||
parentId = child.parentId
|
||||
isDir = child.isDir
|
||||
@@ -40,6 +38,8 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
|
||||
transcodedSuffix = child.transcodedSuffix
|
||||
duration = child.duration
|
||||
bitrate = child.bitrate
|
||||
samplingRate = child.samplingRate
|
||||
bitDepth = child.bitDepth
|
||||
path = child.path
|
||||
isVideo = child.isVideo
|
||||
userRating = child.userRating
|
||||
@@ -60,5 +60,5 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
|
||||
@Keep
|
||||
data class DownloadStack(
|
||||
var id: String,
|
||||
var view: String?
|
||||
var view: String?,
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.cappielloantonio.tempo.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlin.jvm.JvmOverloads
|
||||
|
||||
@Keep
|
||||
@Entity(tableName = "lyrics_cache")
|
||||
data class LyricsCache @JvmOverloads constructor(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "song_id")
|
||||
var songId: String,
|
||||
@ColumnInfo(name = "artist")
|
||||
var artist: String? = null,
|
||||
@ColumnInfo(name = "title")
|
||||
var title: String? = null,
|
||||
@ColumnInfo(name = "lyrics")
|
||||
var lyrics: String? = null,
|
||||
@ColumnInfo(name = "structured_lyrics")
|
||||
var structuredLyrics: String? = null,
|
||||
@ColumnInfo(name = "updated_at")
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -10,20 +10,18 @@ import kotlinx.parcelize.Parcelize
|
||||
@Keep
|
||||
@Parcelize
|
||||
@Entity(tableName = "queue")
|
||||
class Queue(override val id: String) : Child(id) {
|
||||
class Queue(
|
||||
override val id: String,
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "track_order")
|
||||
var trackOrder: Int = 0
|
||||
|
||||
var trackOrder: Int = 0,
|
||||
@ColumnInfo(name = "last_play")
|
||||
var lastPlay: Long = 0
|
||||
|
||||
var lastPlay: Long = 0,
|
||||
@ColumnInfo(name = "playing_changed")
|
||||
var playingChanged: Long = 0
|
||||
|
||||
var playingChanged: Long = 0,
|
||||
@ColumnInfo(name = "stream_id")
|
||||
var streamId: String? = null
|
||||
|
||||
var streamId: String? = null,
|
||||
) : Child(id) {
|
||||
constructor(child: Child) : this(child.id) {
|
||||
parentId = child.parentId
|
||||
isDir = child.isDir
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.model
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.Keep
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaItem.RequestMetadata
|
||||
import androidx.media3.common.MediaMetadata
|
||||
@@ -243,6 +244,13 @@ class SessionMediaItem() {
|
||||
.setAlbumTitle(album)
|
||||
.setArtist(artist)
|
||||
.setArtworkUri(artworkUri)
|
||||
.setUserRating(HeartRating(starred != null))
|
||||
.setSupportedCommands(
|
||||
listOf(
|
||||
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||
)
|
||||
)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import android.util.Log;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.interfaces.DecadesCallback;
|
||||
@@ -31,14 +32,22 @@ public class AlbumRepository {
|
||||
.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().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) {
|
||||
|
||||
listLiveAlbums.setValue(response.body().getSubsonicResponse().getAlbumList2().getAlbums());
|
||||
} else {
|
||||
Log.e("AlbumRepository", "API Error on getAlbums. HTTP Code: " + response.code());
|
||||
listLiveAlbums.setValue(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
|
||||
Log.e("AlbumRepository", "Network Failure on getAlbums: " + t.getMessage());
|
||||
listLiveAlbums.setValue(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import android.util.Log;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistInfo2;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||
@@ -13,12 +15,92 @@ import com.cappielloantonio.tempo.subsonic.models.IndexID3;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class ArtistRepository {
|
||||
private final AlbumRepository albumRepository;
|
||||
|
||||
public ArtistRepository() {
|
||||
this.albumRepository = new AlbumRepository();
|
||||
}
|
||||
|
||||
public void getArtistAllSongs(String artistId, ArtistSongsCallback callback) {
|
||||
Log.d("ArtistSync", "Getting albums for artist: " + artistId);
|
||||
|
||||
// Get the artist info first, which contains the albums
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getBrowsingClient()
|
||||
.getArtist(artistId)
|
||||
.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().getArtist() != null &&
|
||||
response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
|
||||
|
||||
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
|
||||
Log.d("ArtistSync", "Got albums directly: " + albums.size());
|
||||
|
||||
if (!albums.isEmpty()) {
|
||||
fetchAllAlbumSongsWithCallback(albums, callback);
|
||||
} else {
|
||||
Log.d("ArtistSync", "No albums found in artist response");
|
||||
callback.onSongsCollected(new ArrayList<>());
|
||||
}
|
||||
} else {
|
||||
Log.d("ArtistSync", "Failed to get artist info");
|
||||
callback.onSongsCollected(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||
Log.d("ArtistSync", "Error getting artist info: " + t.getMessage());
|
||||
callback.onSongsCollected(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchAllAlbumSongsWithCallback(List<AlbumID3> albums, ArtistSongsCallback callback) {
|
||||
if (albums == null || albums.isEmpty()) {
|
||||
Log.d("ArtistSync", "No albums to process");
|
||||
callback.onSongsCollected(new ArrayList<>());
|
||||
return;
|
||||
}
|
||||
|
||||
List<Child> allSongs = new ArrayList<>();
|
||||
AtomicInteger remainingAlbums = new AtomicInteger(albums.size());
|
||||
Log.d("ArtistSync", "Processing " + albums.size() + " albums");
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
Log.d("ArtistSync", "Getting tracks for album: " + album.getName());
|
||||
MutableLiveData<List<Child>> albumTracks = albumRepository.getAlbumTracks(album.getId());
|
||||
albumTracks.observeForever(songs -> {
|
||||
Log.d("ArtistSync", "Got " + (songs != null ? songs.size() : 0) + " songs from album");
|
||||
if (songs != null) {
|
||||
allSongs.addAll(songs);
|
||||
}
|
||||
albumTracks.removeObservers(null);
|
||||
|
||||
int remaining = remainingAlbums.decrementAndGet();
|
||||
Log.d("ArtistSync", "Remaining albums: " + remaining);
|
||||
|
||||
if (remaining == 0) {
|
||||
Log.d("ArtistSync", "All albums processed. Total songs: " + allSongs.size());
|
||||
callback.onSongsCollected(allSongs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public interface ArtistSongsCallback {
|
||||
void onSongsCollected(List<Child> songs);
|
||||
}
|
||||
|
||||
public MutableLiveData<List<ArtistID3>> getStarredArtists(boolean random, int size) {
|
||||
MutableLiveData<List<ArtistID3>> starredArtists = new MutableLiveData<>(new ArrayList<>());
|
||||
|
||||
@@ -89,7 +171,7 @@ public class ArtistRepository {
|
||||
}
|
||||
|
||||
/*
|
||||
* Metodo che mi restituisce le informazioni essenzionali dell'artista (cover, numero di album...)
|
||||
* Method that returns essential artist information (cover, album number, etc.)
|
||||
*/
|
||||
public void getArtistInfo(List<ArtistID3> artists, MutableLiveData<List<ArtistID3>> list) {
|
||||
List<ArtistID3> liveArtists = list.getValue();
|
||||
|
||||
@@ -18,6 +18,20 @@ public class DownloadRepository {
|
||||
return downloadDao.getAll();
|
||||
}
|
||||
|
||||
public List<Download> getAllDownloads() {
|
||||
GetAllDownloadsThreadSafe getDownloads = new GetAllDownloadsThreadSafe(downloadDao);
|
||||
Thread thread = new Thread(getDownloads);
|
||||
thread.start();
|
||||
|
||||
try {
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return getDownloads.getDownloads();
|
||||
}
|
||||
|
||||
public Download getDownload(String id) {
|
||||
Download download = null;
|
||||
|
||||
@@ -35,6 +49,24 @@ public class DownloadRepository {
|
||||
return download;
|
||||
}
|
||||
|
||||
private static class GetAllDownloadsThreadSafe implements Runnable {
|
||||
private final DownloadDao downloadDao;
|
||||
private List<Download> downloads;
|
||||
|
||||
public GetAllDownloadsThreadSafe(DownloadDao downloadDao) {
|
||||
this.downloadDao = downloadDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
downloads = downloadDao.getAllSync();
|
||||
}
|
||||
|
||||
public List<Download> getDownloads() {
|
||||
return downloads;
|
||||
}
|
||||
}
|
||||
|
||||
private static class GetDownloadThreadSafe implements Runnable {
|
||||
private final DownloadDao downloadDao;
|
||||
private final String id;
|
||||
@@ -143,6 +175,12 @@ public class DownloadRepository {
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void delete(List<String> ids) {
|
||||
DeleteMultipleThreadSafe delete = new DeleteMultipleThreadSafe(downloadDao, ids);
|
||||
Thread thread = new Thread(delete);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static class DeleteThreadSafe implements Runnable {
|
||||
private final DownloadDao downloadDao;
|
||||
private final String id;
|
||||
@@ -157,4 +195,19 @@ public class DownloadRepository {
|
||||
downloadDao.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeleteMultipleThreadSafe implements Runnable {
|
||||
private final DownloadDao downloadDao;
|
||||
private final List<String> ids;
|
||||
|
||||
public DeleteMultipleThreadSafe(DownloadDao downloadDao, List<String> ids) {
|
||||
this.downloadDao = downloadDao;
|
||||
this.ids = ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
downloadDao.deleteByIds(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.cappielloantonio.tempo.repository;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.database.AppDatabase;
|
||||
import com.cappielloantonio.tempo.database.dao.LyricsDao;
|
||||
import com.cappielloantonio.tempo.model.LyricsCache;
|
||||
|
||||
public class LyricsRepository {
|
||||
private final LyricsDao lyricsDao = AppDatabase.getInstance().lyricsDao();
|
||||
|
||||
public LyricsCache getLyrics(String songId) {
|
||||
GetLyricsThreadSafe getLyricsThreadSafe = new GetLyricsThreadSafe(lyricsDao, songId);
|
||||
Thread thread = new Thread(getLyricsThreadSafe);
|
||||
thread.start();
|
||||
|
||||
try {
|
||||
thread.join();
|
||||
return getLyricsThreadSafe.getLyrics();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public LiveData<LyricsCache> observeLyrics(String songId) {
|
||||
return lyricsDao.observeOne(songId);
|
||||
}
|
||||
|
||||
public void insert(LyricsCache lyricsCache) {
|
||||
InsertThreadSafe insert = new InsertThreadSafe(lyricsDao, lyricsCache);
|
||||
Thread thread = new Thread(insert);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void delete(String songId) {
|
||||
DeleteThreadSafe delete = new DeleteThreadSafe(lyricsDao, songId);
|
||||
Thread thread = new Thread(delete);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private static class GetLyricsThreadSafe implements Runnable {
|
||||
private final LyricsDao lyricsDao;
|
||||
private final String songId;
|
||||
private LyricsCache lyricsCache;
|
||||
|
||||
public GetLyricsThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||
this.lyricsDao = lyricsDao;
|
||||
this.songId = songId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lyricsCache = lyricsDao.getOne(songId);
|
||||
}
|
||||
|
||||
public LyricsCache getLyrics() {
|
||||
return lyricsCache;
|
||||
}
|
||||
}
|
||||
|
||||
private static class InsertThreadSafe implements Runnable {
|
||||
private final LyricsDao lyricsDao;
|
||||
private final LyricsCache lyricsCache;
|
||||
|
||||
public InsertThreadSafe(LyricsDao lyricsDao, LyricsCache lyricsCache) {
|
||||
this.lyricsDao = lyricsDao;
|
||||
this.lyricsCache = lyricsCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lyricsDao.insert(lyricsCache);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeleteThreadSafe implements Runnable {
|
||||
private final LyricsDao lyricsDao;
|
||||
private final String songId;
|
||||
|
||||
public DeleteThreadSafe(LyricsDao lyricsDao, String songId) {
|
||||
this.lyricsDao = lyricsDao;
|
||||
this.songId = songId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
lyricsDao.delete(songId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,21 +80,52 @@ public class PlaylistRepository {
|
||||
return listLivePlaylistSongs;
|
||||
}
|
||||
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
||||
public MutableLiveData<Playlist> getPlaylist(String id) {
|
||||
MutableLiveData<Playlist> playlistLiveData = new MutableLiveData<>();
|
||||
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
||||
.getPlaylist(id)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
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()
|
||||
&& response.body() != null
|
||||
&& response.body().getSubsonicResponse().getPlaylist() != null) {
|
||||
playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
|
||||
} else {
|
||||
playlistLiveData.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
playlistLiveData.setValue(null);
|
||||
}
|
||||
});
|
||||
|
||||
return playlistLiveData;
|
||||
}
|
||||
|
||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
||||
if (songsId.isEmpty()) {
|
||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
||||
} else{
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.updatePlaylist(playlistId, null, true, songsId, null)
|
||||
.enqueue(new Callback<ApiResponse>() {
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||
@@ -131,23 +162,6 @@ public class PlaylistRepository {
|
||||
});
|
||||
}
|
||||
|
||||
public void updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList<String> songIdToAdd, ArrayList<Integer> songIndexToRemove) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
.updatePlaylist(playlistId, name, isPublic, songIdToAdd, songIndexToRemove)
|
||||
.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 deletePlaylist(String playlistId) {
|
||||
App.getSubsonicClientInstance(false)
|
||||
.getPlaylistClient()
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.media.audiofx.Equalizer
|
||||
|
||||
class EqualizerManager {
|
||||
|
||||
private var equalizer: Equalizer? = null
|
||||
|
||||
fun attachToSession(audioSessionId: Int): Boolean {
|
||||
release()
|
||||
if (audioSessionId != 0 && audioSessionId != -1) {
|
||||
try {
|
||||
equalizer = Equalizer(0, audioSessionId).apply {
|
||||
enabled = true
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
// Some devices may not support Equalizer or audio session may be invalid
|
||||
equalizer = null
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun setBandLevel(band: Short, level: Short) {
|
||||
equalizer?.setBandLevel(band, level)
|
||||
}
|
||||
|
||||
fun getNumberOfBands(): Short = equalizer?.numberOfBands ?: 0
|
||||
|
||||
fun getBandLevelRange(): ShortArray? = equalizer?.bandLevelRange
|
||||
|
||||
fun getCenterFreq(band: Short): Int? =
|
||||
equalizer?.getCenterFreq(band)?.div(1000)
|
||||
|
||||
fun getBandLevel(band: Short): Short? =
|
||||
equalizer?.getBandLevel(band)
|
||||
|
||||
fun setEnabled(enabled: Boolean) {
|
||||
equalizer?.enabled = enabled
|
||||
}
|
||||
|
||||
fun release() {
|
||||
equalizer?.release()
|
||||
equalizer = null
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
package com.cappielloantonio.tempo.service;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
@@ -21,14 +27,79 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class MediaManager {
|
||||
private static final String TAG = "MediaManager";
|
||||
private static WeakReference<MediaBrowser> attachedBrowserRef = new WeakReference<>(null);
|
||||
|
||||
public static void registerPlaybackObserver(
|
||||
ListenableFuture<MediaBrowser> browserFuture,
|
||||
PlaybackViewModel playbackViewModel
|
||||
) {
|
||||
if (browserFuture == null) return;
|
||||
|
||||
Futures.addCallback(browserFuture, new FutureCallback<MediaBrowser>() {
|
||||
@Override
|
||||
public void onSuccess(MediaBrowser browser) {
|
||||
MediaBrowser current = attachedBrowserRef.get();
|
||||
if (current != browser) {
|
||||
browser.addListener(new Player.Listener() {
|
||||
@Override
|
||||
public void onEvents(@NonNull Player player, @NonNull Player.Events events) {
|
||||
if (events.contains(Player.EVENT_MEDIA_ITEM_TRANSITION)
|
||||
|| events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
|
||||
|| events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
|
||||
|
||||
String mediaId = player.getCurrentMediaItem() != null
|
||||
? player.getCurrentMediaItem().mediaId
|
||||
: null;
|
||||
|
||||
boolean playing = player.getPlaybackState() == Player.STATE_READY
|
||||
&& player.getPlayWhenReady();
|
||||
|
||||
playbackViewModel.update(mediaId, playing);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String mediaId = browser.getCurrentMediaItem() != null
|
||||
? browser.getCurrentMediaItem().mediaId
|
||||
: null;
|
||||
boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady();
|
||||
playbackViewModel.update(mediaId, playing);
|
||||
|
||||
attachedBrowserRef = new WeakReference<>(browser);
|
||||
} else {
|
||||
String mediaId = browser.getCurrentMediaItem() != null
|
||||
? browser.getCurrentMediaItem().mediaId
|
||||
: null;
|
||||
boolean playing = browser.getPlaybackState() == Player.STATE_READY && browser.getPlayWhenReady();
|
||||
playbackViewModel.update(mediaId, playing);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Throwable t) {
|
||||
Log.e(TAG, "Failed to get MediaBrowser instance", t);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
|
||||
public static void onBrowserReleased(@Nullable MediaBrowser released) {
|
||||
MediaBrowser attached = attachedBrowserRef.get();
|
||||
if (attached == released) {
|
||||
attachedBrowserRef.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static void reset(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
|
||||
if (mediaBrowserListenableFuture != null) {
|
||||
@@ -107,11 +178,24 @@ public class MediaManager {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
mediaBrowserListenableFuture.get().clearMediaItems();
|
||||
mediaBrowserListenableFuture.get().setMediaItems(MappingUtil.mapMediaItems(media));
|
||||
mediaBrowserListenableFuture.get().prepare();
|
||||
mediaBrowserListenableFuture.get().seekTo(startIndex, 0);
|
||||
mediaBrowserListenableFuture.get().play();
|
||||
MediaBrowser browser = mediaBrowserListenableFuture.get();
|
||||
browser.clearMediaItems();
|
||||
browser.setMediaItems(MappingUtil.mapMediaItems(media));
|
||||
browser.prepare();
|
||||
|
||||
Player.Listener timelineListener = new Player.Listener() {
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||
int itemCount = browser.getMediaItemCount();
|
||||
if (itemCount > 0 && startIndex >= 0 && startIndex < itemCount) {
|
||||
browser.seekTo(startIndex, 0);
|
||||
browser.play();
|
||||
browser.removeListener(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
browser.addListener(timelineListener);
|
||||
|
||||
enqueueDatabase(media, true, 0);
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
@@ -139,6 +223,25 @@ public class MediaManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static void playDownloadedMediaItem(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, MediaItem mediaItem) {
|
||||
if (mediaBrowserListenableFuture != null && mediaItem != null) {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
if (mediaBrowserListenableFuture.isDone()) {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
mediaBrowser.clearMediaItems();
|
||||
mediaBrowser.setMediaItem(mediaItem);
|
||||
mediaBrowser.prepare();
|
||||
mediaBrowser.play();
|
||||
clearDatabase();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
}
|
||||
}
|
||||
|
||||
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
||||
if (mediaBrowserListenableFuture != null) {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
|
||||
@@ -2,22 +2,28 @@ package com.cappielloantonio.tempo.subsonic
|
||||
|
||||
import com.cappielloantonio.tempo.App
|
||||
import com.cappielloantonio.tempo.subsonic.utils.CacheUtil
|
||||
import com.cappielloantonio.tempo.subsonic.utils.EmptyDateTypeAdapter
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RetrofitClient(subsonic: Subsonic) {
|
||||
var retrofit: Retrofit
|
||||
|
||||
init {
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(Date::class.java, EmptyDateTypeAdapter())
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
retrofit = Retrofit.Builder()
|
||||
.baseUrl(subsonic.url)
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()))
|
||||
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create()))
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.client(getOkHttpClient())
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import android.util.Log;
|
||||
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
|
||||
import com.cappielloantonio.tempo.subsonic.Subsonic;
|
||||
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import retrofit2.Call;
|
||||
|
||||
@@ -21,7 +24,15 @@ public class SystemClient {
|
||||
|
||||
public Call<ApiResponse> ping() {
|
||||
Log.d(TAG, "ping()");
|
||||
return systemService.ping(subsonic.getParams());
|
||||
Call<ApiResponse> pingCall = systemService.ping(subsonic.getParams());
|
||||
if (Preferences.isInUseServerAddressLocal()) {
|
||||
pingCall.timeout()
|
||||
.timeout(1, TimeUnit.SECONDS);
|
||||
} else {
|
||||
pingCall.timeout()
|
||||
.timeout(3, TimeUnit.SECONDS);
|
||||
}
|
||||
return pingCall;
|
||||
}
|
||||
|
||||
public Call<ApiResponse> getLicense() {
|
||||
|
||||
@@ -4,38 +4,36 @@ import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
open class AlbumID3 : Parcelable {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var artist: String? = null
|
||||
var artistId: String? = null
|
||||
open class AlbumID3(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var artist: String? = null,
|
||||
var artistId: String? = null,
|
||||
@SerializedName("coverArt")
|
||||
var coverArtId: String? = null
|
||||
var songCount: Int? = 0
|
||||
var duration: Int? = 0
|
||||
var playCount: Long? = 0
|
||||
var created: Date? = null
|
||||
var starred: Date? = null
|
||||
var year: Int = 0
|
||||
var genre: String? = null
|
||||
var played: Date? = Date(0)
|
||||
var userRating: Int? = 0
|
||||
var recordLabels: List<RecordLabel>? = null
|
||||
var musicBrainzId: String? = null
|
||||
var genres: List<ItemGenre>? = null
|
||||
var artists: List<ArtistID3>? = null
|
||||
var displayArtist: String? = null
|
||||
var releaseTypes: List<String>? = null
|
||||
var moods: List<String>? = null
|
||||
var sortName: String? = null
|
||||
var originalReleaseDate: ItemDate? = null
|
||||
var releaseDate: ItemDate? = null
|
||||
var isCompilation: Boolean? = null
|
||||
var discTitles: List<DiscTitle>? = null
|
||||
}
|
||||
var coverArtId: String? = null,
|
||||
var songCount: Int? = 0,
|
||||
var duration: Int? = 0,
|
||||
var playCount: Long? = 0,
|
||||
var created: Date? = null,
|
||||
var starred: Date? = null,
|
||||
var year: Int = 0,
|
||||
var genre: String? = null,
|
||||
var played: Date? = Date(0),
|
||||
var userRating: Int? = 0,
|
||||
var recordLabels: List<RecordLabel>? = null,
|
||||
var musicBrainzId: String? = null,
|
||||
var genres: List<ItemGenre>? = null,
|
||||
var artists: List<ArtistID3>? = null,
|
||||
var displayArtist: String? = null,
|
||||
var releaseTypes: List<String>? = null,
|
||||
var moods: List<String>? = null,
|
||||
var sortName: String? = null,
|
||||
var originalReleaseDate: ItemDate? = null,
|
||||
var releaseDate: ItemDate? = null,
|
||||
var isCompilation: Boolean? = null,
|
||||
var discTitles: List<DiscTitle>? = null,
|
||||
) : Parcelable
|
||||
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class AlbumWithSongsID3 : AlbumID3(), Parcelable {
|
||||
class AlbumWithSongsID3(
|
||||
@SerializedName("song")
|
||||
var songs: List<Child>? = null
|
||||
}
|
||||
var songs: List<Child>? = null,
|
||||
) : AlbumID3(), Parcelable
|
||||
@@ -7,10 +7,10 @@ import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class Artist : Parcelable {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var starred: Date? = null
|
||||
var userRating: Int? = null
|
||||
var averageRating: Double? = null
|
||||
}
|
||||
class Artist(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var starred: Date? = null,
|
||||
var userRating: Int? = null,
|
||||
var averageRating: Double? = null,
|
||||
) : Parcelable
|
||||
@@ -4,15 +4,15 @@ import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
open class ArtistID3 : Parcelable {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
open class ArtistID3(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
@SerializedName("coverArt")
|
||||
var coverArtId: String? = null
|
||||
var albumCount = 0
|
||||
var starred: Date? = null
|
||||
}
|
||||
var coverArtId: String? = null,
|
||||
var albumCount: Int = 0,
|
||||
var starred: Date? = null,
|
||||
) : Parcelable
|
||||
@@ -7,7 +7,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class ArtistWithAlbumsID3 : ArtistID3(), Parcelable {
|
||||
class ArtistWithAlbumsID3(
|
||||
@SerializedName("album")
|
||||
var albums: List<AlbumID3>? = null
|
||||
}
|
||||
var albums: List<AlbumID3>? = null,
|
||||
) : ArtistID3(), Parcelable
|
||||
@@ -8,15 +8,15 @@ import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class Directory : Parcelable {
|
||||
class Directory(
|
||||
@SerializedName("child")
|
||||
var children: List<Child>? = null
|
||||
var id: String? = null
|
||||
var children: List<Child>? = null,
|
||||
var id: String? = null,
|
||||
@SerializedName("parent")
|
||||
var parentId: String? = null
|
||||
var name: String? = null
|
||||
var starred: Date? = null
|
||||
var userRating: Int? = null
|
||||
var averageRating: Double? = null
|
||||
var playCount: Long? = null
|
||||
}
|
||||
var parentId: String? = null,
|
||||
var name: String? = null,
|
||||
var starred: Date? = null,
|
||||
var userRating: Int? = null,
|
||||
var averageRating: Double? = null,
|
||||
var playCount: Long? = null,
|
||||
) : Parcelable
|
||||
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
open class DiscTitle : Parcelable {
|
||||
var disc: Int? = null
|
||||
var title: String? = null
|
||||
}
|
||||
open class DiscTitle(
|
||||
var disc: Int? = null,
|
||||
var title: String? = null,
|
||||
) : Parcelable
|
||||
@@ -7,9 +7,9 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class Genre : Parcelable {
|
||||
class Genre(
|
||||
@SerializedName("value")
|
||||
var genre: String? = null
|
||||
var songCount = 0
|
||||
var albumCount = 0
|
||||
}
|
||||
var genre: String? = null,
|
||||
var songCount: Int = 0,
|
||||
var albumCount: Int = 0,
|
||||
) : Parcelable
|
||||
@@ -6,9 +6,9 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class InternetRadioStation : Parcelable {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var streamUrl: String? = null
|
||||
var homePageUrl: String? = null
|
||||
}
|
||||
class InternetRadioStation(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
var streamUrl: String? = null,
|
||||
var homePageUrl: String? = null,
|
||||
) : Parcelable
|
||||
@@ -9,11 +9,11 @@ import java.util.Locale
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
open class ItemDate : Parcelable {
|
||||
var year: Int? = null
|
||||
var month: Int? = null
|
||||
var day: Int? = null
|
||||
|
||||
open class ItemDate(
|
||||
var year: Int? = null,
|
||||
var month: Int? = null,
|
||||
var day: Int? = null,
|
||||
) : Parcelable {
|
||||
fun getFormattedDate(): String? {
|
||||
if (year == null && month == null && day == null) return null
|
||||
|
||||
@@ -22,8 +22,7 @@ open class ItemDate : Parcelable {
|
||||
SimpleDateFormat("yyyy", Locale.getDefault())
|
||||
} else if (day == null) {
|
||||
SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
open class ItemGenre : Parcelable {
|
||||
var name: String? = null
|
||||
}
|
||||
open class ItemGenre(
|
||||
var name: String? = null,
|
||||
) : Parcelable
|
||||
@@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class MusicFolder : Parcelable {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
}
|
||||
class MusicFolder(
|
||||
var id: String? = null,
|
||||
var name: String? = null,
|
||||
) : Parcelable
|
||||
@@ -8,10 +8,9 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
class NowPlayingEntry(
|
||||
@SerializedName("_id")
|
||||
override val id: String
|
||||
) : Child(id) {
|
||||
var username: String? = null
|
||||
var minutesAgo = 0
|
||||
var playerId = 0
|
||||
var playerName: String? = null
|
||||
}
|
||||
override val id: String,
|
||||
var username: String? = null,
|
||||
var minutesAgo: Int = 0,
|
||||
var playerId: Int = 0,
|
||||
var playerName: String? = null,
|
||||
) : Child(id)
|
||||
@@ -7,8 +7,9 @@ import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
@@ -16,27 +17,56 @@ import java.util.*
|
||||
open class Playlist(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
open var id: String
|
||||
) : Parcelable {
|
||||
open var id: String,
|
||||
@ColumnInfo(name = "name")
|
||||
var name: String? = null
|
||||
var name: String? = null,
|
||||
@ColumnInfo(name = "duration")
|
||||
var duration: Long = 0,
|
||||
@ColumnInfo(name = "coverArt")
|
||||
var coverArtId: String? = null,
|
||||
) : Parcelable {
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var comment: String? = null
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var owner: String? = null
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
@SerializedName("public")
|
||||
var isUniversal: Boolean? = null
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var songCount: Int = 0
|
||||
@ColumnInfo(name = "duration")
|
||||
var duration: Long = 0
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var created: Date? = null
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var changed: Date? = null
|
||||
@ColumnInfo(name = "coverArt")
|
||||
var coverArtId: String? = null
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var allowedUsers: List<String>? = null
|
||||
@Ignore
|
||||
constructor(
|
||||
id: String,
|
||||
name: String?,
|
||||
comment: String?,
|
||||
owner: String?,
|
||||
isUniversal: Boolean?,
|
||||
songCount: Int,
|
||||
duration: Long,
|
||||
created: Date?,
|
||||
changed: Date?,
|
||||
coverArtId: String?,
|
||||
allowedUsers: List<String>?,
|
||||
) : this(id, name, duration, coverArtId) {
|
||||
this.comment = comment
|
||||
this.owner = owner
|
||||
this.isUniversal = isUniversal
|
||||
this.songCount = songCount
|
||||
this.created = created
|
||||
this.changed = changed
|
||||
this.allowedUsers = allowedUsers
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,7 @@ import kotlinx.parcelize.Parcelize
|
||||
@Parcelize
|
||||
class PlaylistWithSongs(
|
||||
@SerializedName("_id")
|
||||
override var id: String
|
||||
) : Playlist(id), Parcelable {
|
||||
override var id: String,
|
||||
@SerializedName("entry")
|
||||
var entries: List<Child>? = null
|
||||
}
|
||||
var entries: List<Child>? = null,
|
||||
) : Playlist(id), Parcelable
|
||||
@@ -6,5 +6,5 @@ import com.google.gson.annotations.SerializedName
|
||||
@Keep
|
||||
class Playlists(
|
||||
@SerializedName("playlist")
|
||||
var playlists: List<Playlist>? = null
|
||||
var playlists: List<Playlist>? = null,
|
||||
)
|
||||
@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class PodcastChannel : Parcelable {
|
||||
class PodcastChannel(
|
||||
@SerializedName("episode")
|
||||
var episodes: List<PodcastEpisode>? = null
|
||||
var id: String? = null
|
||||
var url: String? = null
|
||||
var title: String? = null
|
||||
var description: String? = null
|
||||
var episodes: List<PodcastEpisode>? = null,
|
||||
var id: String? = null,
|
||||
var url: String? = null,
|
||||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
@SerializedName("coverArt")
|
||||
var coverArtId: String? = null
|
||||
var originalImageUrl: String? = null
|
||||
var status: String? = null
|
||||
var errorMessage: String? = null
|
||||
}
|
||||
var coverArtId: String? = null,
|
||||
var originalImageUrl: String? = null,
|
||||
var status: String? = null,
|
||||
var errorMessage: String? = null,
|
||||
) : Parcelable
|
||||
@@ -3,37 +3,38 @@ package com.cappielloantonio.tempo.subsonic.models
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class PodcastEpisode : Parcelable {
|
||||
var id: String? = null
|
||||
class PodcastEpisode(
|
||||
var id: String? = null,
|
||||
@SerializedName("parent")
|
||||
var parentId: String? = null
|
||||
var isDir = false
|
||||
var title: String? = null
|
||||
var album: String? = null
|
||||
var artist: String? = null
|
||||
var year: Int? = null
|
||||
var genre: String? = null
|
||||
var parentId: String? = null,
|
||||
var isDir: Boolean = false,
|
||||
var title: String? = null,
|
||||
var album: String? = null,
|
||||
var artist: String? = null,
|
||||
var year: Int? = null,
|
||||
var genre: String? = null,
|
||||
@SerializedName("coverArt")
|
||||
var coverArtId: String? = null
|
||||
var size: Long? = null
|
||||
var contentType: String? = null
|
||||
var suffix: String? = null
|
||||
var duration: Int? = null
|
||||
var coverArtId: String? = null,
|
||||
var size: Long? = null,
|
||||
var contentType: String? = null,
|
||||
var suffix: String? = null,
|
||||
var duration: Int? = null,
|
||||
@SerializedName("bitRate")
|
||||
var bitrate: Int? = null
|
||||
var path: String? = null
|
||||
var isVideo: Boolean = false
|
||||
var created: Date? = null
|
||||
var artistId: String? = null
|
||||
var type: String? = null
|
||||
var streamId: String? = null
|
||||
var channelId: String? = null
|
||||
var description: String? = null
|
||||
var status: String? = null
|
||||
var publishDate: Date? = null
|
||||
}
|
||||
var bitrate: Int? = null,
|
||||
var path: String? = null,
|
||||
var isVideo: Boolean = false,
|
||||
var created: Date? = null,
|
||||
var artistId: String? = null,
|
||||
var type: String? = null,
|
||||
var streamId: String? = null,
|
||||
var channelId: String? = null,
|
||||
var description: String? = null,
|
||||
var status: String? = null,
|
||||
var publishDate: Date? = null,
|
||||
) : Parcelable
|
||||
@@ -2,10 +2,11 @@ package com.cappielloantonio.tempo.subsonic.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
open class RecordLabel : Parcelable {
|
||||
var name: String? = null
|
||||
}
|
||||
open class RecordLabel(
|
||||
var name: String? = null,
|
||||
) : Parcelable
|
||||
@@ -3,20 +3,21 @@ package com.cappielloantonio.tempo.subsonic.models
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Keep
|
||||
@Parcelize
|
||||
class Share : Parcelable {
|
||||
data class Share(
|
||||
@SerializedName("entry")
|
||||
var entries: List<Child>? = null
|
||||
var id: String? = null
|
||||
var url: String? = null
|
||||
var description: String? = null
|
||||
var username: String? = null
|
||||
var created: Date? = null
|
||||
var expires: Date? = null
|
||||
var lastVisited: Date? = null
|
||||
var visitCount = 0
|
||||
}
|
||||
var entries: List<Child>? = null,
|
||||
var id: String? = null,
|
||||
var url: String? = null,
|
||||
var description: String? = null,
|
||||
var username: String? = null,
|
||||
var created: Date? = null,
|
||||
var expires: Date? = null,
|
||||
var lastVisited: Date? = null,
|
||||
var visitCount: Int = 0
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.cappielloantonio.tempo.subsonic.utils
|
||||
|
||||
import com.google.gson.JsonDeserializationContext
|
||||
import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import java.lang.reflect.Type
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
// This adapter handles Date objects, returning null if the JSON string is empty or unparsable.
|
||||
class EmptyDateTypeAdapter : JsonDeserializer<Date> {
|
||||
|
||||
// Define the date formats expected from the Subsonic server.
|
||||
private val dateFormats: List<SimpleDateFormat> = listOf(
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") },
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
)
|
||||
|
||||
@Throws(JsonParseException::class)
|
||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Date? {
|
||||
val jsonString = json.asString.trim()
|
||||
|
||||
if (jsonString.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (format in dateFormats) {
|
||||
try {
|
||||
return format.parse(jsonString)
|
||||
} catch (e: ParseException) {
|
||||
// Ignore and try the next format
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.cappielloantonio.tempo.ui.activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
@@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
|
||||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
@@ -31,6 +37,8 @@ import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
|
||||
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
|
||||
import com.cappielloantonio.tempo.util.AssetLinkNavigator;
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.MainViewModel;
|
||||
@@ -54,8 +62,11 @@ public class MainActivity extends BaseActivity {
|
||||
private BottomNavigationView bottomNavigationView;
|
||||
public NavController navController;
|
||||
private BottomSheetBehavior bottomSheetBehavior;
|
||||
private AssetLinkNavigator assetLinkNavigator;
|
||||
private AssetLinkUtil.AssetLink pendingAssetLink;
|
||||
|
||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||
private Intent pendingDownloadPlaybackIntent;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -69,6 +80,7 @@ public class MainActivity extends BaseActivity {
|
||||
setContentView(view);
|
||||
|
||||
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
|
||||
assetLinkNavigator = new AssetLinkNavigator(this);
|
||||
|
||||
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
|
||||
connectivityStatusReceiverManager(true);
|
||||
@@ -77,12 +89,16 @@ public class MainActivity extends BaseActivity {
|
||||
checkConnectionType();
|
||||
getOpenSubsonicExtensions();
|
||||
checkTempoUpdate();
|
||||
|
||||
maybeSchedulePlaybackIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
pingServer();
|
||||
initService();
|
||||
consumePendingPlaybackIntent();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -98,6 +114,14 @@ public class MainActivity extends BaseActivity {
|
||||
bind = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
maybeSchedulePlaybackIntent(intent);
|
||||
consumePendingPlaybackIntent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
||||
@@ -292,6 +316,24 @@ public class MainActivity extends BaseActivity {
|
||||
public void goFromLogin() {
|
||||
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
|
||||
goToHome();
|
||||
consumePendingAssetLink();
|
||||
}
|
||||
|
||||
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink) {
|
||||
openAssetLink(assetLink, true);
|
||||
}
|
||||
|
||||
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink, boolean collapsePlayer) {
|
||||
if (!isUserAuthenticated()) {
|
||||
pendingAssetLink = assetLink;
|
||||
return;
|
||||
}
|
||||
if (collapsePlayer) {
|
||||
setBottomSheetInPeek(true);
|
||||
}
|
||||
if (assetLinkNavigator != null) {
|
||||
assetLinkNavigator.open(assetLink);
|
||||
}
|
||||
}
|
||||
|
||||
public void quit() {
|
||||
@@ -316,6 +358,7 @@ public class MainActivity extends BaseActivity {
|
||||
Preferences.setSkipSilenceMode(false);
|
||||
Preferences.setDataSavingMode(false);
|
||||
Preferences.setStarredSyncEnabled(false);
|
||||
Preferences.setStarredAlbumsSyncEnabled(false);
|
||||
}
|
||||
|
||||
private void resetMusicSession() {
|
||||
@@ -350,6 +393,7 @@ public class MainActivity extends BaseActivity {
|
||||
Preferences.switchInUseServerAddress();
|
||||
App.refreshSubsonicClient();
|
||||
pingServer();
|
||||
resetView();
|
||||
} else {
|
||||
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
|
||||
}
|
||||
@@ -360,6 +404,7 @@ public class MainActivity extends BaseActivity {
|
||||
Preferences.switchInUseServerAddress();
|
||||
App.refreshSubsonicClient();
|
||||
pingServer();
|
||||
resetView();
|
||||
} else {
|
||||
mainViewModel.ping().observe(this, subsonicResponse -> {
|
||||
if (subsonicResponse == null) {
|
||||
@@ -375,6 +420,13 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void resetView() {
|
||||
resetViewModel();
|
||||
int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
|
||||
navController.popBackStack(id, true);
|
||||
navController.navigate(id);
|
||||
}
|
||||
|
||||
private void getOpenSubsonicExtensions() {
|
||||
if (Preferences.getToken() != null) {
|
||||
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
||||
@@ -407,4 +459,98 @@ public class MainActivity extends BaseActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSchedulePlaybackIntent(Intent intent) {
|
||||
if (intent == null) return;
|
||||
if (Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD.equals(intent.getAction())
|
||||
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
|
||||
pendingDownloadPlaybackIntent = new Intent(intent);
|
||||
}
|
||||
handleAssetLinkIntent(intent);
|
||||
}
|
||||
|
||||
private void consumePendingPlaybackIntent() {
|
||||
if (pendingDownloadPlaybackIntent == null) return;
|
||||
Intent intent = pendingDownloadPlaybackIntent;
|
||||
pendingDownloadPlaybackIntent = null;
|
||||
playDownloadedMedia(intent);
|
||||
}
|
||||
|
||||
private void handleAssetLinkIntent(Intent intent) {
|
||||
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.parse(intent);
|
||||
if (assetLink == null) {
|
||||
return;
|
||||
}
|
||||
if (!isUserAuthenticated()) {
|
||||
pendingAssetLink = assetLink;
|
||||
intent.setData(null);
|
||||
return;
|
||||
}
|
||||
if (assetLinkNavigator != null) {
|
||||
assetLinkNavigator.open(assetLink);
|
||||
}
|
||||
intent.setData(null);
|
||||
}
|
||||
|
||||
private boolean isUserAuthenticated() {
|
||||
return Preferences.getPassword() != null
|
||||
|| (Preferences.getToken() != null && Preferences.getSalt() != null);
|
||||
}
|
||||
|
||||
private void consumePendingAssetLink() {
|
||||
if (pendingAssetLink == null || assetLinkNavigator == null) {
|
||||
return;
|
||||
}
|
||||
assetLinkNavigator.open(pendingAssetLink);
|
||||
pendingAssetLink = null;
|
||||
}
|
||||
|
||||
private void playDownloadedMedia(Intent intent) {
|
||||
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
String mediaId = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID);
|
||||
if (TextUtils.isEmpty(mediaId)) {
|
||||
mediaId = uri.toString();
|
||||
}
|
||||
|
||||
String title = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_TITLE);
|
||||
String artist = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ARTIST);
|
||||
String album = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ALBUM);
|
||||
int duration = intent.getIntExtra(Constants.EXTRA_DOWNLOAD_DURATION, 0);
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString("id", mediaId);
|
||||
extras.putString("title", title);
|
||||
extras.putString("artist", artist);
|
||||
extras.putString("album", album);
|
||||
extras.putString("uri", uri.toString());
|
||||
extras.putString("type", Constants.MEDIA_TYPE_MUSIC);
|
||||
extras.putInt("duration", duration);
|
||||
|
||||
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder()
|
||||
.setExtras(extras)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true);
|
||||
|
||||
if (!TextUtils.isEmpty(title)) metadataBuilder.setTitle(title);
|
||||
if (!TextUtils.isEmpty(artist)) metadataBuilder.setArtist(artist);
|
||||
if (!TextUtils.isEmpty(album)) metadataBuilder.setAlbumTitle(album);
|
||||
|
||||
MediaItem mediaItem = new MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.setUri(uri)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.setRequestMetadata(new MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setExtras(extras)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAdapter.ViewHolder> implements Filterable {
|
||||
@@ -152,12 +153,20 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
|
||||
}
|
||||
|
||||
public void sort(String order) {
|
||||
if (albums == null) return;
|
||||
|
||||
switch (order) {
|
||||
case Constants.ALBUM_ORDER_BY_NAME:
|
||||
albums.sort(Comparator.comparing(AlbumID3::getName));
|
||||
albums.sort(Comparator.comparing(
|
||||
album -> album.getName() != null ? album.getName() : "",
|
||||
String.CASE_INSENSITIVE_ORDER
|
||||
));
|
||||
break;
|
||||
case Constants.ALBUM_ORDER_BY_ARTIST:
|
||||
albums.sort(Comparator.comparing(AlbumID3::getArtist, Comparator.nullsLast(Comparator.naturalOrder())));
|
||||
albums.sort(Comparator.comparing(
|
||||
album -> album.getArtist() != null ? album.getArtist() : "",
|
||||
String.CASE_INSENSITIVE_ORDER
|
||||
));
|
||||
break;
|
||||
case Constants.ALBUM_ORDER_BY_YEAR:
|
||||
albums.sort(Comparator.comparing(AlbumID3::getYear));
|
||||
@@ -166,15 +175,23 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
|
||||
Collections.shuffle(albums);
|
||||
break;
|
||||
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
|
||||
albums.sort(Comparator.comparing(AlbumID3::getCreated));
|
||||
albums.sort(Comparator.comparing(
|
||||
album -> album.getCreated() != null ? album.getCreated() : new Date(0),
|
||||
Comparator.nullsLast(Date::compareTo)
|
||||
));
|
||||
Collections.reverse(albums);
|
||||
break;
|
||||
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
|
||||
albums.sort(Comparator.comparing(AlbumID3::getPlayed));
|
||||
albums.sort(Comparator.comparing(
|
||||
album -> album.getPlayed() != null ? album.getPlayed() : new Date(0),
|
||||
Comparator.nullsLast(Date::compareTo)
|
||||
));
|
||||
Collections.reverse(albums);
|
||||
break;
|
||||
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
|
||||
albums.sort(Comparator.comparing(AlbumID3::getPlayCount));
|
||||
albums.sort(Comparator.comparing(
|
||||
album -> album.getPlayCount() != null ? album.getPlayCount() : 0L
|
||||
));
|
||||
Collections.reverse(albums);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
|
||||
R.string.song_subtitle_formatter,
|
||||
song.getArtist(),
|
||||
MusicUtil.getReadableDurationString(song.getDuration(), false),
|
||||
""
|
||||
MusicUtil.getReadableAudioQualityString(song)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -23,17 +24,24 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
|
||||
private static final String TAG = "PlayerSongQueueAdapter";
|
||||
private final ClickCallback click;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private List<Child> songs;
|
||||
|
||||
private String currentPlayingId;
|
||||
private boolean isPlaying;
|
||||
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||
|
||||
public PlayerSongQueueAdapter(ClickCallback click) {
|
||||
this.click = click;
|
||||
this.songs = Collections.emptyList();
|
||||
@@ -104,6 +112,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
||||
} else {
|
||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
int pos = holder.getBindingAdapterPosition();
|
||||
Child s = songs.get(pos);
|
||||
if (currentPlayingId != null && currentPlayingId.equals(s.getId())) {
|
||||
if (isPlaying) {
|
||||
mediaBrowser.pause();
|
||||
} else {
|
||||
mediaBrowser.play();
|
||||
}
|
||||
} else {
|
||||
mediaBrowser.seekTo(pos, 0);
|
||||
mediaBrowser.play();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error obtaining MediaBrowser", e);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
|
||||
});
|
||||
bindPlaybackState(holder, song);
|
||||
}
|
||||
|
||||
private void bindPlaybackState(@NonNull PlayerSongQueueAdapter.ViewHolder holder, @NonNull Child song) {
|
||||
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
|
||||
|
||||
if (isCurrent) {
|
||||
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
|
||||
if (isPlaying) {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
|
||||
} else {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
|
||||
}
|
||||
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
|
||||
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Child> getItems() {
|
||||
@@ -132,6 +180,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
||||
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
||||
}
|
||||
|
||||
public void setPlaybackState(String mediaId, boolean playing) {
|
||||
String oldId = this.currentPlayingId;
|
||||
boolean oldPlaying = this.isPlaying;
|
||||
List<Integer> oldPositions = currentPlayingPositions;
|
||||
|
||||
this.currentPlayingId = mediaId;
|
||||
this.isPlaying = playing;
|
||||
|
||||
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
|
||||
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
if (oldPositions.equals(newPositionsCheck)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
|
||||
for (int pos : oldPositions) {
|
||||
if (pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
for (int pos : currentPlayingPositions) {
|
||||
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> findPositionsById(String id) {
|
||||
if (id == null) return Collections.emptyList();
|
||||
List<Integer> positions = new ArrayList<>();
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
if (id.equals(songs.get(i).getId())) {
|
||||
positions.add(i);
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
public Child getItem(int id) {
|
||||
return songs.get(id);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.cappielloantonio.tempo.ui.adapter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -9,7 +11,9 @@ import android.widget.Filterable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
@@ -21,8 +25,11 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -30,6 +37,7 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@UnstableApi
|
||||
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> implements Filterable {
|
||||
@@ -42,6 +50,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
private List<Child> songs;
|
||||
private String currentFilter;
|
||||
|
||||
private String currentPlayingId;
|
||||
private boolean isPlaying;
|
||||
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private final Filter filtering = new Filter() {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence constraint) {
|
||||
@@ -70,10 +83,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
songs = (List<Child>) results.values;
|
||||
notifyDataSetChanged();
|
||||
|
||||
for (int pos : currentPlayingPositions) {
|
||||
if (pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
||||
public SongHorizontalAdapter(LifecycleOwner lifecycleOwner, ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
||||
this.click = click;
|
||||
this.showCoverArt = showCoverArt;
|
||||
this.showAlbum = showAlbum;
|
||||
@@ -81,6 +100,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
this.songsFull = Collections.emptyList();
|
||||
this.currentFilter = "";
|
||||
this.album = album;
|
||||
setHasStableIds(false);
|
||||
|
||||
if (lifecycleOwner != null) {
|
||||
MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -91,7 +115,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && payloads.contains("payload_playback")) {
|
||||
bindPlaybackState(holder, songs.get(position));
|
||||
} else {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
Child song = songs.get(position);
|
||||
|
||||
holder.item.searchResultSongTitleTextView.setText(song.getTitle());
|
||||
@@ -109,10 +142,18 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
|
||||
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
||||
|
||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||
if (ExternalAudioReader.getUri(song) != null) {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (showCoverArt) CustomGlideRequest.Builder
|
||||
@@ -165,6 +206,39 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
} else {
|
||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
bindPlaybackState(holder, song);
|
||||
}
|
||||
|
||||
private void handleExternalAudioRefresh() {
|
||||
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) {
|
||||
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
|
||||
|
||||
if (isCurrent) {
|
||||
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
|
||||
if (isPlaying) {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
|
||||
} else {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
|
||||
}
|
||||
if (!showCoverArt) {
|
||||
holder.item.trackNumberTextView.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
|
||||
if (!showCoverArt) {
|
||||
holder.item.trackNumberTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -188,6 +262,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPlaybackState(String mediaId, boolean playing) {
|
||||
String oldId = this.currentPlayingId;
|
||||
boolean oldPlaying = this.isPlaying;
|
||||
List<Integer> oldPositions = currentPlayingPositions;
|
||||
|
||||
this.currentPlayingId = mediaId;
|
||||
this.isPlaying = playing;
|
||||
|
||||
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
|
||||
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
if (oldPositions.equals(newPositionsCheck)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
|
||||
for (int pos : oldPositions) {
|
||||
if (pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
for (int pos : currentPlayingPositions) {
|
||||
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> findPositionsById(String id) {
|
||||
if (id == null) return Collections.emptyList();
|
||||
List<Integer> positions = new ArrayList<>();
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
if (id.equals(songs.get(i).getId())) {
|
||||
positions.add(i);
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filtering;
|
||||
@@ -215,11 +329,29 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
}
|
||||
|
||||
public void onClick() {
|
||||
int pos = getBindingAdapterPosition();
|
||||
Child tappedSong = songs.get(pos);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())));
|
||||
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()));
|
||||
|
||||
click.onMediaClick(bundle);
|
||||
if (tappedSong.getId().equals(currentPlayingId)) {
|
||||
Log.i("SongHorizontalAdapter", "Tapping on currently playing song, toggling playback");
|
||||
try{
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
Log.i("SongHorizontalAdapter", "MediaBrowser retrieved, isPlaying: " + isPlaying);
|
||||
if (isPlaying) {
|
||||
mediaBrowser.pause();
|
||||
} else {
|
||||
mediaBrowser.play();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.e("SongHorizontalAdapter", "Error getting MediaBrowser", e);
|
||||
}
|
||||
} else {
|
||||
click.onMediaClick(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onLongClick() {
|
||||
@@ -247,4 +379,8 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setMediaBrowserListenableFuture(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
|
||||
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
@@ -12,6 +15,9 @@ import androidx.media3.common.util.UnstableApi;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
@@ -42,7 +48,21 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
||||
if (dialog != null) {
|
||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
}
|
||||
|
||||
String uriString = Preferences.getDownloadDirectoryUri();
|
||||
if (uriString != null) {
|
||||
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
|
||||
if (directory != null && directory.canWrite()) {
|
||||
for (DocumentFile file : directory.listFiles()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
ExternalAudioReader.refreshCache();
|
||||
ExternalDownloadMetadataStore.clear();
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
public class DownloadDirectoryPickerDialog extends DialogFragment {
|
||||
|
||||
private ActivityResultLauncher<Intent> folderPickerLauncher;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public android.app.Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Register launcher *before* button triggers
|
||||
folderPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == android.app.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(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Set Download Directory")
|
||||
.setMessage("Choose a folder where downloaded songs will be stored.")
|
||||
.setPositiveButton("Choose Folder", (dialog, which) -> {
|
||||
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);
|
||||
folderPickerLauncher.launch(intent);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public class DownloadStorageDialog extends DialogFragment {
|
||||
.setTitle(R.string.download_storage_dialog_title)
|
||||
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
||||
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
||||
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
@@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||
neutralButton.setOnClickListener(v -> {
|
||||
int currentPreference = Preferences.getDownloadStoragePreference();
|
||||
int newPreference = 2;
|
||||
|
||||
if (currentPreference != newPreference) {
|
||||
Preferences.setDownloadStoragePreference(newPreference);
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
dialogClickCallback.onNeutralClick();
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
@@ -26,6 +27,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||
|
||||
private PlaylistDialogHorizontalAdapter playlistDialogHorizontalAdapter;
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
@@ -97,8 +99,11 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
|
||||
|
||||
@Override
|
||||
public void onPlaylistClick(Bundle bundle) {
|
||||
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
||||
playlistChooserViewModel.addSongsToPlaylist(playlist.getId());
|
||||
dismiss();
|
||||
if (playlistChooserViewModel.getSongsToAdd() != null && !playlistChooserViewModel.getSongsToAdd().isEmpty()) {
|
||||
Playlist playlist = bundle.getParcelable(Constants.PLAYLIST_OBJECT);
|
||||
playlistChooserViewModel.addSongsToPlaylist(this, getDialog(), playlist.getId());
|
||||
} else {
|
||||
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_add_failure, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,11 @@ public class RatingDialog extends DialogFragment {
|
||||
bind.ratingBar.setRating(song.getUserRating() != null ? song.getUserRating() : 0);
|
||||
});
|
||||
} else if (ratingViewModel.getAlbum() != null) {
|
||||
ratingViewModel.getLiveAlbum().observe(this, album -> bind.ratingBar.setRating(/*album.getRating()*/ 0));
|
||||
ratingViewModel.getLiveAlbum().observe(this, album -> {
|
||||
if (album != null) {
|
||||
bind.ratingBar.setRating(album.getUserRating() != null ? album.getUserRating() : 0);
|
||||
}
|
||||
});
|
||||
} else if (ratingViewModel.getArtist() != null) {
|
||||
ratingViewModel.getLiveArtist().observe(this, artist -> bind.ratingBar.setRating(/*artist.getRating()*/ 0));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogStarredAlbumSyncBinding;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.StarredAlbumsSyncViewModel;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class StarredAlbumSyncDialog extends DialogFragment {
|
||||
private StarredAlbumsSyncViewModel starredAlbumsSyncViewModel;
|
||||
|
||||
private Runnable onCancel;
|
||||
|
||||
public StarredAlbumSyncDialog(Runnable onCancel) {
|
||||
this.onCancel = onCancel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
DialogStarredAlbumSyncBinding bind = DialogStarredAlbumSyncBinding.inflate(getLayoutInflater());
|
||||
|
||||
starredAlbumsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredAlbumsSyncViewModel.class);
|
||||
|
||||
return new MaterialAlertDialogBuilder(getActivity())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.starred_album_sync_dialog_title)
|
||||
.setPositiveButton(R.string.starred_sync_dialog_positive_button, null)
|
||||
.setNeutralButton(R.string.starred_sync_dialog_neutral_button, null)
|
||||
.setNegativeButton(R.string.starred_sync_dialog_negative_button, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setButtonAction(requireContext());
|
||||
}
|
||||
|
||||
private void setButtonAction(Context context) {
|
||||
androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog();
|
||||
|
||||
if (dialog != null) {
|
||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
starredAlbumsSyncViewModel.getStarredAlbumSongs(requireActivity()).observe(this, allSongs -> {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(allSongs),
|
||||
allSongs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||
neutralButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredAlbumsSyncEnabled(true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
|
||||
negativeButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredAlbumsSyncEnabled(false);
|
||||
if (onCancel != null) onCancel.run();
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogStarredArtistSyncBinding;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.StarredArtistsSyncViewModel;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class StarredArtistSyncDialog extends DialogFragment {
|
||||
private StarredArtistsSyncViewModel starredArtistsSyncViewModel;
|
||||
|
||||
private Runnable onCancel;
|
||||
|
||||
public StarredArtistSyncDialog(Runnable onCancel) {
|
||||
this.onCancel = onCancel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
DialogStarredArtistSyncBinding bind = DialogStarredArtistSyncBinding.inflate(getLayoutInflater());
|
||||
|
||||
starredArtistsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredArtistsSyncViewModel.class);
|
||||
|
||||
return new MaterialAlertDialogBuilder(getActivity())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.starred_artist_sync_dialog_title)
|
||||
.setPositiveButton(R.string.starred_sync_dialog_positive_button, null)
|
||||
.setNeutralButton(R.string.starred_sync_dialog_neutral_button, null)
|
||||
.setNegativeButton(R.string.starred_sync_dialog_negative_button, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setButtonAction(requireContext());
|
||||
}
|
||||
|
||||
private void setButtonAction(Context context) {
|
||||
androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog();
|
||||
|
||||
if (dialog != null) {
|
||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
starredArtistsSyncViewModel.getStarredArtistSongs(requireActivity()).observe(this, allSongs -> {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(allSongs),
|
||||
allSongs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||
neutralButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredArtistsSyncEnabled(true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
|
||||
negativeButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredArtistsSyncEnabled(false);
|
||||
if (onCancel != null) onCancel.run();
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,12 @@ import java.util.stream.Collectors;
|
||||
public class StarredSyncDialog extends DialogFragment {
|
||||
private StarredSyncViewModel starredSyncViewModel;
|
||||
|
||||
private Runnable onCancel;
|
||||
|
||||
public StarredSyncDialog(Runnable onCancel) {
|
||||
this.onCancel = onCancel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
@@ -55,7 +61,7 @@ public class StarredSyncDialog extends DialogFragment {
|
||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
||||
if (songs != null) {
|
||||
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
@@ -75,6 +81,7 @@ public class StarredSyncDialog extends DialogFragment {
|
||||
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
|
||||
negativeButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredSyncEnabled(false);
|
||||
if (onCancel != null) onCancel.run();
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
@@ -10,15 +11,23 @@ import androidx.media3.common.MediaMetadata;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class TrackInfoDialog extends DialogFragment {
|
||||
private DialogTrackInfoBinding bind;
|
||||
|
||||
private final MediaMetadata mediaMetadata;
|
||||
private AssetLinkUtil.AssetLink songLink;
|
||||
private AssetLinkUtil.AssetLink albumLink;
|
||||
private AssetLinkUtil.AssetLink artistLink;
|
||||
private AssetLinkUtil.AssetLink genreLink;
|
||||
private AssetLinkUtil.AssetLink yearLink;
|
||||
|
||||
public TrackInfoDialog(MediaMetadata mediaMetadata) {
|
||||
this.mediaMetadata = mediaMetadata;
|
||||
@@ -50,32 +59,69 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
}
|
||||
|
||||
private void setTrackInfo() {
|
||||
genreLink = null;
|
||||
yearLink = null;
|
||||
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
|
||||
bind.trakArtistInfoTextView.setText(mediaMetadata.artist);
|
||||
bind.trakArtistInfoTextView.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
if (mediaMetadata.extras != null) {
|
||||
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
|
||||
albumLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ALBUM, mediaMetadata.extras.getString("albumId"));
|
||||
artistLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, mediaMetadata.extras.getString("artistId"));
|
||||
genreLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkGenre"));
|
||||
yearLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkYear"));
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
|
||||
.build()
|
||||
.into(bind.trackCoverInfoImageView);
|
||||
|
||||
bind.titleValueSector.setText(mediaMetadata.extras.getString("title", getString(R.string.label_placeholder)));
|
||||
bind.albumValueSector.setText(mediaMetadata.extras.getString("album", getString(R.string.label_placeholder)));
|
||||
bind.artistValueSector.setText(mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder)));
|
||||
bind.trackNumberValueSector.setText(String.valueOf(mediaMetadata.extras.getInt("track", 0)));
|
||||
bind.yearValueSector.setText(String.valueOf(mediaMetadata.extras.getInt("year", 0)));
|
||||
bind.genreValueSector.setText(mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder)));
|
||||
bind.sizeValueSector.setText(MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)));
|
||||
bindAssetLink(bind.trackCoverInfoImageView, albumLink != null ? albumLink : songLink);
|
||||
bindAssetLink(bind.trakTitleInfoTextView, songLink);
|
||||
bindAssetLink(bind.trakArtistInfoTextView, artistLink != null ? artistLink : songLink);
|
||||
|
||||
String titleValue = mediaMetadata.extras.getString("title", getString(R.string.label_placeholder));
|
||||
String albumValue = mediaMetadata.extras.getString("album", 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));
|
||||
int yearValue = mediaMetadata.extras.getInt("year", 0);
|
||||
|
||||
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
|
||||
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
|
||||
}
|
||||
|
||||
if (yearLink == null && yearValue != 0) {
|
||||
yearLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(yearValue));
|
||||
}
|
||||
|
||||
bind.titleValueSector.setText(titleValue);
|
||||
bind.albumValueSector.setText(albumValue);
|
||||
bind.artistValueSector.setText(artistValue);
|
||||
bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder));
|
||||
bind.yearValueSector.setText(yearValue != 0 ? String.valueOf(yearValue) : getString(R.string.label_placeholder));
|
||||
bind.genreValueSector.setText(genreValue);
|
||||
bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder));
|
||||
bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder)));
|
||||
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
|
||||
bind.transcodedContentTypeValueSector.setText(mediaMetadata.extras.getString("transcodedContentType", getString(R.string.label_placeholder)));
|
||||
bind.transcodedSuffixValueSector.setText(mediaMetadata.extras.getString("transcodedSuffix", getString(R.string.label_placeholder)));
|
||||
bind.durationValueSector.setText(MusicUtil.getReadableDurationString(mediaMetadata.extras.getInt("duration", 0), false));
|
||||
bind.bitrateValueSector.setText(mediaMetadata.extras.getInt("bitrate", 0) + " kbps");
|
||||
bind.durationValueSector.setText(mediaMetadata.extras.getInt("duration", 0) != 0 ? MusicUtil.getReadableDurationString(mediaMetadata.extras.getInt("duration", 0), false) : getString(R.string.label_placeholder));
|
||||
bind.bitrateValueSector.setText(mediaMetadata.extras.getInt("bitrate", 0) != 0 ? mediaMetadata.extras.getInt("bitrate", 0) + " kbps" : getString(R.string.label_placeholder));
|
||||
bind.samplingRateValueSector.setText(mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? mediaMetadata.extras.getInt("samplingRate", 0) + " Hz" : getString(R.string.label_placeholder));
|
||||
bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : getString(R.string.label_placeholder));
|
||||
bind.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder)));
|
||||
bind.discNumberValueSector.setText(String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)));
|
||||
bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder));
|
||||
|
||||
bindAssetLink(bind.titleValueSector, songLink);
|
||||
bindAssetLink(bind.albumValueSector, albumLink);
|
||||
bindAssetLink(bind.artistValueSector, artistLink);
|
||||
bindAssetLink(bind.genreValueSector, genreLink);
|
||||
bindAssetLink(bind.yearValueSector, yearLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,4 +174,31 @@ public class TrackInfoDialog extends DialogFragment {
|
||||
bind.trakTranscodingInfoTextView.setText(info);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindAssetLink(android.view.View view, AssetLinkUtil.AssetLink assetLink) {
|
||||
if (view == null) return;
|
||||
if (assetLink == null) {
|
||||
AssetLinkUtil.clearLinkAppearance(view);
|
||||
view.setOnClickListener(null);
|
||||
view.setOnLongClickListener(null);
|
||||
view.setClickable(false);
|
||||
view.setLongClickable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setClickable(true);
|
||||
view.setLongClickable(true);
|
||||
AssetLinkUtil.applyLinkAppearance(view);
|
||||
view.setOnClickListener(v -> {
|
||||
dismissAllowingStateLoss();
|
||||
boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
|
||||
((com.cappielloantonio.tempo.ui.activity.MainActivity) requireActivity()).openAssetLink(assetLink, collapse);
|
||||
});
|
||||
view.setOnLongClickListener(v -> {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,17 +32,19 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumCatalogueViewModel;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
private static final String TAG = "ArtistCatalogueFragment";
|
||||
private static final String TAG = "AlbumCatalogueFragment";
|
||||
|
||||
private FragmentAlbumCatalogueBinding bind;
|
||||
private MainActivity activity;
|
||||
private AlbumCatalogueViewModel albumCatalogueViewModel;
|
||||
|
||||
private AlbumCatalogueAdapter albumAdapter;
|
||||
private String currentSortOrder;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
@@ -115,7 +117,10 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
albumAdapter = new AlbumCatalogueAdapter(this, true);
|
||||
albumAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
|
||||
bind.albumCatalogueRecyclerView.setAdapter(albumAdapter);
|
||||
albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> albumAdapter.setItems(albums));
|
||||
albumCatalogueViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> {
|
||||
albumAdapter.setItems(albums);
|
||||
applySavedSortOrder();
|
||||
});
|
||||
|
||||
bind.albumCatalogueRecyclerView.setOnTouchListener((v, event) -> {
|
||||
hideKeyboard(v);
|
||||
@@ -137,6 +142,44 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
});
|
||||
}
|
||||
|
||||
private void applySavedSortOrder() {
|
||||
String savedSortOrder = Preferences.getAlbumSortOrder();
|
||||
currentSortOrder = savedSortOrder;
|
||||
albumAdapter.sort(savedSortOrder);
|
||||
updateSortIndicator();
|
||||
}
|
||||
|
||||
private void updateSortIndicator() {
|
||||
if (bind == null) return;
|
||||
|
||||
String sortText = getSortDisplayText(currentSortOrder);
|
||||
bind.albumListSortTextView.setText(sortText);
|
||||
bind.albumListSortTextView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private String getSortDisplayText(String sortOrder) {
|
||||
if (sortOrder == null) return "";
|
||||
|
||||
switch (sortOrder) {
|
||||
case Constants.ALBUM_ORDER_BY_NAME:
|
||||
return getString(R.string.menu_sort_name);
|
||||
case Constants.ALBUM_ORDER_BY_ARTIST:
|
||||
return getString(R.string.menu_group_by_artist);
|
||||
case Constants.ALBUM_ORDER_BY_YEAR:
|
||||
return getString(R.string.menu_sort_year);
|
||||
case Constants.ALBUM_ORDER_BY_RANDOM:
|
||||
return getString(R.string.menu_sort_random);
|
||||
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
|
||||
return getString(R.string.menu_sort_recently_added);
|
||||
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
|
||||
return getString(R.string.menu_sort_recently_played);
|
||||
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
|
||||
return getString(R.string.menu_sort_most_played);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.toolbar_menu, menu);
|
||||
@@ -172,26 +215,29 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
|
||||
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
String newSortOrder = null;
|
||||
|
||||
if (menuItem.getItemId() == R.id.menu_album_sort_name) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_NAME);
|
||||
return true;
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_NAME;
|
||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_artist) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_ARTIST);
|
||||
return true;
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_ARTIST;
|
||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_year) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_YEAR);
|
||||
return true;
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_YEAR;
|
||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_random) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RANDOM);
|
||||
return true;
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_RANDOM;
|
||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_ADDED);
|
||||
return true;
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_ADDED;
|
||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED);
|
||||
return true;
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED;
|
||||
} else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) {
|
||||
albumAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_PLAYED);
|
||||
newSortOrder = Constants.ALBUM_ORDER_BY_MOST_PLAYED;
|
||||
}
|
||||
|
||||
if (newSortOrder != null) {
|
||||
currentSortOrder = newSortOrder;
|
||||
albumAdapter.sort(newSortOrder);
|
||||
Preferences.setAlbumSortOrder(newSortOrder);
|
||||
updateSortIndicator();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SearchView;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -145,7 +145,7 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.toolbar_menu, menu);
|
||||
inflater.inflate(R.menu.artist_list_menu, menu);
|
||||
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -27,16 +28,22 @@ import com.cappielloantonio.tempo.databinding.FragmentAlbumPageBinding;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.interfaces.ClickCallback;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -49,6 +56,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
private FragmentAlbumPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private AlbumPageViewModel albumPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@@ -71,6 +79,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentAlbumPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
@@ -88,6 +97,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,9 +121,26 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_rate_album) {
|
||||
Bundle bundle = new Bundle();
|
||||
AlbumID3 album = albumPageViewModel.getAlbum().getValue();
|
||||
bundle.putParcelable(Constants.ALBUM_OBJECT, (Parcelable) album);
|
||||
RatingDialog dialog = new RatingDialog();
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.action_download_album) {
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -144,8 +178,35 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.albumNameLabel.setText(album.getName());
|
||||
bind.albumArtistLabel.setText(album.getArtist());
|
||||
AssetLinkUtil.applyLinkAppearance(bind.albumArtistLabel);
|
||||
AssetLinkUtil.AssetLink artistLink = buildArtistLink(album);
|
||||
bind.albumArtistLabel.setOnLongClickListener(v -> {
|
||||
if (artistLink != null) {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), artistLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, artistLink.id), Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
|
||||
bind.albumReleaseYearLabel.setVisibility(album.getYear() != 0 ? View.VISIBLE : View.GONE);
|
||||
if (album.getYear() != 0) {
|
||||
bind.albumReleaseYearLabel.setVisibility(View.VISIBLE);
|
||||
AssetLinkUtil.applyLinkAppearance(bind.albumReleaseYearLabel);
|
||||
bind.albumReleaseYearLabel.setOnClickListener(v -> openYearLink(album.getYear()));
|
||||
bind.albumReleaseYearLabel.setOnLongClickListener(v -> {
|
||||
AssetLinkUtil.AssetLink yearLink = buildYearLink(album.getYear());
|
||||
if (yearLink != null) {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), yearLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, yearLink.id), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
bind.albumReleaseYearLabel.setVisibility(View.GONE);
|
||||
bind.albumReleaseYearLabel.setOnClickListener(null);
|
||||
bind.albumReleaseYearLabel.setOnLongClickListener(null);
|
||||
AssetLinkUtil.clearLinkAppearance(bind.albumReleaseYearLabel);
|
||||
}
|
||||
bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0));
|
||||
if (album.getGenre() != null && !album.getGenre().isEmpty()) {
|
||||
bind.albumGenresTextview.setText(album.getGenre());
|
||||
@@ -256,10 +317,15 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.songRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, false, false, album);
|
||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -282,4 +348,50 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||
public void onMediaLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
|
||||
private void openYearLink(int year) {
|
||||
AssetLinkUtil.AssetLink link = buildYearLink(year);
|
||||
if (link != null) {
|
||||
activity.openAssetLink(link);
|
||||
}
|
||||
}
|
||||
|
||||
private AssetLinkUtil.AssetLink buildYearLink(int year) {
|
||||
if (year <= 0) return null;
|
||||
return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year));
|
||||
}
|
||||
|
||||
private AssetLinkUtil.AssetLink buildArtistLink(AlbumID3 album) {
|
||||
if (album == null || album.getArtistId() == null || album.getArtistId().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, album.getArtistId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.SearchView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -24,6 +25,8 @@ import androidx.navigation.Navigation;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.FragmentArtistCatalogueBinding;
|
||||
import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration;
|
||||
@@ -32,6 +35,10 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@UnstableApi
|
||||
public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
@@ -125,23 +132,50 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
|
||||
searchView.setQueryHint(getString(R.string.filter_artist));
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
searchView.clearFocus();
|
||||
return false;
|
||||
// this toast may be overkill...
|
||||
Toast.makeText(requireContext(), "Search: " + query, Toast.LENGTH_SHORT).show();
|
||||
filterArtists(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
artistAdapter.getFilter().filter(newText);
|
||||
return false;
|
||||
filterArtists(newText);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
searchView.setPadding(-32, 0, 0, 0);
|
||||
}
|
||||
|
||||
private void filterArtists(String query) {
|
||||
List<ArtistID3> allArtists = artistCatalogueViewModel.getArtistList().getValue();
|
||||
|
||||
if (allArtists == null || allArtists.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (query == null || query.trim().isEmpty()) {
|
||||
artistAdapter.setItems(allArtists);
|
||||
} else {
|
||||
String searchQuery = query.toLowerCase().trim();
|
||||
List<ArtistID3> filteredArtists = new ArrayList<>();
|
||||
|
||||
for (ArtistID3 artist : allArtists) {
|
||||
if (artist.getName() != null &&
|
||||
artist.getName().toLowerCase().contains(searchQuery)) {
|
||||
filteredArtists.add(artist);
|
||||
}
|
||||
}
|
||||
artistAdapter.setItems(filteredArtists);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideKeyboard(View view) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
|
||||
@@ -29,19 +29,16 @@ import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@UnstableApi
|
||||
@@ -49,6 +46,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
private FragmentArtistPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private ArtistPageViewModel artistPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
private AlbumCatalogueAdapter albumCatalogueAdapter;
|
||||
@@ -63,6 +61,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentArtistPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
@@ -80,6 +79,13 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -159,7 +165,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.artistPageRadioButton.setOnClickListener(v -> {
|
||||
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (!songs.isEmpty()) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
} else {
|
||||
@@ -172,8 +178,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
private void initTopSongsView() {
|
||||
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, true, null);
|
||||
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (songs == null) {
|
||||
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
|
||||
@@ -183,6 +191,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
if (bind != null)
|
||||
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -273,4 +282,31 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
||||
public void onArtistLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -109,10 +111,14 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
||||
if (isVisible() && getActivity() != null) {
|
||||
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,11 +28,17 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -40,6 +46,7 @@ import java.util.Objects;
|
||||
@UnstableApi
|
||||
public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
private static final String TAG = "DownloadFragment";
|
||||
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
|
||||
|
||||
private FragmentDownloadBinding bind;
|
||||
private MainActivity activity;
|
||||
@@ -129,8 +136,27 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
});
|
||||
|
||||
downloadViewModel.getRefreshResult().observe(getViewLifecycleOwner(), count -> {
|
||||
if (count == null || bind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == -1) {
|
||||
Toast.makeText(requireContext(), R.string.download_refresh_no_directory, Toast.LENGTH_SHORT).show();
|
||||
} else if (count == 0) {
|
||||
Toast.makeText(requireContext(), R.string.download_refresh_no_changes, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getResources().getQuantityString(R.plurals.download_refresh_removed, count, count),
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
});
|
||||
|
||||
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
||||
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
||||
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
|
||||
}
|
||||
|
||||
private void finishDownloadView(List<Child> songs) {
|
||||
@@ -216,6 +242,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
||||
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
||||
return true;
|
||||
} else if (menuItem.getItemId() == R.id.menu_download_set_directory) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -267,4 +297,21 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
public void onDownloadGroupLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_PICK_DIRECTORY && resultCode == Activity.RESULT_OK) {
|
||||
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(), "Download directory set", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.cappielloantonio.tempo.ui.fragment
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||
import com.cappielloantonio.tempo.service.MediaService
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
|
||||
class EqualizerFragment : Fragment() {
|
||||
|
||||
private var equalizerManager: EqualizerManager? = null
|
||||
private lateinit var eqBandsContainer: LinearLayout
|
||||
private lateinit var eqSwitch: Switch
|
||||
private lateinit var resetButton: Button
|
||||
private lateinit var safeSpace: Space
|
||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as MediaService.LocalBinder
|
||||
equalizerManager = binder.getEqualizerManager()
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||
equalizerManager = null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Intent(requireContext(), MediaService::class.java).also { intent ->
|
||||
intent.action = MediaService.ACTION_BIND_EQUALIZER
|
||||
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
requireActivity().unbindService(connection)
|
||||
equalizerManager = null
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val root = inflater.inflate(R.layout.fragment_equalizer, container, false)
|
||||
eqSwitch = root.findViewById(R.id.equalizer_switch)
|
||||
eqSwitch.isChecked = Preferences.isEqualizerEnabled()
|
||||
eqSwitch.jumpDrawablesToCurrentState()
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
eqBandsContainer = view.findViewById(R.id.eq_bands_container)
|
||||
resetButton = view.findViewById(R.id.equalizer_reset_button)
|
||||
safeSpace = view.findViewById(R.id.equalizer_bottom_space)
|
||||
}
|
||||
|
||||
private fun initUI() {
|
||||
val manager = equalizerManager
|
||||
val notSupportedView = view?.findViewById<LinearLayout>(R.id.equalizer_not_supported_container)
|
||||
val switchRow = view?.findViewById<View>(R.id.equalizer_switch_row)
|
||||
|
||||
if (manager == null || manager.getNumberOfBands().toInt() == 0) {
|
||||
switchRow?.visibility = View.GONE
|
||||
resetButton.visibility = View.GONE
|
||||
eqBandsContainer.visibility = View.GONE
|
||||
safeSpace.visibility = View.GONE
|
||||
notSupportedView?.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
|
||||
notSupportedView?.visibility = View.GONE
|
||||
switchRow?.visibility = View.VISIBLE
|
||||
resetButton.visibility = View.VISIBLE
|
||||
eqBandsContainer.visibility = View.VISIBLE
|
||||
safeSpace.visibility = View.VISIBLE
|
||||
|
||||
eqSwitch.setOnCheckedChangeListener(null)
|
||||
updateUiEnabledState(eqSwitch.isChecked)
|
||||
eqSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
manager.setEnabled(isChecked)
|
||||
Preferences.setEqualizerEnabled(isChecked)
|
||||
updateUiEnabledState(isChecked)
|
||||
}
|
||||
|
||||
createBandSliders()
|
||||
|
||||
resetButton.setOnClickListener {
|
||||
resetEqualizer()
|
||||
saveBandLevelsToPreferences()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUiEnabledState(isEnabled: Boolean) {
|
||||
resetButton.isEnabled = isEnabled
|
||||
bandSeekBars.forEach { it.isEnabled = isEnabled }
|
||||
}
|
||||
|
||||
private fun formatDb(value: Int): String = if (value > 0) "+$value dB" else "$value dB"
|
||||
|
||||
private fun createBandSliders() {
|
||||
val manager = equalizerManager ?: return
|
||||
eqBandsContainer.removeAllViews()
|
||||
bandSeekBars.clear()
|
||||
val bands = manager.getNumberOfBands()
|
||||
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
|
||||
val minLevelDb = bandLevelRange[0] / 100
|
||||
val maxLevelDb = bandLevelRange[1] / 100
|
||||
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
val band = i.toShort()
|
||||
val freq = manager.getCenterFreq(band) ?: 0
|
||||
|
||||
val row = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
val topBottomMarginDp = 16
|
||||
topMargin = topBottomMarginDp.dpToPx(context)
|
||||
bottomMargin = topBottomMarginDp.dpToPx(context)
|
||||
}
|
||||
setPadding(0, 8, 0, 8)
|
||||
}
|
||||
|
||||
val freqLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply {
|
||||
text = if (freq >= 1000) {
|
||||
if (freq % 1000 == 0) {
|
||||
"${freq / 1000} kHz"
|
||||
} else {
|
||||
String.format("%.1f kHz", freq / 1000f)
|
||||
}
|
||||
} else {
|
||||
"$freq Hz"
|
||||
}
|
||||
gravity = Gravity.START
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
|
||||
}
|
||||
row.addView(freqLabel)
|
||||
|
||||
val initialLevelDb = (savedLevels.getOrNull(i) ?: (manager.getBandLevel(band) ?: 0)) / 100
|
||||
val dbLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply {
|
||||
text = formatDb(initialLevelDb)
|
||||
setPadding(12, 0, 0, 0)
|
||||
gravity = Gravity.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
|
||||
}
|
||||
|
||||
val seekBar = SeekBar(requireContext()).apply {
|
||||
max = maxLevelDb - minLevelDb
|
||||
progress = initialLevelDb - minLevelDb
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 6f)
|
||||
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
val thisLevelDb = progress + minLevelDb
|
||||
if (fromUser) {
|
||||
manager.setBandLevel(band, (thisLevelDb * 100).toShort())
|
||||
saveBandLevelsToPreferences()
|
||||
}
|
||||
dbLabel.text = formatDb(thisLevelDb)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
}
|
||||
bandSeekBars.add(seekBar)
|
||||
row.addView(seekBar)
|
||||
row.addView(dbLabel)
|
||||
eqBandsContainer.addView(row)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetEqualizer() {
|
||||
val manager = equalizerManager ?: return
|
||||
val bands = manager.getNumberOfBands()
|
||||
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
|
||||
val minLevelDb = bandLevelRange[0] / 100
|
||||
val midLevelDb = 0
|
||||
|
||||
for (i in 0 until bands) {
|
||||
manager.setBandLevel(i.toShort(), (0).toShort())
|
||||
bandSeekBars.getOrNull(i)?.progress = midLevelDb - minLevelDb
|
||||
}
|
||||
Preferences.setEqualizerBandLevels(ShortArray(bands.toInt()))
|
||||
}
|
||||
|
||||
private fun saveBandLevelsToPreferences() {
|
||||
val manager = equalizerManager ?: return
|
||||
val bands = manager.getNumberOfBands()
|
||||
val levels = ShortArray(bands.toInt()) { i -> manager.getBandLevel(i.toShort()) ?: 0 }
|
||||
Preferences.setEqualizerBandLevels(levels)
|
||||
}
|
||||
|
||||
private fun restoreEqualizerPreferences() {
|
||||
val manager = equalizerManager ?: return
|
||||
eqSwitch.isChecked = Preferences.isEqualizerEnabled()
|
||||
updateUiEnabledState(eqSwitch.isChecked)
|
||||
|
||||
val bands = manager.getNumberOfBands()
|
||||
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
|
||||
val minLevelDb = bandLevelRange[0] / 100
|
||||
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
val savedDb = savedLevels[i] / 100
|
||||
manager.setBandLevel(i.toShort(), (savedDb * 100).toShort())
|
||||
bandSeekBars.getOrNull(i)?.progress = savedDb - minLevelDb
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(context: Context): Int =
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
@@ -9,6 +9,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -39,6 +40,8 @@ import com.cappielloantonio.tempo.service.MediaManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
||||
@@ -59,9 +62,12 @@ import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.UIUtil;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -73,6 +79,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
private FragmentHomeTabMusicBinding bind;
|
||||
private MainActivity activity;
|
||||
private HomeViewModel homeViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private DiscoverSongAdapter discoverSongAdapter;
|
||||
private SimilarTrackAdapter similarMusicAdapter;
|
||||
@@ -100,6 +107,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
|
||||
@@ -111,6 +119,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
initSyncStarredView();
|
||||
initSyncStarredAlbumsView();
|
||||
initSyncStarredArtistsView();
|
||||
initDiscoverSongSlideView();
|
||||
initSimilarSongView();
|
||||
initArtistRadio();
|
||||
@@ -136,12 +146,18 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observeStarredSongsPlayback();
|
||||
observeTopSongsPlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
refreshSharesView();
|
||||
if (topSongAdapter != null) setTopSongsMediaBrowserListenableFuture();
|
||||
if (starredSongAdapter != null) setStarredSongsMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -263,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
|
||||
private void initSyncStarredView() {
|
||||
if (Preferences.isStarredSyncEnabled()) {
|
||||
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
@@ -314,6 +330,174 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
});
|
||||
}
|
||||
|
||||
private void initSyncStarredAlbumsView() {
|
||||
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<AlbumID3>>() {
|
||||
@Override
|
||||
public void onChanged(List<AlbumID3> albums) {
|
||||
if (albums != null && !albums.isEmpty()) {
|
||||
checkIfAlbumsNeedSync(albums);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind.homeSyncStarredAlbumsCancel.setOnClickListener(v -> {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
||||
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void checkIfAlbumsNeedSync(List<AlbumID3> albums) {
|
||||
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
List<String> albumsNeedingSync = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
boolean albumNeedsSync = false;
|
||||
// Check if any songs from this album need downloading
|
||||
for (Child song : allSongs) {
|
||||
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
albumNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (albumNeedsSync) {
|
||||
albumsNeedingSync.add(album.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_albums_count,
|
||||
albumsNeedingSync.size(),
|
||||
albumsNeedingSync.size()
|
||||
);
|
||||
bind.homeSyncStarredAlbumsToSync.setText(message);
|
||||
} else {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initSyncStarredArtistsView() {
|
||||
if (Preferences.isStarredArtistsSyncEnabled()) {
|
||||
homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<ArtistID3>>() {
|
||||
@Override
|
||||
public void onChanged(List<ArtistID3> artists) {
|
||||
if (artists != null && !artists.isEmpty()) {
|
||||
checkIfArtistsNeedSync(artists);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind.homeSyncStarredArtistsCancel.setOnClickListener(v -> {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
bind.homeSyncStarredArtistsDownload.setOnClickListener(v -> {
|
||||
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void checkIfArtistsNeedSync(List<ArtistID3> artists) {
|
||||
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
List<String> artistsNeedingSync = new ArrayList<>();
|
||||
|
||||
for (ArtistID3 artist : artists) {
|
||||
boolean artistNeedsSync = false;
|
||||
// Check if any songs from this artist need downloading
|
||||
for (Child song : allSongs) {
|
||||
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
artistNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (artistNeedsSync) {
|
||||
artistsNeedingSync.add(artist.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_artists_count,
|
||||
artistsNeedingSync.size(),
|
||||
artistsNeedingSync.size()
|
||||
);
|
||||
bind.homeSyncStarredArtistsToSync.setText(message);
|
||||
} else {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initDiscoverSongSlideView() {
|
||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||
|
||||
@@ -416,8 +600,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.topSongsRecyclerView.setHasFixedSize(true);
|
||||
|
||||
topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
topSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
|
||||
setTopSongsMediaBrowserListenableFuture();
|
||||
reapplyTopSongsPlayback();
|
||||
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
|
||||
if (chronologies == null || chronologies.isEmpty()) {
|
||||
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
|
||||
@@ -433,6 +619,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
topSongAdapter.setItems(topSongs);
|
||||
reapplyTopSongsPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -454,8 +641,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
|
||||
bind.starredTracksRecyclerView.setHasFixedSize(true);
|
||||
|
||||
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
starredSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
|
||||
setStarredSongsMediaBrowserListenableFuture();
|
||||
reapplyStarredSongsPlayback();
|
||||
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
||||
if (songs == null) {
|
||||
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE);
|
||||
@@ -466,6 +655,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
|
||||
|
||||
starredSongAdapter.setItems(songs);
|
||||
reapplyStarredSongsPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -895,6 +1085,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||
activity.setBottomSheetInPeek(true);
|
||||
}
|
||||
topSongAdapter.notifyDataSetChanged();
|
||||
starredSongAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -984,4 +1176,58 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||
public void onShareLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observeStarredSongsPlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (starredSongAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
starredSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (starredSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
starredSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void observeTopSongsPlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (topSongAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
topSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (topSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
topSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyStarredSongsPlayback() {
|
||||
if (starredSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
starredSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void reapplyTopSongsPlayback() {
|
||||
if (topSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
topSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTopSongsMediaBrowserListenableFuture() {
|
||||
topSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
|
||||
private void setStarredSongsMediaBrowserListenableFuture() {
|
||||
starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,12 @@ public class PlayerBottomSheetFragment extends Fragment {
|
||||
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
|
||||
|
||||
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(mediaMetadata.extras.getString("artist"));
|
||||
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? mediaMetadata.artist
|
||||
: Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO)
|
||||
? mediaMetadata.extras.getString("uri", getString(R.string.label_placeholder))
|
||||
: "");
|
||||
|
||||
CustomGlideRequest.Builder
|
||||
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
|
||||
@@ -182,10 +187,15 @@ public class PlayerBottomSheetFragment extends Fragment {
|
||||
.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"), "") ? 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
||||
if (mediaBrowser.getMediaMetadata().extras != null) {
|
||||
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RatingBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.ToggleButton;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
@@ -22,21 +29,27 @@ import androidx.media3.common.util.RepeatModeUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavOptions;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
|
||||
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.RatingDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
|
||||
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.android.material.elevation.SurfaceColors;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
@@ -53,6 +66,8 @@ public class PlayerControllerFragment extends Fragment {
|
||||
private InnerFragmentPlayerControllerBinding bind;
|
||||
private ViewPager2 playerMediaCoverViewPager;
|
||||
private ToggleButton buttonFavorite;
|
||||
private RatingViewModel ratingViewModel;
|
||||
private RatingBar songRatingBar;
|
||||
private TextView playerMediaTitleLabel;
|
||||
private TextView playerArtistNameLabel;
|
||||
private Button playbackSpeedButton;
|
||||
@@ -62,11 +77,20 @@ public class PlayerControllerFragment extends Fragment {
|
||||
private ConstraintLayout playerQuickActionView;
|
||||
private ImageButton playerOpenQueueButton;
|
||||
private ImageButton playerTrackInfo;
|
||||
private LinearLayout ratingContainer;
|
||||
private ImageButton equalizerButton;
|
||||
private ChipGroup assetLinkChipGroup;
|
||||
private Chip playerSongLinkChip;
|
||||
private Chip playerAlbumLinkChip;
|
||||
private Chip playerArtistLinkChip;
|
||||
|
||||
private MainActivity activity;
|
||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private MediaService.LocalBinder mediaServiceBinder;
|
||||
private boolean isServiceBound = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
activity = (MainActivity) getActivity();
|
||||
@@ -75,6 +99,7 @@ public class PlayerControllerFragment extends Fragment {
|
||||
View view = bind.getRoot();
|
||||
|
||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||
ratingViewModel = new ViewModelProvider(requireActivity()).get(RatingViewModel.class);
|
||||
|
||||
init();
|
||||
initQuickActionView();
|
||||
@@ -82,6 +107,7 @@ public class PlayerControllerFragment extends Fragment {
|
||||
initMediaListenable();
|
||||
initMediaLabelButton();
|
||||
initArtistLabelButton();
|
||||
initEqualizerButton();
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -117,6 +143,14 @@ public class PlayerControllerFragment extends Fragment {
|
||||
playerQuickActionView = bind.getRoot().findViewById(R.id.player_quick_action_view);
|
||||
playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button);
|
||||
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
|
||||
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
|
||||
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
|
||||
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
|
||||
assetLinkChipGroup = bind.getRoot().findViewById(R.id.asset_link_chip_group);
|
||||
playerSongLinkChip = bind.getRoot().findViewById(R.id.asset_link_song_chip);
|
||||
playerAlbumLinkChip = bind.getRoot().findViewById(R.id.asset_link_album_chip);
|
||||
playerArtistLinkChip = bind.getRoot().findViewById(R.id.asset_link_artist_chip);
|
||||
checkAndSetRatingContainerVisibility();
|
||||
}
|
||||
|
||||
private void initQuickActionView() {
|
||||
@@ -146,7 +180,6 @@ public class PlayerControllerFragment extends Fragment {
|
||||
bind.nowPlayingMediaControllerView.setPlayer(mediaBrowser);
|
||||
mediaBrowser.setShuffleModeEnabled(Preferences.isShuffleModeEnabled());
|
||||
mediaBrowser.setRepeatMode(Preferences.getRepeatMode());
|
||||
|
||||
setMediaControllerListener(mediaBrowser);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
@@ -181,18 +214,29 @@ public class PlayerControllerFragment extends Fragment {
|
||||
|
||||
private void setMetadata(MediaMetadata mediaMetadata) {
|
||||
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
|
||||
playerArtistNameLabel.setText(String.valueOf(mediaMetadata.artist));
|
||||
playerArtistNameLabel.setText(
|
||||
mediaMetadata.artist != null
|
||||
? 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);
|
||||
playerArtistNameLabel.setSelected(true);
|
||||
|
||||
playerMediaTitleLabel.setVisibility(mediaMetadata.title != null && !Objects.equals(mediaMetadata.title, "") ? View.VISIBLE : View.GONE);
|
||||
playerArtistNameLabel.setVisibility(mediaMetadata.artist != null && !Objects.equals(mediaMetadata.artist, "") ? View.VISIBLE : View.GONE);
|
||||
playerArtistNameLabel.setVisibility(
|
||||
(mediaMetadata.artist != null && !Objects.equals(mediaMetadata.artist, ""))
|
||||
|| mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
|
||||
updateAssetLinkChips(mediaMetadata);
|
||||
}
|
||||
|
||||
private void setMediaInfo(MediaMetadata mediaMetadata) {
|
||||
if (mediaMetadata.extras != null) {
|
||||
String extension = mediaMetadata.extras.getString("suffix", "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";
|
||||
String samplingRate = mediaMetadata.extras.getInt("samplingRate", 0) != 0 ? new 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" : "";
|
||||
@@ -218,8 +262,8 @@ public class PlayerControllerFragment extends Fragment {
|
||||
boolean isTranscodingBitrate = !MusicUtil.getBitratePreference().equals("0");
|
||||
|
||||
if (isTranscodingExtension || isTranscodingBitrate) {
|
||||
playerMediaExtension.setText("Transcoding");
|
||||
playerMediaBitrate.setText("requested");
|
||||
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 -> {
|
||||
@@ -228,6 +272,110 @@ public class PlayerControllerFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
|
||||
if (assetLinkChipGroup == null) return;
|
||||
String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC;
|
||||
if (!Constants.MEDIA_TYPE_MUSIC.equals(mediaType)) {
|
||||
clearAssetLinkChip(playerSongLinkChip);
|
||||
clearAssetLinkChip(playerAlbumLinkChip);
|
||||
clearAssetLinkChip(playerArtistLinkChip);
|
||||
syncAssetLinkGroupVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
String songId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("id") : null;
|
||||
String albumId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("albumId") : null;
|
||||
String artistId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("artistId") : null;
|
||||
|
||||
AssetLinkUtil.AssetLink songLink = bindAssetLinkChip(playerSongLinkChip, AssetLinkUtil.TYPE_SONG, songId);
|
||||
AssetLinkUtil.AssetLink albumLink = bindAssetLinkChip(playerAlbumLinkChip, AssetLinkUtil.TYPE_ALBUM, albumId);
|
||||
AssetLinkUtil.AssetLink artistLink = bindAssetLinkChip(playerArtistLinkChip, AssetLinkUtil.TYPE_ARTIST, artistId);
|
||||
bindAssetLinkView(playerMediaTitleLabel, songLink);
|
||||
bindAssetLinkView(playerArtistNameLabel, artistLink != null ? artistLink : songLink);
|
||||
bindAssetLinkView(playerMediaCoverViewPager, songLink);
|
||||
syncAssetLinkGroupVisibility();
|
||||
}
|
||||
|
||||
private AssetLinkUtil.AssetLink bindAssetLinkChip(Chip chip, String type, String id) {
|
||||
if (chip == null) return null;
|
||||
if (TextUtils.isEmpty(id)) {
|
||||
clearAssetLinkChip(chip);
|
||||
return null;
|
||||
}
|
||||
|
||||
String label = getString(AssetLinkUtil.getLabelRes(type));
|
||||
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
|
||||
if (assetLink == null) {
|
||||
clearAssetLinkChip(chip);
|
||||
return null;
|
||||
}
|
||||
|
||||
chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
|
||||
chip.setVisibility(View.VISIBLE);
|
||||
|
||||
chip.setOnClickListener(v -> {
|
||||
if (assetLink != null) {
|
||||
activity.openAssetLink(assetLink);
|
||||
}
|
||||
});
|
||||
|
||||
chip.setOnLongClickListener(v -> {
|
||||
if (assetLink != null) {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return assetLink;
|
||||
}
|
||||
|
||||
private void clearAssetLinkChip(Chip chip) {
|
||||
if (chip == null) return;
|
||||
chip.setVisibility(View.GONE);
|
||||
chip.setText("");
|
||||
chip.setOnClickListener(null);
|
||||
chip.setOnLongClickListener(null);
|
||||
}
|
||||
|
||||
private void bindAssetLinkView(View view, AssetLinkUtil.AssetLink assetLink) {
|
||||
if (view == null) return;
|
||||
if (assetLink == null) {
|
||||
AssetLinkUtil.clearLinkAppearance(view);
|
||||
view.setOnClickListener(null);
|
||||
view.setOnLongClickListener(null);
|
||||
view.setClickable(false);
|
||||
view.setLongClickable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setClickable(true);
|
||||
view.setLongClickable(true);
|
||||
AssetLinkUtil.applyLinkAppearance(view);
|
||||
view.setOnClickListener(v -> {
|
||||
boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
|
||||
activity.openAssetLink(assetLink, collapse);
|
||||
});
|
||||
view.setOnLongClickListener(v -> {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void syncAssetLinkGroupVisibility() {
|
||||
if (assetLinkChipGroup == null) return;
|
||||
boolean hasVisible = false;
|
||||
for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
|
||||
View child = assetLinkChipGroup.getChildAt(i);
|
||||
if (child.getVisibility() == View.VISIBLE) {
|
||||
hasVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
|
||||
initPlaybackSpeedButton(mediaBrowser);
|
||||
|
||||
@@ -305,6 +453,7 @@ public class PlayerControllerFragment extends Fragment {
|
||||
private void initMediaListenable() {
|
||||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), media -> {
|
||||
if (media != null) {
|
||||
ratingViewModel.setSong(media);
|
||||
buttonFavorite.setChecked(media.getStarred() != null);
|
||||
buttonFavorite.setOnClickListener(v -> playerBottomSheetViewModel.setFavorite(requireContext(), media));
|
||||
buttonFavorite.setOnLongClickListener(v -> {
|
||||
@@ -315,9 +464,29 @@ public class PlayerControllerFragment extends Fragment {
|
||||
dialog.setArguments(bundle);
|
||||
dialog.show(requireActivity().getSupportFragmentManager(), null);
|
||||
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
Integer currentRating = media.getUserRating();
|
||||
|
||||
if (currentRating != null) {
|
||||
songRatingBar.setRating(currentRating);
|
||||
} else {
|
||||
songRatingBar.setRating(0);
|
||||
}
|
||||
|
||||
songRatingBar.setOnRatingBarChangeListener(new RatingBar.OnRatingBarChangeListener() {
|
||||
@Override
|
||||
public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
ratingViewModel.rate((int) rating);
|
||||
media.setUserRating((int) rating);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (getActivity() != null) {
|
||||
playerBottomSheetViewModel.refreshMediaInfo(requireActivity(), media);
|
||||
}
|
||||
@@ -387,6 +556,18 @@ public class PlayerControllerFragment extends Fragment {
|
||||
});
|
||||
}
|
||||
|
||||
private void initEqualizerButton() {
|
||||
equalizerButton.setOnClickListener(v -> {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
NavOptions navOptions = new NavOptions.Builder()
|
||||
.setLaunchSingleTop(true)
|
||||
.setPopUpTo(R.id.equalizerFragment, true)
|
||||
.build();
|
||||
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
||||
if (activity != null) activity.collapseBottomSheetDelayed();
|
||||
});
|
||||
}
|
||||
|
||||
public void goToControllerPage() {
|
||||
playerMediaCoverViewPager.setCurrentItem(0, false);
|
||||
}
|
||||
@@ -395,6 +576,17 @@ public class PlayerControllerFragment extends Fragment {
|
||||
playerMediaCoverViewPager.setCurrentItem(1, true);
|
||||
}
|
||||
|
||||
private void checkAndSetRatingContainerVisibility() {
|
||||
if (ratingContainer == null) return;
|
||||
|
||||
if (Preferences.showItemStarRating()) {
|
||||
ratingContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
ratingContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPlaybackParameters(MediaBrowser mediaBrowser) {
|
||||
Button playbackSpeedButton = bind.getRoot().findViewById(R.id.player_playback_speed_button);
|
||||
float currentSpeed = Preferences.getPlaybackSpeed();
|
||||
@@ -411,4 +603,66 @@ public class PlayerControllerFragment extends Fragment {
|
||||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
||||
// TODO Resettare lo skip del silenzio
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if (equalizerButton != null) {
|
||||
if (numBands == 0) {
|
||||
equalizerButton.setVisibility(View.GONE);
|
||||
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams();
|
||||
params.startToEnd = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
||||
playerOpenQueueButton.setLayoutParams(params);
|
||||
} else {
|
||||
equalizerButton.setVisibility(View.VISIBLE);
|
||||
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams();
|
||||
params.startToStart = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.startToEnd = R.id.player_open_equalizer_button;
|
||||
playerOpenQueueButton.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
bindMediaService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (isServiceBound) {
|
||||
requireActivity().unbindService(serviceConnection);
|
||||
isServiceBound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,19 @@ import android.transition.TransitionManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.Player;
|
||||
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.databinding.InnerFragmentPlayerCoverBinding;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
@@ -29,7 +32,9 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
@@ -112,15 +117,21 @@ public class PlayerCoverFragment extends Fragment {
|
||||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
||||
if (song != null && bind != null) {
|
||||
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
} else {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
}
|
||||
});
|
||||
|
||||
bind.innerButtonTopRight.setOnClickListener(view -> {
|
||||
ArrayList<Child> tracks = new ArrayList<>();
|
||||
tracks.add(song);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(Constants.TRACK_OBJECT, song);
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, tracks);
|
||||
|
||||
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
|
||||
dialog.setArguments(bundle);
|
||||
@@ -136,7 +147,7 @@ public class PlayerCoverFragment extends Fragment {
|
||||
|
||||
bind.innerButtonBottomRight.setOnClickListener(view -> {
|
||||
if (playerBottomSheetViewModel.savePlayQueue()) {
|
||||
Snackbar.make(requireView(), "Salvato", Snackbar.LENGTH_LONG).show();
|
||||
Snackbar.make(requireView(), R.string.player_queue_save_queue_success, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Layout;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Line;
|
||||
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.List;
|
||||
@@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
private MediaBrowser mediaBrowser;
|
||||
private Handler syncLyricsHandler;
|
||||
private Runnable syncLyricsRunnable;
|
||||
private String currentLyrics;
|
||||
private LyricsList currentLyricsList;
|
||||
private String currentDescription;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
@@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
initPanelContent();
|
||||
observeDownloadState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
bind = null;
|
||||
currentLyrics = null;
|
||||
currentLyricsList = null;
|
||||
currentDescription = null;
|
||||
}
|
||||
|
||||
private void initOverlay() {
|
||||
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
||||
playerBottomSheetViewModel.changeSyncLyricsState();
|
||||
});
|
||||
|
||||
bind.downloadLyricsButton.setOnClickListener(view -> {
|
||||
boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics();
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure,
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeBrowser() {
|
||||
@@ -136,50 +155,91 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void initPanelContent() {
|
||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
setPanelContent(null, lyricsList);
|
||||
});
|
||||
} else {
|
||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||
setPanelContent(lyrics, null);
|
||||
});
|
||||
}
|
||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||
currentLyrics = lyrics;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
currentLyricsList = lyricsList;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||
currentDescription = description;
|
||||
updatePanelContent();
|
||||
});
|
||||
}
|
||||
|
||||
private void setPanelContent(String lyrics, LyricsList lyricsList) {
|
||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||
private void observeDownloadState() {
|
||||
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
|
||||
if (bind != null) {
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||
|
||||
if (lyrics != null && !lyrics.trim().equals("")) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
|
||||
setSyncLirics(lyricsList);
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||
} else if (description != null && !description.trim().equals("")) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
|
||||
if (cached != null && cached) {
|
||||
downloadButton.setIconResource(R.drawable.ic_done);
|
||||
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
|
||||
} else {
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
downloadButton.setIconResource(R.drawable.ic_download);
|
||||
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePanelContent() {
|
||||
if (bind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||
|
||||
if (hasStructuredLyrics(currentLyricsList)) {
|
||||
setSyncLirics(currentLyricsList);
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setEnabled(true);
|
||||
} else if (hasText(currentLyrics)) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setEnabled(true);
|
||||
} else if (hasText(currentDescription)) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setEnabled(false);
|
||||
} else {
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||
return lyricsList != null
|
||||
&& lyricsList.getStructuredLyrics() != null
|
||||
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void setSyncLirics(LyricsList lyricsList) {
|
||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||
@@ -198,28 +258,28 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
|
||||
private void defineProgressHandler() {
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
if (lyricsList != null) {
|
||||
|
||||
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
syncLyricsHandler = new Handler();
|
||||
syncLyricsRunnable = () -> {
|
||||
if (syncLyricsHandler != null) {
|
||||
if (bind != null) {
|
||||
displaySyncedLyrics();
|
||||
}
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
}
|
||||
};
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
} else {
|
||||
if (!hasStructuredLyrics(lyricsList)) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
syncLyricsHandler = new Handler();
|
||||
syncLyricsRunnable = () -> {
|
||||
if (syncLyricsHandler != null) {
|
||||
if (bind != null) {
|
||||
displaySyncedLyrics();
|
||||
}
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
}
|
||||
};
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
|
||||
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||
|
||||
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||
if (hasStructuredLyrics(lyricsList)) {
|
||||
StringBuilder lyricsBuilder = new StringBuilder();
|
||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
@@ -38,6 +39,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
private InnerFragmentPlayerQueueBinding bind;
|
||||
|
||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private PlayerSongQueueAdapter playerSongQueueAdapter;
|
||||
@@ -48,6 +50,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
View view = bind.getRoot();
|
||||
|
||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
initQueueRecyclerView();
|
||||
|
||||
@@ -59,6 +62,9 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
super.onStart();
|
||||
initializeBrowser();
|
||||
bindMediaController();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,9 +116,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
|
||||
playerSongQueueAdapter = new PlayerSongQueueAdapter(this);
|
||||
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
|
||||
reapplyPlayback();
|
||||
|
||||
playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> {
|
||||
if (queue != null) {
|
||||
playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList()));
|
||||
reapplyPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -216,4 +225,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
||||
public void onMediaClick(Bundle bundle) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,9 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -49,6 +52,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
private FragmentPlaylistPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private PlaylistPageViewModel playlistPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
|
||||
@@ -94,6 +98,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentPlaylistPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
@@ -109,6 +114,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,7 +142,8 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
if (item.getItemId() == R.id.action_download_playlist) {
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (isVisible() && getActivity() != null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(child -> {
|
||||
Download toDownload = new Download(child);
|
||||
@@ -136,7 +151,10 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
||||
return toDownload;
|
||||
}).collect(Collectors.toList())
|
||||
);
|
||||
);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
@@ -246,10 +264,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.songRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
@@ -270,4 +293,31 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||
public void onMediaLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,11 @@ import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -34,6 +31,7 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -46,6 +44,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
private FragmentSearchBinding bind;
|
||||
private MainActivity activity;
|
||||
private SearchViewModel searchViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private ArtistAdapter artistAdapter;
|
||||
private AlbumAdapter albumAdapter;
|
||||
@@ -61,6 +60,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentSearchBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
initSearchResultView();
|
||||
initSearchView();
|
||||
@@ -73,6 +73,15 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,7 +121,10 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
|
||||
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
}
|
||||
|
||||
@@ -242,7 +254,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
}
|
||||
|
||||
private boolean isQueryValid(String query) {
|
||||
return !query.equals("") && query.trim().length() > 2;
|
||||
return !query.equals("") && query.trim().length() > 1;
|
||||
}
|
||||
|
||||
private void inputFocus() {
|
||||
@@ -260,6 +272,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
@Override
|
||||
public void onMediaClick(Bundle bundle) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||
songHorizontalAdapter.notifyDataSetChanged();
|
||||
activity.setBottomSheetInPeek(true);
|
||||
}
|
||||
|
||||
@@ -287,4 +300,31 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
||||
public void onArtistLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
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.Handler;
|
||||
import android.os.IBinder;
|
||||
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;
|
||||
@@ -18,23 +24,32 @@ 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.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.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.UIUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -47,15 +62,41 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private MainActivity activity;
|
||||
private SettingViewModel settingViewModel;
|
||||
|
||||
private ActivityResultLauncher<Intent> someActivityResultLauncher;
|
||||
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||
|
||||
private MediaService.LocalBinder mediaServiceBinder;
|
||||
private boolean isServiceBound = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
someActivityResultLauncher = registerForActivityResult(
|
||||
equalizerResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {}
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,9 +125,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
checkEqualizer();
|
||||
checkSystemEqualizer();
|
||||
checkCacheStorage();
|
||||
checkStorage();
|
||||
checkDownloadDirectory();
|
||||
|
||||
setStreamingCacheSize();
|
||||
setAppLanguage();
|
||||
@@ -94,11 +136,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
actionLogout();
|
||||
actionScan();
|
||||
actionSyncStarredAlbums();
|
||||
actionSyncStarredTracks();
|
||||
actionSyncStarredArtists();
|
||||
actionChangeStreamingCacheStorage();
|
||||
actionChangeDownloadStorage();
|
||||
actionSetDownloadDirectory();
|
||||
actionDeleteDownloadStorage();
|
||||
actionKeepScreenOn();
|
||||
actionAutoDownloadLyrics();
|
||||
actionMiniPlayerHeart();
|
||||
|
||||
bindMediaService();
|
||||
actionAppEqualizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,8 +171,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkEqualizer() {
|
||||
Preference equalizer = findPreference("equalizer");
|
||||
private void checkSystemEqualizer() {
|
||||
Preference equalizer = findPreference("system_equalizer");
|
||||
|
||||
if (equalizer == null) return;
|
||||
|
||||
@@ -130,7 +180,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||
equalizer.setOnPreferenceClickListener(preference -> {
|
||||
someActivityResultLauncher.launch(intent);
|
||||
equalizerResultLauncher.launch(intent);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
@@ -147,7 +197,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||
storage.setVisible(false);
|
||||
} else {
|
||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
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);
|
||||
@@ -163,13 +213,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||
storage.setVisible(false);
|
||||
} else {
|
||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
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");
|
||||
|
||||
@@ -242,7 +325,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onSuccess(boolean isScanning, long count) {
|
||||
findPreference("scan_library").setSummary("Scanning: counting " + count + " tracks");
|
||||
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||
if (isScanning) getScanStatus();
|
||||
}
|
||||
});
|
||||
@@ -255,7 +338,37 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (newValue instanceof Boolean) {
|
||||
if ((Boolean) newValue) {
|
||||
StarredSyncDialog dialog = new StarredSyncDialog();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -287,11 +400,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
@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);
|
||||
@@ -299,6 +420,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -307,6 +453,36 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
@@ -316,7 +492,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onSuccess(boolean isScanning, long count) {
|
||||
findPreference("scan_library").setSummary("Scanning: counting " + count + " tracks");
|
||||
findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count));
|
||||
if (isScanning) getScanStatus();
|
||||
}
|
||||
});
|
||||
@@ -334,4 +510,63 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -49,6 +50,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||
private FragmentSongListPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private SongListPageViewModel songListPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
|
||||
@@ -69,6 +71,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||
bind = FragmentSongListPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
@@ -82,6 +85,15 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -189,11 +201,14 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.songListRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
|
||||
isLoading = false;
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
setSongListPageSubtitle(songs);
|
||||
});
|
||||
|
||||
@@ -325,4 +340,31 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
||||
public void onMediaLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
@@ -37,6 +38,8 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
@@ -54,6 +57,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
|
||||
private AlbumID3 album;
|
||||
|
||||
private TextView removeAllTextView;
|
||||
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@Nullable
|
||||
@@ -72,6 +79,12 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateRemoveAllVisibility);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
@@ -102,7 +115,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
albumBottomSheetViewModel.setFavorite();
|
||||
albumBottomSheetViewModel.setFavorite(requireContext());
|
||||
});
|
||||
|
||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
@@ -163,7 +176,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
|
||||
downloadAll.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
});
|
||||
@@ -182,19 +199,23 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
});
|
||||
});
|
||||
|
||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
currentAlbumTracks = songs != null ? songs : Collections.emptyList();
|
||||
currentAlbumMediaItems = MappingUtil.mapDownloads(currentAlbumTracks);
|
||||
|
||||
removeAll.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||
removeAllTextView.setOnClickListener(v -> {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<Download> downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads);
|
||||
} else {
|
||||
currentAlbumTracks.forEach(ExternalAudioReader::delete);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
updateRemoveAllVisibility();
|
||||
});
|
||||
|
||||
initDownloadUI(removeAll);
|
||||
|
||||
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
|
||||
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
|
||||
if (artist != null) {
|
||||
@@ -234,14 +255,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private void initDownloadUI(TextView removeAll) {
|
||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
private void updateRemoveAllVisibility() {
|
||||
if (removeAllTextView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||
removeAll.setVisibility(View.VISIBLE);
|
||||
if (currentAlbumTracks == null || currentAlbumTracks.isEmpty()) {
|
||||
removeAllTextView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<MediaItem> mediaItems = currentAlbumMediaItems;
|
||||
if (mediaItems == null || mediaItems.isEmpty()) {
|
||||
removeAllTextView.setVisibility(View.GONE);
|
||||
} else if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||
removeAllTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
removeAllTextView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
boolean hasLocal = currentAlbumTracks.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null);
|
||||
removeAllTextView.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
|
||||
@@ -66,7 +66,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
// TODO Utilizzare il viewmodel come tramite ed evitare le chiamate dirette
|
||||
// TODO Use the viewmodel as a conduit and avoid direct calls
|
||||
private void init(View view) {
|
||||
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
|
||||
CustomGlideRequest.Builder
|
||||
@@ -81,7 +81,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
artistBottomSheetViewModel.setFavorite();
|
||||
artistBottomSheetViewModel.setFavorite(requireContext());
|
||||
});
|
||||
|
||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
|
||||
@@ -25,6 +25,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
@@ -117,10 +119,13 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
|
||||
|
||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||
removeAll.setOnClickListener(v -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||
} else {
|
||||
songs.forEach(ExternalAudioReader::delete);
|
||||
}
|
||||
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
@@ -29,16 +30,24 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||
import com.cappielloantonio.tempo.util.AssetLinkUtil;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
@@ -48,6 +57,16 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private SongBottomSheetViewModel songBottomSheetViewModel;
|
||||
private Child song;
|
||||
|
||||
private TextView downloadButton;
|
||||
private TextView removeButton;
|
||||
private ChipGroup assetLinkChipGroup;
|
||||
private Chip songLinkChip;
|
||||
private Chip albumLinkChip;
|
||||
private Chip artistLinkChip;
|
||||
private AssetLinkUtil.AssetLink currentSongLink;
|
||||
private AssetLinkUtil.AssetLink currentAlbumLink;
|
||||
private AssetLinkUtil.AssetLink currentArtistLink;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@Nullable
|
||||
@@ -66,6 +85,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateDownloadButtons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
@@ -94,6 +119,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
|
||||
artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
|
||||
|
||||
initAssetLinkChips(view);
|
||||
bindAssetLinkView(coverSong, currentSongLink);
|
||||
bindAssetLinkView(titleSong, currentSongLink);
|
||||
bindAssetLinkView(artistSong, currentArtistLink != null ? currentArtistLink : currentSongLink);
|
||||
|
||||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
@@ -157,25 +187,33 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
TextView download = view.findViewById(R.id.download_text_view);
|
||||
download.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
downloadButton = view.findViewById(R.id.download_text_view);
|
||||
downloadButton.setOnClickListener(v -> {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
} else {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
TextView remove = view.findViewById(R.id.remove_text_view);
|
||||
remove.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
removeButton = view.findViewById(R.id.remove_text_view);
|
||||
removeButton.setOnClickListener(v -> {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
} else {
|
||||
ExternalAudioReader.delete(song);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
initDownloadUI(download, remove);
|
||||
updateDownloadButtons();
|
||||
|
||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||
addToPlaylist.setOnClickListener(v -> {
|
||||
@@ -243,13 +281,109 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
dismiss();
|
||||
}
|
||||
|
||||
private void initDownloadUI(TextView download, TextView remove) {
|
||||
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
|
||||
remove.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
download.setVisibility(View.VISIBLE);
|
||||
remove.setVisibility(View.GONE);
|
||||
private void updateDownloadButtons() {
|
||||
if (downloadButton == null || removeButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
boolean downloaded = DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId());
|
||||
downloadButton.setVisibility(downloaded ? View.GONE : View.VISIBLE);
|
||||
removeButton.setVisibility(downloaded ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
boolean hasLocal = ExternalAudioReader.getUri(song) != null;
|
||||
downloadButton.setVisibility(hasLocal ? View.GONE : View.VISIBLE);
|
||||
removeButton.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initAssetLinkChips(View root) {
|
||||
assetLinkChipGroup = root.findViewById(R.id.asset_link_chip_group);
|
||||
songLinkChip = root.findViewById(R.id.asset_link_song_chip);
|
||||
albumLinkChip = root.findViewById(R.id.asset_link_album_chip);
|
||||
artistLinkChip = root.findViewById(R.id.asset_link_artist_chip);
|
||||
|
||||
currentSongLink = bindAssetLinkChip(songLinkChip, AssetLinkUtil.TYPE_SONG, song.getId());
|
||||
currentAlbumLink = bindAssetLinkChip(albumLinkChip, AssetLinkUtil.TYPE_ALBUM, song.getAlbumId());
|
||||
currentArtistLink = bindAssetLinkChip(artistLinkChip, AssetLinkUtil.TYPE_ARTIST, song.getArtistId());
|
||||
syncAssetLinkGroupVisibility();
|
||||
}
|
||||
|
||||
private AssetLinkUtil.AssetLink bindAssetLinkChip(@Nullable Chip chip, String type, @Nullable String id) {
|
||||
if (chip == null) return null;
|
||||
if (id == null || id.isEmpty()) {
|
||||
clearAssetLinkChip(chip);
|
||||
return null;
|
||||
}
|
||||
|
||||
String label = getString(AssetLinkUtil.getLabelRes(type));
|
||||
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
|
||||
if (assetLink == null) {
|
||||
clearAssetLinkChip(chip);
|
||||
return null;
|
||||
}
|
||||
|
||||
chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
|
||||
chip.setVisibility(View.VISIBLE);
|
||||
|
||||
chip.setOnClickListener(v -> {
|
||||
if (assetLink != null) {
|
||||
((MainActivity) requireActivity()).openAssetLink(assetLink);
|
||||
}
|
||||
});
|
||||
|
||||
chip.setOnLongClickListener(v -> {
|
||||
if (assetLink != null) {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return assetLink;
|
||||
}
|
||||
|
||||
private void clearAssetLinkChip(@Nullable Chip chip) {
|
||||
if (chip == null) return;
|
||||
chip.setVisibility(View.GONE);
|
||||
chip.setText("");
|
||||
chip.setOnClickListener(null);
|
||||
chip.setOnLongClickListener(null);
|
||||
}
|
||||
|
||||
private void syncAssetLinkGroupVisibility() {
|
||||
if (assetLinkChipGroup == null) return;
|
||||
boolean hasVisible = false;
|
||||
for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
|
||||
View child = assetLinkChipGroup.getChildAt(i);
|
||||
if (child.getVisibility() == View.VISIBLE) {
|
||||
hasVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void bindAssetLinkView(@Nullable View view, @Nullable AssetLinkUtil.AssetLink assetLink) {
|
||||
if (view == null) return;
|
||||
if (assetLink == null) {
|
||||
AssetLinkUtil.clearLinkAppearance(view);
|
||||
view.setOnClickListener(null);
|
||||
view.setOnLongClickListener(null);
|
||||
view.setClickable(false);
|
||||
view.setLongClickable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
view.setClickable(true);
|
||||
view.setLongClickable(true);
|
||||
AssetLinkUtil.applyLinkAppearance(view);
|
||||
view.setOnClickListener(v -> ((MainActivity) requireActivity()).openAssetLink(assetLink, !AssetLinkUtil.TYPE_SONG.equals(assetLink.type)));
|
||||
view.setOnLongClickListener(v -> {
|
||||
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
|
||||
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
@@ -263,4 +397,4 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||
private void refreshShares() {
|
||||
homeViewModel.refreshShares(requireActivity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavOptions;
|
||||
|
||||
import com.cappielloantonio.tempo.BuildConfig;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Playlist;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Genre;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog;
|
||||
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||
|
||||
public final class AssetLinkNavigator {
|
||||
private final MainActivity activity;
|
||||
private final SongRepository songRepository = new SongRepository();
|
||||
private final AlbumRepository albumRepository = new AlbumRepository();
|
||||
private final ArtistRepository artistRepository = new ArtistRepository();
|
||||
private final PlaylistRepository playlistRepository = new PlaylistRepository();
|
||||
|
||||
public AssetLinkNavigator(@NonNull MainActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
public void open(@Nullable AssetLinkUtil.AssetLink assetLink) {
|
||||
if (assetLink == null) {
|
||||
return;
|
||||
}
|
||||
switch (assetLink.type) {
|
||||
case AssetLinkUtil.TYPE_SONG:
|
||||
openSong(assetLink.id);
|
||||
break;
|
||||
case AssetLinkUtil.TYPE_ALBUM:
|
||||
openAlbum(assetLink.id);
|
||||
break;
|
||||
case AssetLinkUtil.TYPE_ARTIST:
|
||||
openArtist(assetLink.id);
|
||||
break;
|
||||
case AssetLinkUtil.TYPE_PLAYLIST:
|
||||
openPlaylist(assetLink.id);
|
||||
break;
|
||||
case AssetLinkUtil.TYPE_GENRE:
|
||||
openGenre(assetLink.id);
|
||||
break;
|
||||
case AssetLinkUtil.TYPE_YEAR:
|
||||
openYear(assetLink.id);
|
||||
break;
|
||||
default:
|
||||
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void openSong(@NonNull String id) {
|
||||
MutableLiveData<Child> liveData = songRepository.getSong(id);
|
||||
Observer<Child> observer = new Observer<Child>() {
|
||||
@Override
|
||||
public void onChanged(Child child) {
|
||||
liveData.removeObserver(this);
|
||||
if (child == null) {
|
||||
Toast.makeText(activity, R.string.asset_link_error_song, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
SongBottomSheetViewModel viewModel = new ViewModelProvider(activity).get(SongBottomSheetViewModel.class);
|
||||
viewModel.setSong(child);
|
||||
SongBottomSheetDialog dialog = new SongBottomSheetDialog();
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(Constants.TRACK_OBJECT, child);
|
||||
dialog.setArguments(args);
|
||||
dialog.show(activity.getSupportFragmentManager(), null);
|
||||
}
|
||||
};
|
||||
liveData.observe(activity, observer);
|
||||
}
|
||||
|
||||
private void openAlbum(@NonNull String id) {
|
||||
MutableLiveData<AlbumID3> liveData = albumRepository.getAlbum(id);
|
||||
Observer<AlbumID3> observer = new Observer<AlbumID3>() {
|
||||
@Override
|
||||
public void onChanged(AlbumID3 album) {
|
||||
liveData.removeObserver(this);
|
||||
if (album == null) {
|
||||
Toast.makeText(activity, R.string.asset_link_error_album, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(Constants.ALBUM_OBJECT, album);
|
||||
navigateSafely(R.id.albumPageFragment, args);
|
||||
}
|
||||
};
|
||||
liveData.observe(activity, observer);
|
||||
}
|
||||
|
||||
private void openArtist(@NonNull String id) {
|
||||
MutableLiveData<ArtistID3> liveData = artistRepository.getArtist(id);
|
||||
Observer<ArtistID3> observer = new Observer<ArtistID3>() {
|
||||
@Override
|
||||
public void onChanged(ArtistID3 artist) {
|
||||
liveData.removeObserver(this);
|
||||
if (artist == null) {
|
||||
Toast.makeText(activity, R.string.asset_link_error_artist, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(Constants.ARTIST_OBJECT, artist);
|
||||
navigateSafely(R.id.artistPageFragment, args);
|
||||
}
|
||||
};
|
||||
liveData.observe(activity, observer);
|
||||
}
|
||||
|
||||
private void openPlaylist(@NonNull String id) {
|
||||
MutableLiveData<Playlist> liveData = playlistRepository.getPlaylist(id);
|
||||
Observer<Playlist> observer = new Observer<Playlist>() {
|
||||
@Override
|
||||
public void onChanged(Playlist playlist) {
|
||||
liveData.removeObserver(this);
|
||||
if (playlist == null) {
|
||||
Toast.makeText(activity, R.string.asset_link_error_playlist, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(Constants.PLAYLIST_OBJECT, playlist);
|
||||
navigateSafely(R.id.playlistPageFragment, args);
|
||||
}
|
||||
};
|
||||
liveData.observe(activity, observer);
|
||||
}
|
||||
|
||||
private void openGenre(@NonNull String genreName) {
|
||||
String trimmed = genreName.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Genre genre = new Genre();
|
||||
genre.setGenre(trimmed);
|
||||
genre.setSongCount(0);
|
||||
genre.setAlbumCount(0);
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(Constants.GENRE_OBJECT, genre);
|
||||
args.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE);
|
||||
navigateSafely(R.id.songListPageFragment, args);
|
||||
}
|
||||
|
||||
private void openYear(@NonNull String yearValue) {
|
||||
try {
|
||||
int year = Integer.parseInt(yearValue.trim());
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("year_object", year);
|
||||
args.putString(Constants.MEDIA_BY_YEAR, Constants.MEDIA_BY_YEAR);
|
||||
navigateSafely(R.id.songListPageFragment, args);
|
||||
} catch (NumberFormatException ex) {
|
||||
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void navigateSafely(int destinationId, @Nullable Bundle args) {
|
||||
activity.runOnUiThread(() -> {
|
||||
NavController navController = activity.navController;
|
||||
if (navController == null) {
|
||||
return;
|
||||
}
|
||||
if (navController.getCurrentDestination() != null
|
||||
&& navController.getCurrentDestination().getId() == destinationId) {
|
||||
navController.navigate(destinationId, args, new NavOptions.Builder().setLaunchSingleTop(true).build());
|
||||
} else {
|
||||
navController.navigate(destinationId, args);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
public final class AssetLinkUtil {
|
||||
public static final String SCHEME = "tempo";
|
||||
public static final String HOST_ASSET = "asset";
|
||||
|
||||
public static final String TYPE_SONG = "song";
|
||||
public static final String TYPE_ALBUM = "album";
|
||||
public static final String TYPE_ARTIST = "artist";
|
||||
public static final String TYPE_PLAYLIST = "playlist";
|
||||
public static final String TYPE_GENRE = "genre";
|
||||
public static final String TYPE_YEAR = "year";
|
||||
|
||||
private AssetLinkUtil() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AssetLink parse(@Nullable Intent intent) {
|
||||
if (intent == null) return null;
|
||||
return parse(intent.getData());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AssetLink parse(@Nullable Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!SCHEME.equalsIgnoreCase(uri.getScheme())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String host = uri.getHost();
|
||||
if (!HOST_ASSET.equalsIgnoreCase(host)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uri.getPathSegments().size() < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String type = uri.getPathSegments().get(0);
|
||||
String id = uri.getPathSegments().get(1);
|
||||
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isSupportedType(type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AssetLink(type, id, uri);
|
||||
}
|
||||
|
||||
public static boolean isSupportedType(@Nullable String type) {
|
||||
if (type == null) return false;
|
||||
switch (type) {
|
||||
case TYPE_SONG:
|
||||
case TYPE_ALBUM:
|
||||
case TYPE_ARTIST:
|
||||
case TYPE_PLAYLIST:
|
||||
case TYPE_GENRE:
|
||||
case TYPE_YEAR:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Uri buildUri(@NonNull String type, @NonNull String id) {
|
||||
return new Uri.Builder()
|
||||
.scheme(SCHEME)
|
||||
.authority(HOST_ASSET)
|
||||
.appendPath(type)
|
||||
.appendPath(id)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String buildLink(@Nullable String type, @Nullable String id) {
|
||||
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id) || !isSupportedType(type)) {
|
||||
return null;
|
||||
}
|
||||
return buildUri(Objects.requireNonNull(type), Objects.requireNonNull(id)).toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AssetLink buildAssetLink(@Nullable String type, @Nullable String id) {
|
||||
String link = buildLink(type, id);
|
||||
return parseLinkString(link);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static AssetLink parseLinkString(@Nullable String link) {
|
||||
if (TextUtils.isEmpty(link)) {
|
||||
return null;
|
||||
}
|
||||
return parse(Uri.parse(link));
|
||||
}
|
||||
|
||||
public static void copyToClipboard(@NonNull Context context, @NonNull AssetLink assetLink) {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboardManager == null) {
|
||||
return;
|
||||
}
|
||||
ClipData clipData = ClipData.newPlainText(context.getString(R.string.asset_link_clipboard_label), assetLink.uri.toString());
|
||||
clipboardManager.setPrimaryClip(clipData);
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public static int getLabelRes(@NonNull String type) {
|
||||
switch (type) {
|
||||
case TYPE_SONG:
|
||||
return R.string.asset_link_label_song;
|
||||
case TYPE_ALBUM:
|
||||
return R.string.asset_link_label_album;
|
||||
case TYPE_ARTIST:
|
||||
return R.string.asset_link_label_artist;
|
||||
case TYPE_PLAYLIST:
|
||||
return R.string.asset_link_label_playlist;
|
||||
case TYPE_GENRE:
|
||||
return R.string.asset_link_label_genre;
|
||||
case TYPE_YEAR:
|
||||
return R.string.asset_link_label_year;
|
||||
default:
|
||||
return R.string.asset_link_label_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyLinkAppearance(@NonNull View view) {
|
||||
if (view instanceof TextView) {
|
||||
TextView textView = (TextView) view;
|
||||
if (textView.getTag(R.id.tag_link_original_color) == null) {
|
||||
textView.setTag(R.id.tag_link_original_color, textView.getCurrentTextColor());
|
||||
}
|
||||
int accent = MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimary,
|
||||
ContextCompat.getColor(view.getContext(), android.R.color.holo_blue_light));
|
||||
textView.setTextColor(accent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearLinkAppearance(@NonNull View view) {
|
||||
if (view instanceof TextView) {
|
||||
TextView textView = (TextView) view;
|
||||
Object original = textView.getTag(R.id.tag_link_original_color);
|
||||
if (original instanceof Integer) {
|
||||
textView.setTextColor((Integer) original);
|
||||
} else {
|
||||
int defaultColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnSurface,
|
||||
ContextCompat.getColor(view.getContext(), android.R.color.primary_text_light));
|
||||
textView.setTextColor(defaultColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class AssetLink {
|
||||
public final String type;
|
||||
public final String id;
|
||||
public final Uri uri;
|
||||
|
||||
AssetLink(@NonNull String type, @NonNull String id, @NonNull Uri uri) {
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.uri = uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,13 @@ object Constants {
|
||||
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
||||
|
||||
const val DOWNLOAD_URI = "rest/download"
|
||||
const val ACTION_PLAY_EXTERNAL_DOWNLOAD = "com.cappielloantonio.tempo.action.PLAY_EXTERNAL_DOWNLOAD"
|
||||
const val EXTRA_DOWNLOAD_URI = "EXTRA_DOWNLOAD_URI"
|
||||
const val EXTRA_DOWNLOAD_MEDIA_ID = "EXTRA_DOWNLOAD_MEDIA_ID"
|
||||
const val EXTRA_DOWNLOAD_TITLE = "EXTRA_DOWNLOAD_TITLE"
|
||||
const val EXTRA_DOWNLOAD_ARTIST = "EXTRA_DOWNLOAD_ARTIST"
|
||||
const val EXTRA_DOWNLOAD_ALBUM = "EXTRA_DOWNLOAD_ALBUM"
|
||||
const val EXTRA_DOWNLOAD_DURATION = "EXTRA_DOWNLOAD_DURATION"
|
||||
|
||||
const val DOWNLOAD_TYPE_TRACK = "download_type_track"
|
||||
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
||||
@@ -116,4 +123,13 @@ object Constants {
|
||||
const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED"
|
||||
const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS"
|
||||
const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED"
|
||||
|
||||
const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = "android.media3.session.demo.SHUFFLE_ON"
|
||||
const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF"
|
||||
const val CUSTOM_COMMAND_TOGGLE_HEART_ON = "android.media3.session.demo.HEART_ON"
|
||||
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = "android.media3.session.demo.HEART_OFF"
|
||||
const val CUSTOM_COMMAND_TOGGLE_HEART_LOADING = "android.media3.session.demo.HEART_LOADING"
|
||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF"
|
||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE"
|
||||
const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL"
|
||||
}
|
||||
@@ -78,32 +78,26 @@ public final class DownloadUtil {
|
||||
return httpDataSourceFactory;
|
||||
}
|
||||
|
||||
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
|
||||
if (dataSourceFactory == null) {
|
||||
context = context.getApplicationContext();
|
||||
public static synchronized DataSource.Factory getUpstreamDataSourceFactory(Context context) {
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
|
||||
|
||||
if (Preferences.getStreamingCacheSize() > 0) {
|
||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||
.setCache(getStreamingCache(context))
|
||||
.setUpstreamDataSourceFactory(upstreamFactory);
|
||||
|
||||
ResolvingDataSource.Factory resolvingFactory = new ResolvingDataSource.Factory(
|
||||
new StreamingCacheDataSource.Factory(streamCacheFactory),
|
||||
dataSpec -> {
|
||||
DataSpec.Builder builder = dataSpec.buildUpon();
|
||||
builder.setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN);
|
||||
return builder.build();
|
||||
}
|
||||
);
|
||||
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(resolvingFactory, getDownloadCache(context));
|
||||
} else {
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
|
||||
}
|
||||
}
|
||||
public static synchronized DataSource.Factory getCacheDataSourceFactory(Context context) {
|
||||
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
|
||||
.setCache(getStreamingCache(context))
|
||||
.setUpstreamDataSourceFactory(getUpstreamDataSourceFactory(context));
|
||||
|
||||
ResolvingDataSource.Factory resolvingFactory = new ResolvingDataSource.Factory(
|
||||
new StreamingCacheDataSource.Factory(streamCacheFactory),
|
||||
dataSpec -> {
|
||||
DataSpec.Builder builder = dataSpec.buildUpon();
|
||||
builder.setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN);
|
||||
return builder.build();
|
||||
}
|
||||
);
|
||||
dataSourceFactory = buildReadOnlyCacheDataSource(resolvingFactory, getDownloadCache(context));
|
||||
return dataSourceFactory;
|
||||
}
|
||||
|
||||
@@ -193,19 +187,21 @@ public final class DownloadUtil {
|
||||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
if (Preferences.getDownloadStoragePreference() == 0) {
|
||||
int pref = Preferences.getDownloadStoragePreference();
|
||||
if (pref == 0) {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
} else {
|
||||
} else if (pref == 1) {
|
||||
try {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
||||
} catch (Exception exception) {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||
Preferences.setDownloadStoragePreference(0);
|
||||
}
|
||||
|
||||
} else {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.cappielloantonio.tempo.util
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||
import androidx.media3.extractor.DefaultExtractorsFactory
|
||||
import androidx.media3.extractor.ExtractorsFactory
|
||||
|
||||
@UnstableApi
|
||||
class DynamicMediaSourceFactory(
|
||||
private val context: Context
|
||||
) : MediaSource.Factory {
|
||||
|
||||
override fun createMediaSource(mediaItem: MediaItem): MediaSource {
|
||||
val mediaType: String? = mediaItem.mediaMetadata.extras?.getString("type", "")
|
||||
|
||||
val streamingCacheSize = Preferences.getStreamingCacheSize()
|
||||
val bypassCache = mediaType == Constants.MEDIA_TYPE_RADIO
|
||||
|
||||
val useUpstream = when {
|
||||
streamingCacheSize.toInt() == 0 -> true
|
||||
streamingCacheSize > 0 && bypassCache -> true
|
||||
streamingCacheSize > 0 && !bypassCache -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
val dataSourceFactory: DataSource.Factory = if (useUpstream) {
|
||||
DownloadUtil.getUpstreamDataSourceFactory(context)
|
||||
} else {
|
||||
DownloadUtil.getCacheDataSourceFactory(context)
|
||||
}
|
||||
|
||||
return when {
|
||||
mediaItem.localConfiguration?.mimeType == MimeTypes.APPLICATION_M3U8 ||
|
||||
mediaItem.localConfiguration?.uri?.lastPathSegment?.endsWith(".m3u8", ignoreCase = true) == true -> {
|
||||
HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val extractorsFactory: ExtractorsFactory = DefaultExtractorsFactory()
|
||||
ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
|
||||
.createMediaSource(mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getSupportedTypes(): IntArray {
|
||||
return intArrayOf(
|
||||
C.CONTENT_TYPE_HLS,
|
||||
C.CONTENT_TYPE_OTHER
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ExternalAudioReader {
|
||||
|
||||
private static final Map<String, DocumentFile> cache = new ConcurrentHashMap<>();
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ExecutorService REFRESH_EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
private static final MutableLiveData<Long> refreshEvents = new MutableLiveData<>();
|
||||
|
||||
private static volatile String cachedDirUri;
|
||||
private static volatile boolean refreshInProgress = false;
|
||||
private static volatile boolean refreshQueued = false;
|
||||
|
||||
private static String sanitizeFileName(String name) {
|
||||
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static String normalizeForComparison(String name) {
|
||||
String s = sanitizeFileName(name);
|
||||
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
|
||||
s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
|
||||
return s.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static void ensureCache() {
|
||||
String uriString = Preferences.getDownloadDirectoryUri();
|
||||
if (uriString == null) {
|
||||
synchronized (LOCK) {
|
||||
cache.clear();
|
||||
cachedDirUri = null;
|
||||
}
|
||||
ExternalDownloadMetadataStore.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (uriString.equals(cachedDirUri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean runSynchronously = false;
|
||||
synchronized (LOCK) {
|
||||
if (refreshInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
scheduleRefreshLocked();
|
||||
return;
|
||||
}
|
||||
|
||||
refreshInProgress = true;
|
||||
runSynchronously = true;
|
||||
}
|
||||
|
||||
if (runSynchronously) {
|
||||
try {
|
||||
rebuildCache();
|
||||
} finally {
|
||||
onRefreshFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void refreshCache() {
|
||||
refreshCacheAsync();
|
||||
}
|
||||
|
||||
public static void refreshCacheAsync() {
|
||||
synchronized (LOCK) {
|
||||
cachedDirUri = null;
|
||||
cache.clear();
|
||||
}
|
||||
requestRefresh();
|
||||
}
|
||||
|
||||
public static LiveData<Long> getRefreshEvents() {
|
||||
return refreshEvents;
|
||||
}
|
||||
|
||||
private static String buildKey(String artist, String title, String album) {
|
||||
String name = artist != null && !artist.isEmpty() ? artist + " - " + title : title;
|
||||
if (album != null && !album.isEmpty()) name += " (" + album + ")";
|
||||
return normalizeForComparison(name);
|
||||
}
|
||||
|
||||
private static Uri findUri(String artist, String title, String album) {
|
||||
ensureCache();
|
||||
if (cachedDirUri == null) return null;
|
||||
|
||||
DocumentFile file = cache.get(buildKey(artist, title, album));
|
||||
return file != null && file.exists() ? file.getUri() : null;
|
||||
}
|
||||
|
||||
public static Uri getUri(Child media) {
|
||||
return findUri(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||
}
|
||||
|
||||
public static Uri getUri(PodcastEpisode episode) {
|
||||
return findUri(episode.getArtist(), episode.getTitle(), episode.getAlbum());
|
||||
}
|
||||
|
||||
public static synchronized void removeMetadata(Child media) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||
cache.remove(key);
|
||||
ExternalDownloadMetadataStore.remove(key);
|
||||
}
|
||||
|
||||
public static boolean delete(Child media) {
|
||||
ensureCache();
|
||||
if (cachedDirUri == null) return false;
|
||||
|
||||
String key = buildKey(media.getArtist(), media.getTitle(), media.getAlbum());
|
||||
DocumentFile file = cache.get(key);
|
||||
boolean deleted = false;
|
||||
if (file != null && file.exists()) {
|
||||
deleted = file.delete();
|
||||
}
|
||||
if (deleted) {
|
||||
cache.remove(key);
|
||||
ExternalDownloadMetadataStore.remove(key);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
private static void requestRefresh() {
|
||||
synchronized (LOCK) {
|
||||
scheduleRefreshLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void scheduleRefreshLocked() {
|
||||
if (refreshInProgress) {
|
||||
refreshQueued = true;
|
||||
return;
|
||||
}
|
||||
|
||||
refreshInProgress = true;
|
||||
REFRESH_EXECUTOR.execute(() -> {
|
||||
try {
|
||||
rebuildCache();
|
||||
} finally {
|
||||
onRefreshFinished();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void rebuildCache() {
|
||||
String uriString = Preferences.getDownloadDirectoryUri();
|
||||
if (uriString == null) {
|
||||
synchronized (LOCK) {
|
||||
cache.clear();
|
||||
cachedDirUri = null;
|
||||
}
|
||||
ExternalDownloadMetadataStore.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentFile directory = DocumentFile.fromTreeUri(App.getContext(), Uri.parse(uriString));
|
||||
Map<String, Long> expectedSizes = ExternalDownloadMetadataStore.snapshot();
|
||||
Set<String> verifiedKeys = new HashSet<>();
|
||||
Map<String, DocumentFile> newEntries = new HashMap<>();
|
||||
|
||||
if (directory != null && directory.canRead()) {
|
||||
for (DocumentFile file : directory.listFiles()) {
|
||||
if (file == null || file.isDirectory()) continue;
|
||||
String existing = file.getName();
|
||||
if (existing == null) continue;
|
||||
|
||||
String base = existing.replaceFirst("\\.[^\\.]+$", "");
|
||||
String key = normalizeForComparison(base);
|
||||
Long expected = expectedSizes.get(key);
|
||||
long actualLength = file.length();
|
||||
|
||||
if (expected != null && expected > 0 && actualLength == expected) {
|
||||
newEntries.put(key, file);
|
||||
verifiedKeys.add(key);
|
||||
} else {
|
||||
ExternalDownloadMetadataStore.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!expectedSizes.isEmpty()) {
|
||||
if (verifiedKeys.isEmpty()) {
|
||||
ExternalDownloadMetadataStore.clear();
|
||||
} else {
|
||||
for (String key : expectedSizes.keySet()) {
|
||||
if (!verifiedKeys.contains(key)) {
|
||||
ExternalDownloadMetadataStore.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (LOCK) {
|
||||
cache.clear();
|
||||
cache.putAll(newEntries);
|
||||
cachedDirUri = uriString;
|
||||
}
|
||||
}
|
||||
|
||||
private static void onRefreshFinished() {
|
||||
boolean runAgain;
|
||||
synchronized (LOCK) {
|
||||
refreshInProgress = false;
|
||||
runAgain = refreshQueued;
|
||||
refreshQueued = false;
|
||||
}
|
||||
|
||||
refreshEvents.postValue(SystemClock.elapsedRealtime());
|
||||
|
||||
if (runAgain) {
|
||||
requestRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.provider.Settings;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.Normalizer;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ExternalAudioWriter {
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
private static final int CONNECT_TIMEOUT_MS = 15_000;
|
||||
private static final int READ_TIMEOUT_MS = 60_000;
|
||||
|
||||
private ExternalAudioWriter() {
|
||||
}
|
||||
|
||||
private static String sanitizeFileName(String name) {
|
||||
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static String normalizeForComparison(String name) {
|
||||
String s = sanitizeFileName(name);
|
||||
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
|
||||
s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
|
||||
return s.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static DocumentFile findFile(DocumentFile dir, String fileName) {
|
||||
String normalized = normalizeForComparison(fileName);
|
||||
for (DocumentFile file : dir.listFiles()) {
|
||||
if (file.isDirectory()) continue;
|
||||
String existing = file.getName();
|
||||
if (existing != null && normalizeForComparison(existing).equals(normalized)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void downloadToUserDirectory(Context context, Child child) {
|
||||
if (context == null || child == null) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
MediaItem mediaItem = MappingUtil.mapDownload(child);
|
||||
String fallbackName = child.getTitle() != null ? child.getTitle() : child.getId();
|
||||
EXECUTOR.execute(() -> performDownload(appContext, mediaItem, fallbackName, child));
|
||||
}
|
||||
|
||||
private static void performDownload(Context context, MediaItem mediaItem, String fallbackName, Child child) {
|
||||
String uriString = Preferences.getDownloadDirectoryUri();
|
||||
if (uriString == null) {
|
||||
notifyUnavailable(context);
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentFile directory = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
|
||||
if (directory == null || !directory.canWrite()) {
|
||||
notifyFailure(context, "Cannot write to folder.");
|
||||
return;
|
||||
}
|
||||
|
||||
String artist = child.getArtist() != null ? child.getArtist() : "";
|
||||
String title = child.getTitle() != null ? child.getTitle() : fallbackName;
|
||||
String album = child.getAlbum() != null ? child.getAlbum() : "";
|
||||
String baseName = artist.isEmpty() ? title : artist + " - " + title;
|
||||
if (!album.isEmpty()) baseName += " (" + album + ")";
|
||||
if (baseName.isEmpty()) {
|
||||
baseName = fallbackName != null ? fallbackName : "download";
|
||||
}
|
||||
String metadataKey = normalizeForComparison(baseName);
|
||||
|
||||
Uri mediaUri = mediaItem != null && mediaItem.requestMetadata != null
|
||||
? mediaItem.requestMetadata.mediaUri
|
||||
: null;
|
||||
if (mediaUri == null) {
|
||||
notifyFailure(context, "Invalid media URI.");
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
return;
|
||||
}
|
||||
|
||||
String scheme = mediaUri.getScheme() != null ? mediaUri.getScheme().toLowerCase(Locale.ROOT) : "";
|
||||
|
||||
HttpURLConnection connection = null;
|
||||
DocumentFile sourceDocument = null;
|
||||
File sourceFile = null;
|
||||
long remoteLength = -1;
|
||||
String mimeType = null;
|
||||
DocumentFile targetFile = null;
|
||||
|
||||
try {
|
||||
if (scheme.equals("http") || scheme.equals("https")) {
|
||||
connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection();
|
||||
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
|
||||
connection.setReadTimeout(READ_TIMEOUT_MS);
|
||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||
connection.connect();
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) {
|
||||
notifyFailure(context, "Server returned " + responseCode);
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
return;
|
||||
}
|
||||
|
||||
mimeType = connection.getContentType();
|
||||
remoteLength = connection.getContentLengthLong();
|
||||
} else if (scheme.equals("content")) {
|
||||
sourceDocument = DocumentFile.fromSingleUri(context, mediaUri);
|
||||
mimeType = context.getContentResolver().getType(mediaUri);
|
||||
if (sourceDocument != null) {
|
||||
remoteLength = sourceDocument.length();
|
||||
}
|
||||
} else if (scheme.equals("file")) {
|
||||
String path = mediaUri.getPath();
|
||||
if (path != null) {
|
||||
sourceFile = new File(path);
|
||||
if (sourceFile.exists()) {
|
||||
remoteLength = sourceFile.length();
|
||||
}
|
||||
}
|
||||
String ext = MimeTypeMap.getFileExtensionFromUrl(mediaUri.toString());
|
||||
if (ext != null && !ext.isEmpty()) {
|
||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||||
}
|
||||
} else {
|
||||
notifyFailure(context, "Unsupported media URI.");
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mimeType == null || mimeType.isEmpty()) {
|
||||
mimeType = "application/octet-stream";
|
||||
}
|
||||
|
||||
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||
if ((extension == null || extension.isEmpty()) && sourceDocument != null && sourceDocument.getName() != null) {
|
||||
String name = sourceDocument.getName();
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot >= 0 && dot < name.length() - 1) {
|
||||
extension = name.substring(dot + 1);
|
||||
}
|
||||
}
|
||||
if ((extension == null || extension.isEmpty()) && sourceFile != null) {
|
||||
String name = sourceFile.getName();
|
||||
int dot = name.lastIndexOf('.');
|
||||
if (dot >= 0 && dot < name.length() - 1) {
|
||||
extension = name.substring(dot + 1);
|
||||
}
|
||||
}
|
||||
if (extension == null || extension.isEmpty()) {
|
||||
String suffix = child.getSuffix();
|
||||
if (suffix != null && !suffix.isEmpty()) {
|
||||
extension = suffix;
|
||||
} else {
|
||||
extension = "bin";
|
||||
}
|
||||
}
|
||||
|
||||
String sanitized = sanitizeFileName(baseName);
|
||||
if (sanitized.isEmpty()) sanitized = sanitizeFileName(fallbackName);
|
||||
if (sanitized.isEmpty()) sanitized = "download";
|
||||
String fileName = sanitized + "." + extension;
|
||||
|
||||
DocumentFile existingFile = findFile(directory, fileName);
|
||||
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
|
||||
if (existingFile != null && existingFile.exists()) {
|
||||
long localLength = existingFile.length();
|
||||
boolean matches = false;
|
||||
if (remoteLength > 0 && localLength == remoteLength) {
|
||||
matches = true;
|
||||
} else if (remoteLength <= 0 && recordedSize != null && localLength == recordedSize) {
|
||||
matches = true;
|
||||
}
|
||||
if (matches) {
|
||||
ExternalDownloadMetadataStore.recordSize(metadataKey, localLength);
|
||||
recordDownload(child, existingFile.getUri());
|
||||
ExternalAudioReader.refreshCacheAsync();
|
||||
notifyExists(context, fileName);
|
||||
return;
|
||||
} else {
|
||||
existingFile.delete();
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
}
|
||||
}
|
||||
|
||||
targetFile = directory.createFile(mimeType, fileName);
|
||||
if (targetFile == null) {
|
||||
notifyFailure(context, "Failed to create file.");
|
||||
return;
|
||||
}
|
||||
|
||||
Uri targetUri = targetFile.getUri();
|
||||
try (InputStream in = openInputStream(context, mediaUri, scheme, connection, sourceFile);
|
||||
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
|
||||
if (out == null) {
|
||||
notifyFailure(context, "Cannot open output stream.");
|
||||
targetFile.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int len;
|
||||
long total = 0;
|
||||
while ((len = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, len);
|
||||
total += len;
|
||||
}
|
||||
out.flush();
|
||||
|
||||
if (total <= 0) {
|
||||
targetFile.delete();
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
notifyFailure(context, "Empty download.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteLength > 0 && total != remoteLength) {
|
||||
targetFile.delete();
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
notifyFailure(context, "Incomplete download.");
|
||||
return;
|
||||
}
|
||||
|
||||
ExternalDownloadMetadataStore.recordSize(metadataKey, total);
|
||||
recordDownload(child, targetUri);
|
||||
notifySuccess(context, fileName, child, targetUri);
|
||||
ExternalAudioReader.refreshCacheAsync();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (targetFile != null) {
|
||||
targetFile.delete();
|
||||
}
|
||||
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||
notifyFailure(context, e.getMessage() != null ? e.getMessage() : "Download failed");
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void notifyUnavailable(Context context) {
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.getPackageName(), null));
|
||||
PendingIntent openSettings = PendingIntent.getActivity(context, 0, settingsIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("No download folder set")
|
||||
.setContentText("Tap to set one in settings")
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSilent(true)
|
||||
.setContentIntent(openSettings)
|
||||
.setAutoCancel(true);
|
||||
|
||||
manager.notify(1011, builder.build());
|
||||
}
|
||||
|
||||
private static void notifyFailure(Context context, String message) {
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("Download failed")
|
||||
.setContentText(message)
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setAutoCancel(true);
|
||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||
}
|
||||
|
||||
private static void notifySuccess(Context context, String name, Child child, Uri fileUri) {
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("Download complete")
|
||||
.setContentText(name)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setAutoCancel(true);
|
||||
|
||||
PendingIntent playIntent = buildPlayIntent(context, child, fileUri);
|
||||
if (playIntent != null) {
|
||||
builder.setContentIntent(playIntent);
|
||||
}
|
||||
|
||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||
}
|
||||
|
||||
private static void recordDownload(Child child, Uri fileUri) {
|
||||
if (child == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Download download = new Download(child);
|
||||
download.setDownloadState(1);
|
||||
if (fileUri != null) {
|
||||
download.setDownloadUri(fileUri.toString());
|
||||
}
|
||||
|
||||
new DownloadRepository().insert(download);
|
||||
}
|
||||
|
||||
private static void notifyExists(Context context, String name) {
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("Already downloaded")
|
||||
.setContentText(name)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||
.setAutoCancel(true);
|
||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||
}
|
||||
|
||||
private static PendingIntent buildPlayIntent(Context context, Child child, Uri fileUri) {
|
||||
if (fileUri == null) return null;
|
||||
Intent intent = new Intent(context, MainActivity.class)
|
||||
.setAction(Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD)
|
||||
.putExtra(Constants.EXTRA_DOWNLOAD_URI, fileUri.toString())
|
||||
.putExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID, child.getId())
|
||||
.putExtra(Constants.EXTRA_DOWNLOAD_TITLE, child.getTitle())
|
||||
.putExtra(Constants.EXTRA_DOWNLOAD_ARTIST, child.getArtist())
|
||||
.putExtra(Constants.EXTRA_DOWNLOAD_ALBUM, child.getAlbum())
|
||||
.putExtra(Constants.EXTRA_DOWNLOAD_DURATION, child.getDuration() != null ? child.getDuration() : 0)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
|
||||
int requestCode;
|
||||
if (child.getId() != null) {
|
||||
requestCode = Math.abs(child.getId().hashCode());
|
||||
} else {
|
||||
requestCode = Math.abs(fileUri.toString().hashCode());
|
||||
}
|
||||
|
||||
return PendingIntent.getActivity(
|
||||
context,
|
||||
requestCode,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
}
|
||||
|
||||
private static InputStream openInputStream(Context context,
|
||||
Uri mediaUri,
|
||||
String scheme,
|
||||
HttpURLConnection connection,
|
||||
File sourceFile) throws IOException {
|
||||
switch (scheme) {
|
||||
case "http":
|
||||
case "https":
|
||||
if (connection == null) {
|
||||
throw new IOException("Connection not initialized");
|
||||
}
|
||||
return connection.getInputStream();
|
||||
case "content":
|
||||
InputStream contentStream = context.getContentResolver().openInputStream(mediaUri);
|
||||
if (contentStream == null) {
|
||||
throw new IOException("Cannot open content stream");
|
||||
}
|
||||
return contentStream;
|
||||
case "file":
|
||||
if (sourceFile == null || !sourceFile.exists()) {
|
||||
throw new IOException("Missing source file");
|
||||
}
|
||||
return new FileInputStream(sourceFile);
|
||||
default:
|
||||
throw new IOException("Unsupported scheme " + scheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ExternalDownloadMetadataStore {
|
||||
|
||||
private static final String PREF_KEY = "external_download_metadata";
|
||||
|
||||
private ExternalDownloadMetadataStore() {
|
||||
}
|
||||
|
||||
private static SharedPreferences preferences() {
|
||||
return App.getInstance().getPreferences();
|
||||
}
|
||||
|
||||
private static JSONObject readAll() {
|
||||
String raw = preferences().getString(PREF_KEY, "{}");
|
||||
try {
|
||||
return new JSONObject(raw);
|
||||
} catch (JSONException e) {
|
||||
return new JSONObject();
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeAll(JSONObject object) {
|
||||
preferences().edit().putString(PREF_KEY, object.toString()).apply();
|
||||
}
|
||||
|
||||
public static synchronized void clear() {
|
||||
writeAll(new JSONObject());
|
||||
}
|
||||
|
||||
public static synchronized void recordSize(String key, long size) {
|
||||
if (key == null || size <= 0) {
|
||||
return;
|
||||
}
|
||||
JSONObject object = readAll();
|
||||
try {
|
||||
object.put(key, size);
|
||||
} catch (JSONException ignored) {
|
||||
}
|
||||
writeAll(object);
|
||||
}
|
||||
|
||||
public static synchronized void remove(String key) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject object = readAll();
|
||||
object.remove(key);
|
||||
writeAll(object);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static synchronized Long getSize(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
JSONObject object = readAll();
|
||||
if (!object.has(key)) {
|
||||
return null;
|
||||
}
|
||||
long size = object.optLong(key, -1L);
|
||||
return size > 0 ? size : null;
|
||||
}
|
||||
|
||||
public static synchronized Map<String, Long> snapshot() {
|
||||
JSONObject object = readAll();
|
||||
if (object.length() == 0) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Long> sizes = new HashMap<>();
|
||||
Iterator<String> keys = object.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
long size = object.optLong(key, -1L);
|
||||
if (size > 0) {
|
||||
sizes.put(key, size);
|
||||
}
|
||||
}
|
||||
return sizes;
|
||||
}
|
||||
|
||||
public static synchronized void retainOnly(Set<String> keysToKeep) {
|
||||
if (keysToKeep == null || keysToKeep.isEmpty()) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
JSONObject object = readAll();
|
||||
if (object.length() == 0) {
|
||||
return;
|
||||
}
|
||||
Set<String> keys = new HashSet<>();
|
||||
Iterator<String> iterator = object.keys();
|
||||
while (iterator.hasNext()) {
|
||||
keys.add(iterator.next());
|
||||
}
|
||||
boolean changed = false;
|
||||
for (String key : keys) {
|
||||
if (!keysToKeep.contains(key)) {
|
||||
object.remove(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
writeAll(object);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.HeartRating;
|
||||
|
||||
import com.cappielloantonio.tempo.App;
|
||||
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
|
||||
@@ -16,6 +18,7 @@ import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
|
||||
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -71,6 +74,12 @@ public class MappingUtil {
|
||||
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
|
||||
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
|
||||
bundle.putString("uri", uri.toString());
|
||||
bundle.putString("assetLinkSong", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId()));
|
||||
bundle.putString("assetLinkAlbum", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId()));
|
||||
bundle.putString("assetLinkArtist", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId()));
|
||||
bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre()));
|
||||
Integer year = media.getYear();
|
||||
bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null);
|
||||
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(media.getId())
|
||||
@@ -83,6 +92,13 @@ public class MappingUtil {
|
||||
.setAlbumTitle(media.getAlbum())
|
||||
.setArtist(media.getArtist())
|
||||
.setArtworkUri(artworkUri)
|
||||
.setUserRating(new HeartRating(media.getStarred() != null))
|
||||
.setSupportedCommands(
|
||||
ImmutableList.of(
|
||||
Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||
Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF
|
||||
)
|
||||
)
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
@@ -110,6 +126,11 @@ public class MappingUtil {
|
||||
}
|
||||
|
||||
public static MediaItem mapDownload(Child media) {
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("samplingRate", media.getSamplingRate() != null ? media.getSamplingRate() : 0);
|
||||
bundle.putInt("bitDepth", media.getBitDepth() != null ? media.getBitDepth() : 0);
|
||||
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(media.getId())
|
||||
.setMediaMetadata(
|
||||
@@ -120,12 +141,14 @@ public class MappingUtil {
|
||||
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
|
||||
.setAlbumTitle(media.getAlbum())
|
||||
.setArtist(media.getArtist())
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
.build()
|
||||
)
|
||||
.setRequestMetadata(
|
||||
new MediaItem.RequestMetadata.Builder()
|
||||
.setExtras(bundle)
|
||||
.setMediaUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId()))
|
||||
.build()
|
||||
)
|
||||
@@ -140,7 +163,6 @@ public class MappingUtil {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("id", internetRadioStation.getId());
|
||||
bundle.putString("title", internetRadioStation.getName());
|
||||
bundle.putString("artist", uri.toString());
|
||||
bundle.putString("uri", uri.toString());
|
||||
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
|
||||
|
||||
@@ -149,7 +171,6 @@ public class MappingUtil {
|
||||
.setMediaMetadata(
|
||||
new MediaMetadata.Builder()
|
||||
.setTitle(internetRadioStation.getName())
|
||||
.setArtist(internetRadioStation.getStreamUrl())
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
@@ -219,12 +240,20 @@ public class MappingUtil {
|
||||
}
|
||||
|
||||
private static Uri getUri(Child media) {
|
||||
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||
Uri local = ExternalAudioReader.getUri(media);
|
||||
return local != null ? local : MusicUtil.getStreamUri(media.getId());
|
||||
}
|
||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
|
||||
? getDownloadUri(media.getId())
|
||||
: MusicUtil.getStreamUri(media.getId());
|
||||
}
|
||||
|
||||
private static Uri getUri(PodcastEpisode podcastEpisode) {
|
||||
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||
Uri local = ExternalAudioReader.getUri(podcastEpisode);
|
||||
return local != null ? local : MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||
}
|
||||
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getStreamId())
|
||||
? getDownloadUri(podcastEpisode.getStreamId())
|
||||
: MusicUtil.getStreamUri(podcastEpisode.getStreamId());
|
||||
@@ -234,4 +263,11 @@ public class MappingUtil {
|
||||
Download download = new DownloadRepository().getDownload(id);
|
||||
return download != null && !download.getDownloadUri().isEmpty() ? Uri.parse(download.getDownloadUri()) : MusicUtil.getDownloadUri(id);
|
||||
}
|
||||
|
||||
public static void observeExternalAudioRefresh(LifecycleOwner owner, Runnable onRefresh) {
|
||||
if (owner == null || onRefresh == null) {
|
||||
return;
|
||||
}
|
||||
ExternalAudioReader.getRefreshEvents().observe(owner, event -> onRefresh.run());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ object Preferences {
|
||||
private const val WIFI_ONLY = "wifi_only"
|
||||
private const val DATA_SAVING_MODE = "data_saving_mode"
|
||||
private const val SERVER_UNREACHABLE = "server_unreachable"
|
||||
private const val SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE = "sync_starred_artists_for_offline_use"
|
||||
private const val SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE = "sync_starred_albums_for_offline_use"
|
||||
private const val SYNC_STARRED_TRACKS_FOR_OFFLINE_USE = "sync_starred_tracks_for_offline_use"
|
||||
private const val QUEUE_SYNCING = "queue_syncing"
|
||||
private const val QUEUE_SYNCING_COUNTDOWN = "queue_syncing_countdown"
|
||||
@@ -44,11 +46,13 @@ object Preferences {
|
||||
private const val ROUNDED_CORNER_SIZE = "rounded_corner_size"
|
||||
private const val PODCAST_SECTION_VISIBILITY = "podcast_section_visibility"
|
||||
private const val RADIO_SECTION_VISIBILITY = "radio_section_visibility"
|
||||
private const val AUTO_DOWNLOAD_LYRICS = "auto_download_lyrics"
|
||||
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
|
||||
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
|
||||
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
||||
private const val DOWNLOAD_STORAGE = "download_storage"
|
||||
private const val DOWNLOAD_DIRECTORY_URI = "download_directory_uri"
|
||||
private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
|
||||
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
||||
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
||||
@@ -63,11 +67,17 @@ object Preferences {
|
||||
private const val ALWAYS_ON_DISPLAY = "always_on_display"
|
||||
private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item"
|
||||
private const val HOME_SECTOR_LIST = "home_sector_list"
|
||||
private const val SONG_RATING_PER_ITEM = "song_rating_per_item"
|
||||
private const val RATING_PER_ITEM = "rating_per_item"
|
||||
private const val NEXT_UPDATE_CHECK = "next_update_check"
|
||||
private const val CONTINUOUS_PLAY = "continuous_play"
|
||||
private const val LAST_INSTANT_MIX = "last_instant_mix"
|
||||
|
||||
private const val ALLOW_PLAYLIST_DUPLICATES = "allow_playlist_duplicates"
|
||||
private const val EQUALIZER_ENABLED = "equalizer_enabled"
|
||||
private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels"
|
||||
private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility"
|
||||
private const val ALBUM_SORT_ORDER = "album_sort_order"
|
||||
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
|
||||
|
||||
@JvmStatic
|
||||
fun getServer(): String? {
|
||||
@@ -159,6 +169,24 @@ object Preferences {
|
||||
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isAutoDownloadLyricsEnabled(): Boolean {
|
||||
val preferences = App.getInstance().preferences
|
||||
|
||||
if (preferences.contains(AUTO_DOWNLOAD_LYRICS)) {
|
||||
return preferences.getBoolean(AUTO_DOWNLOAD_LYRICS, false)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAutoDownloadLyricsEnabled(isEnabled: Boolean) {
|
||||
App.getInstance().preferences.edit()
|
||||
.putBoolean(AUTO_DOWNLOAD_LYRICS, isEnabled)
|
||||
.apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLocalAddress(): String? {
|
||||
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
|
||||
@@ -300,6 +328,30 @@ object Preferences {
|
||||
.apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isStarredArtistsSyncEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setStarredArtistsSyncEnabled(isStarredSyncEnabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(
|
||||
SYNC_STARRED_ARTISTS_FOR_OFFLINE_USE, isStarredSyncEnabled
|
||||
).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isStarredAlbumsSyncEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setStarredAlbumsSyncEnabled(isStarredSyncEnabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(
|
||||
SYNC_STARRED_ALBUMS_FOR_OFFLINE_USE, isStarredSyncEnabled
|
||||
).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isStarredSyncEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, false)
|
||||
@@ -312,6 +364,16 @@ object Preferences {
|
||||
).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showShuffleInsteadOfHeart(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setShuffleInsteadOfHeart(enabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, enabled).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showServerUnreachableDialog(): Boolean {
|
||||
return App.getInstance().preferences.getLong(
|
||||
@@ -405,6 +467,20 @@ object Preferences {
|
||||
).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDownloadDirectoryUri(): String? {
|
||||
return App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setDownloadDirectoryUri(uri: String?) {
|
||||
val current = App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null)
|
||||
if (current != uri) {
|
||||
ExternalDownloadMetadataStore.clear()
|
||||
}
|
||||
App.getInstance().preferences.edit().putString(DOWNLOAD_DIRECTORY_URI, uri).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDefaultDownloadViewType(): String {
|
||||
return App.getInstance().preferences.getString(
|
||||
@@ -486,6 +562,11 @@ object Preferences {
|
||||
App.getInstance().preferences.edit().putString(HOME_SECTOR_LIST, Gson().toJson(extension)).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showItemStarRating(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(SONG_RATING_PER_ITEM, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun showItemRating(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false)
|
||||
@@ -519,4 +600,54 @@ object Preferences {
|
||||
LAST_INSTANT_MIX, 0
|
||||
) + 5000 < System.currentTimeMillis()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAllowPlaylistDuplicates(allowDuplicates: Boolean) {
|
||||
return App.getInstance().preferences.edit().putString(
|
||||
ALLOW_PLAYLIST_DUPLICATES,
|
||||
allowDuplicates.toString()
|
||||
).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun allowPlaylistDuplicates(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(ALLOW_PLAYLIST_DUPLICATES, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setEqualizerEnabled(enabled: Boolean) {
|
||||
App.getInstance().preferences.edit().putBoolean(EQUALIZER_ENABLED, enabled).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isEqualizerEnabled(): Boolean {
|
||||
return App.getInstance().preferences.getBoolean(EQUALIZER_ENABLED, false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setEqualizerBandLevels(bandLevels: ShortArray) {
|
||||
val asString = bandLevels.joinToString(",")
|
||||
App.getInstance().preferences.edit().putString(EQUALIZER_BAND_LEVELS, asString).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEqualizerBandLevels(bandCount: Short): ShortArray {
|
||||
val str = App.getInstance().preferences.getString(EQUALIZER_BAND_LEVELS, null)
|
||||
if (str.isNullOrBlank()) {
|
||||
return ShortArray(bandCount.toInt())
|
||||
}
|
||||
val parts = str.split(",")
|
||||
if (parts.size < bandCount) return ShortArray(bandCount.toInt())
|
||||
return ShortArray(bandCount.toInt()) { i -> parts[i].toShortOrNull() ?: 0 }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getAlbumSortOrder(): String {
|
||||
return App.getInstance().preferences.getString(ALBUM_SORT_ORDER, DEFAULT_ALBUM_SORT_ORDER) ?: DEFAULT_ALBUM_SORT_ORDER
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setAlbumSortOrder(sortOrder: String) {
|
||||
App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
@@ -16,10 +19,14 @@ import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
private final AlbumRepository albumRepository;
|
||||
@@ -54,7 +61,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
return albumRepository.getAlbumTracks(album.getId());
|
||||
}
|
||||
|
||||
public void setFavorite() {
|
||||
public void setFavorite(Context context) {
|
||||
if (album.getStarred() != null) {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
removeFavoriteOffline();
|
||||
@@ -65,7 +72,7 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
setFavoriteOffline();
|
||||
} else {
|
||||
setFavoriteOnline();
|
||||
setFavoriteOnline(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +90,6 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
favoriteRepository.unstar(null, album.getId(), null, new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
// album.setStarred(new Date());
|
||||
favoriteRepository.starLater(null, album.getId(), null, false);
|
||||
}
|
||||
});
|
||||
@@ -96,15 +102,31 @@ public class AlbumBottomSheetViewModel extends AndroidViewModel {
|
||||
album.setStarred(new Date());
|
||||
}
|
||||
|
||||
private void setFavoriteOnline() {
|
||||
private void setFavoriteOnline(Context context) {
|
||||
favoriteRepository.star(null, album.getId(), null, new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
// album.setStarred(null);
|
||||
favoriteRepository.starLater(null, album.getId(), null, true);
|
||||
}
|
||||
});
|
||||
|
||||
album.setStarred(new Date());
|
||||
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
||||
AlbumRepository albumRepository = new AlbumRepository();
|
||||
MutableLiveData<List<Child>> tracksLiveData = albumRepository.getAlbumTracks(album.getId());
|
||||
|
||||
tracksLiveData.observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
tracksLiveData.removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.interfaces.StarCallback;
|
||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.NetworkUtil;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.List;
|
||||
|
||||
public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
private final ArtistRepository artistRepository;
|
||||
@@ -34,7 +42,7 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
this.artist = artist;
|
||||
}
|
||||
|
||||
public void setFavorite() {
|
||||
public void setFavorite(Context context) {
|
||||
if (artist.getStarred() != null) {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
removeFavoriteOffline();
|
||||
@@ -43,9 +51,9 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
}
|
||||
} else {
|
||||
if (NetworkUtil.isOffline()) {
|
||||
setFavoriteOffline();
|
||||
setFavoriteOffline(context);
|
||||
} else {
|
||||
setFavoriteOnline();
|
||||
setFavoriteOnline(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +67,6 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
favoriteRepository.unstar(null, null, artist.getId(), new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
// artist.setStarred(new Date());
|
||||
favoriteRepository.starLater(null, null, artist.getId(), false);
|
||||
}
|
||||
});
|
||||
@@ -67,20 +74,45 @@ public class ArtistBottomSheetViewModel extends AndroidViewModel {
|
||||
artist.setStarred(null);
|
||||
}
|
||||
|
||||
private void setFavoriteOffline() {
|
||||
private void setFavoriteOffline(Context context) {
|
||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||
artist.setStarred(new Date());
|
||||
}
|
||||
|
||||
private void setFavoriteOnline() {
|
||||
private void setFavoriteOnline(Context context) {
|
||||
favoriteRepository.star(null, null, artist.getId(), new StarCallback() {
|
||||
@Override
|
||||
public void onError() {
|
||||
// artist.setStarred(null);
|
||||
favoriteRepository.starLater(null, null, artist.getId(), true);
|
||||
}
|
||||
});
|
||||
|
||||
artist.setStarred(new Date());
|
||||
|
||||
Log.d("ArtistSync", "Checking preference: " + Preferences.isStarredArtistsSyncEnabled());
|
||||
|
||||
if (Preferences.isStarredArtistsSyncEnabled()) {
|
||||
Log.d("ArtistSync", "Starting artist sync for: " + artist.getName());
|
||||
|
||||
artistRepository.getArtistAllSongs(artist.getId(), new ArtistRepository.ArtistSongsCallback() {
|
||||
@Override
|
||||
public void onSongsCollected(List<Child> songs) {
|
||||
Log.d("ArtistSync", "Callback triggered with songs: " + (songs != null ? songs.size() : 0));
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
Log.d("ArtistSync", "Starting download of " + songs.size() + " songs");
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
Log.d("ArtistSync", "Download started successfully");
|
||||
} else {
|
||||
Log.d("ArtistSync", "No songs to download");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Log.d("ArtistSync", "Artist sync preference is disabled");
|
||||
}
|
||||
}
|
||||
///
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.cappielloantonio.tempo.viewmodel;
|
||||
|
||||
import android.app.Application;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -8,10 +9,13 @@ import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.model.DownloadStack;
|
||||
import com.cappielloantonio.tempo.repository.DownloadRepository;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -25,6 +29,7 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||
|
||||
private final MutableLiveData<List<Child>> downloadedTrackSample = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<ArrayList<DownloadStack>> viewStack = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<Integer> refreshResult = new MutableLiveData<>();
|
||||
|
||||
public DownloadViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
@@ -43,6 +48,10 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||
return viewStack;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getRefreshResult() {
|
||||
return refreshResult;
|
||||
}
|
||||
|
||||
public void initViewStack(DownloadStack level) {
|
||||
ArrayList<DownloadStack> stack = new ArrayList<>();
|
||||
stack.add(level);
|
||||
@@ -60,4 +69,59 @@ public class DownloadViewModel extends AndroidViewModel {
|
||||
stack.remove(stack.size() - 1);
|
||||
viewStack.setValue(stack);
|
||||
}
|
||||
|
||||
public void refreshExternalDownloads() {
|
||||
new Thread(() -> {
|
||||
String directoryUri = Preferences.getDownloadDirectoryUri();
|
||||
if (directoryUri == null) {
|
||||
refreshResult.postValue(-1);
|
||||
return;
|
||||
}
|
||||
|
||||
List<Download> downloads = downloadRepository.getAllDownloads();
|
||||
if (downloads == null || downloads.isEmpty()) {
|
||||
refreshResult.postValue(0);
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<Download> toRemove = new ArrayList<>();
|
||||
|
||||
for (Download download : downloads) {
|
||||
String uriString = download.getDownloadUri();
|
||||
if (uriString == null || uriString.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if (uri.getScheme() == null || !uri.getScheme().equalsIgnoreCase("content")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DocumentFile file;
|
||||
try {
|
||||
file = DocumentFile.fromSingleUri(getApplication(), uri);
|
||||
} catch (SecurityException exception) {
|
||||
file = null;
|
||||
}
|
||||
|
||||
if (file == null || !file.exists()) {
|
||||
toRemove.add(download);
|
||||
}
|
||||
}
|
||||
|
||||
if (!toRemove.isEmpty()) {
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
for (Download download : toRemove) {
|
||||
ids.add(download.getId());
|
||||
ExternalAudioReader.removeMetadata(download);
|
||||
}
|
||||
|
||||
downloadRepository.delete(ids);
|
||||
ExternalAudioReader.refreshCache();
|
||||
refreshResult.postValue(ids.size());
|
||||
} else {
|
||||
refreshResult.postValue(0);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,9 @@ public class HomeViewModel extends AndroidViewModel {
|
||||
private final PlaylistRepository playlistRepository;
|
||||
private final SharingRepository sharingRepository;
|
||||
|
||||
private final StarredAlbumsSyncViewModel albumsSyncViewModel;
|
||||
private final StarredArtistsSyncViewModel artistSyncViewModel;
|
||||
|
||||
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<List<AlbumID3>> newReleasedAlbum = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<List<Child>> starredTracksSample = new MutableLiveData<>(null);
|
||||
@@ -82,6 +85,9 @@ public class HomeViewModel extends AndroidViewModel {
|
||||
playlistRepository = new PlaylistRepository();
|
||||
sharingRepository = new SharingRepository();
|
||||
|
||||
albumsSyncViewModel = new StarredAlbumsSyncViewModel(application);
|
||||
artistSyncViewModel = new StarredArtistsSyncViewModel(application);
|
||||
|
||||
setOfflineFavorite();
|
||||
}
|
||||
|
||||
@@ -166,6 +172,14 @@ public class HomeViewModel extends AndroidViewModel {
|
||||
return starredAlbums;
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getAllStarredAlbumSongs() {
|
||||
return albumsSyncViewModel.getAllStarredAlbumSongs();
|
||||
}
|
||||
|
||||
public LiveData<List<Child>> getAllStarredArtistSongs() {
|
||||
return artistSyncViewModel.getAllStarredArtistSongs();
|
||||
}
|
||||
|
||||
public LiveData<List<ArtistID3>> getStarredArtists(LifecycleOwner owner) {
|
||||
if (starredArtists.getValue() == null) {
|
||||
artistRepository.getStarredArtists(true, 20).observe(owner, starredArtists::postValue);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user