75 Commits

Author SHA1 Message Date
eddyizm
0c3b43c5dc fix: build paths 2025-10-26 11:49:08 -07:00
eddyizm
830e9076f1 never surrender 2025-10-26 11:32:23 -07:00
eddyizm
cd9ae97bc7 fix: bumped version, update path from build error logs 2025-10-26 11:17:59 -07:00
eddyizm
1b59a8e8ef chore: bumped build version 2025-10-26 10:59:22 -07:00
eddyizm
391405fc76 fix: workflow broken, taking another approach 2025-10-26 10:58:18 -07:00
eddyizm
4bdcbacf62 chore: bumped version for new workflow build 2025-10-26 10:15:22 -07:00
eddyizm
6cbae700bf fix: type in the workflow yaml 2025-10-26 10:02:59 -07:00
eddyizm
e7555119f0 chore: bumped version, updated ignore file 2025-10-26 09:35:12 -07:00
eddyizm
e228e74e6a Update Polish translation (#188) 2025-10-26 07:27:11 -07:00
skajmer
062f4db2cf Merge branch 'eddyizm:development' into development 2025-10-26 09:53:04 +01:00
eddyizm
cb75e34b92 fix: updated work flow for new tempus release 2025-10-25 19:51:12 -07:00
eddyizm
36005c5f51 Tempus rebrand (#183) 2025-10-25 13:12:49 -07:00
eddyizm
8ae0900269 Merge branch 'development' into tempus-rebrand 2025-10-25 13:12:32 -07:00
eddyizm
f286c7b1b9 fix: readme update for new screenshot size 2025-10-25 13:10:58 -07:00
eddyizm
c5d0af67a7 chore: added a few more screenshots 2025-10-25 13:06:24 -07:00
skajmer
3fb4ccd791 Update strings.xml
#158, #161
2025-10-24 23:10:57 +02:00
eddyizm
577b50a85b chore: bumped to alpha to release debug apks 2025-10-22 21:45:03 -07:00
eddyizm
e6a56ba1d2 chore: update readme and usage references to tempus. added new banner… (#182) 2025-10-22 08:27:57 -07:00
eddyizm
2740b6da29 feat: added green launcher to degoogled variant 2025-10-22 07:55:13 -07:00
eddyizm
21c4ae77ba fix: splash logo sized to not crop anymore 2025-10-21 11:25:01 -07:00
eddyizm
7a83a03a90 feat: first pass swapping new icon, build config and rename folder structure 2025-10-21 09:23:25 -07:00
eddyizm
be0480538e Merge branch 'development' into tempus-readme-update 2025-10-20 12:22:25 -07:00
eddyizm
b48057b4a2 fix: persist album sorting on resume (#181) 2025-10-20 12:18:41 -07:00
eddyizm
fa430eaac4 Unhide genre from album details view (#161) 2025-10-20 12:18:11 -07:00
eddyizm
82ee9b4639 chore: reset changelog to 1 since its a new app 2025-10-20 09:27:31 -07:00
eddyizm
9b807fde31 fix: typo in spanish/port language, fixing capitalizion in git url 2025-10-19 18:25:34 -07:00
eddyizm
c7ba4235b3 chore: updated strings with tempus and updated screenshots. 2025-10-19 17:37:55 -07:00
eddyizm
d27e431f73 fix: typo in readme link 2025-10-19 11:18:19 -07:00
eddyizm
d23eea4f27 chore: updated fastlane docs, icon, and reset changelogs to start initial release 2025-10-19 10:08:23 -07:00
eddyizm
430e7105eb fix: updated html for banner 2025-10-19 08:19:48 -07:00
eddyizm
024c4e6118 chore: update readme and usage references to tempus. added new banner logo 2025-10-18 22:19:03 -07:00
eddyizm
7b2ee9da3a fix: updated workflow for 32/64 bit apks (#176) 2025-10-18 17:27:00 -07:00
eddyizm
c3cce18600 fix: persist album sorting on resume 2025-10-18 17:16:11 -07:00
sebaFlame
442fe1ea01 Merge branch 'development' into main 2025-10-18 01:48:47 +02:00
cba
cb0874dca4 Added setting to make album detail visible 2025-10-18 01:00:00 +02:00
cba
079149c1d5 Revert "removed dropdown for album info"
This reverts commit ceaffa254b.
2025-10-18 00:37:06 +02:00
eddyizm
118f742cb6 Check also underlying transport (#90) 2025-10-17 06:46:56 -07:00
Thomas Anderson
c028c52576 Merge branch 'main' into patch-1 2025-10-17 12:26:42 +03:00
eddyizm
96c5d0fca8 fix: updated workflow for 32/64 bit apks 2025-10-16 21:54:53 -07:00
eddyizm
e39a5e2d5c chore: updated changelog 2025-10-16 17:19:49 -07:00
eddyizm
a06ab77b42 chore: bumped version for release 2025-10-16 14:26:14 -07:00
eddyizm
04a6176bfd fix: limits image size to prevent widget crash #172 (#175) 2025-10-16 13:25:25 -07:00
eddyizm
1f4464e089 fix: Include shuffle/repeat controls in f-droid build's media notific… (#174) 2025-10-16 13:25:04 -07:00
eddyizm
9d01d2057a fix: limits image size to prevent widget crash #172 2025-10-15 21:57:24 -07:00
eddyizm
ad440c490a Fix album parse empty date field (#171) 2025-10-15 07:26:22 -07:00
le-firehawk
acdcfff9ac fix: Include shuffle/repeat controls in f-droid build's media notification window 2025-10-15 21:00:39 +10:30
eddyizm
8c7a25cbd0 fix: update to handle nulls in the sort function 2025-10-13 21:49:41 -07:00
eddyizm
bdca5e16ed Merge branch 'development' into fix-album-parse-empty-date-field 2025-10-13 21:10:01 -07:00
eddyizm
f091b3d248 fix: handle empty date fields from subsonic json 2025-10-13 21:09:27 -07:00
eddyizm
18cd84f820 fix: persist album sort preference (#168) 2025-10-13 16:53:06 -07:00
eddyizm
281ebf8263 fix: General build warning and playback issues (#167) 2025-10-13 06:59:33 -07:00
eddyizm
2854ac6354 fix: persist album sort preference. 2025-10-12 22:12:53 -07:00
eddyizm
16d25a1f1d Merge branch 'main' into development 2025-10-12 09:57:55 -07:00
eddyizm
5d3ca8acfa chore: added multi library documentation 2025-10-12 09:57:35 -07:00
eddyizm
0689272046 fix: workflow trigger updated for my tagging convention 2025-10-12 09:56:52 -07:00
le-firehawk
17372fc4d0 fix: Resolve parcel serialization build warnings 2025-10-12 23:20:47 +10:30
le-firehawk
44679855cd fix: Replace poor syntax that created warnings during build 2025-10-12 23:20:47 +10:30
le-firehawk
78e7032903 fix: When creating MediaService, restore player from previous queue 2025-10-12 23:20:47 +10:30
eddyizm
8d8087f2d6 chore: fix some grammar in readme 2025-10-11 09:07:38 -07:00
eddyizm
5b6a4fab62 chore: updated changelog 2025-10-10 22:30:55 -07:00
eddyizm
fdc41b299c chore: updated ignore file for release apk files 2025-10-10 22:29:41 -07:00
eddyizm
82c22ed247 chore: bumped version for release 2025-10-10 22:17:09 -07:00
eddyizm
48ce3a2a4f chore: updated release info 2025-10-10 22:02:01 -07:00
eddyizm
b93acc6563 fix: Glide module incorrectly encoding IPv6 addresses (#159) 2025-10-09 22:18:49 -07:00
eddyizm
9c088a7e88 feat: Make all objects in Tempo references for quick access (#158) 2025-10-09 22:18:11 -07:00
eddyizm
de2f1067a7 chore: Update Polish translation (#160) 2025-10-09 22:11:19 -07:00
eddyizm
1c21546461 chore: adding screenshot and docs for 4 icons/buttons in player control (#162) 2025-10-09 21:54:38 -07:00
eddyizm
a4121e8d49 chore: adding screenshot and docs for 4 icons/buttons in player control 2025-10-09 21:53:52 -07:00
cba
ceaffa254b removed dropdown for album info 2025-10-09 23:03:25 +02:00
skajmer
4cc4cc7363 Update Polish translation
Stuff from:
#140 
#135 
#98 
#152
2025-10-09 21:41:12 +02:00
le-firehawk
c5ef274916 fix: Glide module incorrectly encoding IPv6 addresses 2025-10-09 23:14:54 +10:30
le-firehawk
2c53f36a18 fix: Support content URIs for external downloader 2025-10-09 23:03:57 +10:30
le-firehawk
6c637dcbcb feat: Make all objects in Tempo references for quick access 2025-10-09 23:03:57 +10:30
eddyizm
89fa38f5a0 chore: updated change log 2025-10-08 21:55:00 -07:00
Thomas Anderson
ffcfd81c28 Check also underlaying transport 2025-09-05 15:45:00 +03:00
194 changed files with 2480 additions and 1026 deletions

View File

@@ -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,21 +35,21 @@ jobs:
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- name: Build All APKs
- name: Build All Release APKs
id: build
run: |
# Build release variants
bash ./gradlew assembleTempoRelease
bash ./gradlew assembleNotquitemyRelease
# Build debug variants
bash ./gradlew assembleTempoDebug
bash ./gradlew assembleNotquitemyDebug
# Only build release variants (removed debug builds)
bash ./gradlew assembleTempusRelease
bash ./gradlew assembleDegoogledRelease
- name: Sign Tempo Release APKs
id: sign_tempo_release
- name: Create Artifact Staging Directory
run: mkdir -p release-artifacts
- name: Sign Tempus Release APKs
id: sign_tempus_release
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/tempo/release
releaseDirectory: app/build/outputs/apk/tempus/release
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -57,11 +57,31 @@ jobs:
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Sign NotQuiteMy Release APKs
id: sign_notquitemy_release
- name: Prepare Signed Tempus APKs for Release
run: |
TEMPUS_PATH=app/build/outputs/apk/tempus/release
echo "--- Tempus Files BEFORE Move ---"
ls -la $TEMPUS_PATH
echo "--------------------------------"
# FIX: Use find/xargs for robust file matching and moving.
# Renaming 64-bit APK and moving to safe staging directory
find $TEMPUS_PATH -name '*arm64-v8a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*arm64-v8a*signed.apk ./release-artifacts/app-tempus-arm64-v8a-release.apk
# Renaming 32-bit APK and moving to safe staging directory
find $TEMPUS_PATH -name '*armeabi-v7a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*armeabi-v7a*signed.apk ./release-artifacts/app-tempus-armeabi-v7a-release.apk
echo "Prepared Tempus APKs."
- name: Sign Degoogled Release APKs
id: sign_degoogled_release
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/notquitemy/release
releaseDirectory: app/build/outputs/apk/degoogled/release
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -69,50 +89,41 @@ jobs:
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Prepare Signed Degoogled APKs for Release
run: |
DEGOOGLED_PATH=app/build/outputs/apk/degoogled/release
echo "--- Degoogled Files BEFORE Move ---"
ls -la $DEGOOGLED_PATH
echo "--------------------------------"
# FIX: Use find/xargs for robust file matching and moving.
# Renaming 64-bit APK and moving to safe staging directory
find $DEGOOGLED_PATH -name '*arm64-v8a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*arm64-v8a*signed.apk ./release-artifacts/app-degoogled-arm64-v8a-release.apk
# Renaming 32-bit APK and moving to safe staging directory
find $DEGOOGLED_PATH -name '*armeabi-v7a*signed.apk' -print0 | xargs -0 mv -t ./release-artifacts/
mv ./release-artifacts/*armeabi-v7a*signed.apk ./release-artifacts/app-degoogled-armeabi-v7a-release.apk
echo "Prepared Degoogled APKs."
ls -la ./release-artifacts/
- name: Create Release
id: create_release
uses: actions/create-release@v1
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ github.ref }}
release_name: Release v${{ github.ref }}
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body: '> Changelog coming soon'
env:
GITHUB_TOKEN: ${{ github.token }}
draft: false
prerelease: false
files: ./release-artifacts/*.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_tempo_release.outputs.signedReleaseFile}}
asset_name: app-tempo-release.apk
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
- name: Upload Release APKs as artifacts (For easy pipeline access)
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
path: ./release-artifacts/*.apk
retention-days: 30

3
.gitignore vendored
View File

@@ -17,4 +17,5 @@
.vscode/settings.json
# release / debug files
tempus-release-key.jks
app/tempo/
app/tempus/
app/degoogled/

View File

@@ -1,6 +1,57 @@
# Changelog
***This log is for this fork to detail updates since 3.9.0 from the main repo.***
## [4.0.2](https://github.com/eddyizm/tempo/releases/tag/v4.0.2) (2025-10-26)
## Attention
This release will not update previous installs as it is considered a new app, no longer `Tempo`, new icon, new app id, and new app name. Hoping it will not be a huge inconvenience but was necessary in order to publish to app stores like IzzyDroid and FDroid.
**Android Auto**
Support should be the same as before, however, I was not able to test any of the icons/visuals, so please let me know if there are any remnants of the tempo logo/icon as I believe I removed them all and replaced them successfully.
## What's Changed
* Check also underlying transport by @zc-devs in https://github.com/eddyizm/tempus/pull/90
* fix: updated workflow for 32/64 bit apks by @eddyizm in https://github.com/eddyizm/tempus/pull/176
* Unhide genre from album details view by @sebaFlame in https://github.com/eddyizm/tempus/pull/161
* fix: persist album sorting on resume by @eddyizm in https://github.com/eddyizm/tempus/pull/181
* chore: update readme and usage references to tempus. added new banner… by @eddyizm in https://github.com/eddyizm/tempus/pull/182
* Tempus rebrand by @eddyizm in https://github.com/eddyizm/tempus/pull/183
* Update Polish translation by @skajmer in https://github.com/eddyizm/tempus/pull/188
## New Contributors
* @zc-devs made their first contribution in https://github.com/eddyizm/tempus/pull/90
* @sebaFlame made their first contribution in https://github.com/eddyizm/tempus/pull/161
**Full Changelog**: https://github.com/eddyizm/tempus/compare/v3.17.14...v4.0.1
## [3.17.14](https://github.com/eddyizm/tempo/releases/tag/v3.17.14) (2025-10-16)
## What's Changed
* fix: General build warning and playback issues by @le-firehawk in https://github.com/eddyizm/tempo/pull/167
* fix: persist album sort preference by @eddyizm in https://github.com/eddyizm/tempo/pull/168
* Fix album parse empty date field by @eddyizm in https://github.com/eddyizm/tempo/pull/171
* fix: Include shuffle/repeat controls in f-droid build's media notific… by @le-firehawk in https://github.com/eddyizm/tempo/pull/174
* fix: limits image size to prevent widget crash #172 by @eddyizm in https://github.com/eddyizm/tempo/pull/175
**Full Changelog**: https://github.com/eddyizm/tempo/compare/v3.17.0...v3.17.14
## [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
@@ -139,3 +190,5 @@
[\#400](https://github.com/CappielloAntonio/tempo/pull/400)
- [Chore] Spanish translation [\#374](https://github.com/CappielloAntonio/tempo/pull/374)
- [Chore] Polish translation [\#378](https://github.com/CappielloAntonio/tempo/pull/378)
***This log is for this fork to detail updates since 3.9.0 from the main repo.***

View File

@@ -1,5 +1,5 @@
<p align="center">
<img alt="Tempo" title="Tempo" src="mockup/svg/horizontal_logo.svg" width="250">
<img alt="Tempus" title="Tempus" src="mockup/svg/tempus_horizontal_logo.png" width="250">
</p>
<p align="center">
@@ -14,47 +14,52 @@
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.cappielloantonio.tempo"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
</p> -->
**Tempo** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
**Tempus** is an open-source and lightweight music client for Subsonic, designed and built natively for Android. It provides a seamless and intuitive music streaming experience, allowing you to access and play your Subsonic music library directly from your Android device.
Tempo does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience.
Tempus does not rely on magic algorithms to decide what you should listen to. Instead, the interface is built around your listening history, randomness, and optionally integrates with services like Last.fm to personalize your music experience.
**If you find Tempo useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
The project is a fork of [Tempo](#credits).
**If you find Tempus useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
**Use the Github version of the app for full Android Auto and Chromecast support.**
## Fork
sha256 signing key fingerprint
`SHA256: 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`
`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.
### Releases
Moved details to [CHANGELOG.md](https://github.com/eddyizm/tempo/blob/main/CHANGELOG.md)
Please note the two variants in the release assets include release/debug and 32/64 bit flavors.
`app-tempus` <- The github release with all the android auto/chromecast features
`app-degoogled*` <- 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.
[CHANGELOG.md](CHANGELOG.md)
[**Buy me a coffee**](https://ko-fi.com/eddyizm)
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.
- **Subsonic Integration**: Tempus seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
- **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history.
- **Browse and Search**: Easily navigate through your music library using various browsing and searching options, including artists, albums, genres, playlists, decades and more.
- **Streaming and Offline Mode**: Stream music directly from your Subsonic server. Offline mode is currently under active development and may have limitations when using multiple servers.
- **Playlist Management**: Create, edit, and manage playlists to curate your perfect music collection.
- **Gapless Playback**: Experience uninterrupted playback with gapless listening mode.
- **Chromecast Support**: Stream your music to Chromecast devices. The support is currently in a rudimentary state.
- **Scrobbling Integration**: Optionally integrate Tempo with Last.fm to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
- **Scrobbling Integration**: Optionally integrate Tempus with Last.fm or Listenbrainz.org to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempus, expanding your audio entertainment options.
- **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.
## Sponsors
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (3.9.0)
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.
- **Multiple Libraries**: Tempus handles multi-library setups gracefully. They are displayed as Library folders.
- **Equalizer**: Option to use in app equalizer.
- **Widget**: New widget to keeping the basic controls on your screen at all times.
- **Available in 11 languages**: Currently in Chinese, French, German, Italian, Korean, Polish, Portuguese, Russion, Spanish and Turkish
## Screenshot
@@ -63,14 +68,13 @@ Tempo is an open-source project developed and maintained solely by me. I would l
</p>
<p align="center">
<img src="mockup/light/1_screenshot.png" width=200>
<img src="mockup/light/2_screenshot.png" width=200>
<img src="mockup/light/3_screenshot.png" width=200>
<img src="mockup/light/4_screenshot.png" width=200>
<img src="mockup/light/5_screenshot.png" width=200>
<img src="mockup/light/6_screenshot.png" width=200>
<img src="mockup/light/7_screenshot.png" width=200>
<img src="mockup/light/8_screenshot.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_light.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_light.png" width=200>
</p>
<br>
@@ -80,16 +84,31 @@ Tempo is an open-source project developed and maintained solely by me. I would l
</p>
<p align="center">
<img src="mockup/dark/1_screenshot.png" width=200>
<img src="mockup/dark/2_screenshot.png" width=200>
<img src="mockup/dark/3_screenshot.png" width=200>
<img src="mockup/dark/4_screenshot.png" width=200>
<img src="mockup/dark/5_screenshot.png" width=200>
<img src="mockup/dark/6_screenshot.png" width=200>
<img src="mockup/dark/7_screenshot.png" width=200>
<img src="mockup/dark/8_screenshot.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6_dark.png" width=200>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/8_dark.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.
Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome.
## Credits
Thanks to the original repo/creator [CappielloAntonio](https://github.com/CappielloAntonio) (forked from v3.9.0)
[Opensvg.org](https://opensvg.org) for the new turntable logo.

View File

@@ -1,4 +1,4 @@
# Tempo Usage Guide
# Tempus Usage Guide
[<- back home](README.md)
## Table of Contents
@@ -34,7 +34,7 @@ This app works with any service that implements the Subsonic API, including:
## Getting Started
### Installation
1. Download the APK from the [Releases](https://github.com/eddyizm/tempo/releases) section
1. Download the APK from the [Releases](https://github.com/eddyizm/tempus/releases) section
2. Enable "Install from unknown sources" in your Android settings
3. Install the application
@@ -57,10 +57,30 @@ This app works with any service that implements the Subsonic API, including:
## Main Features
### Library View
**TODO**
**Multi-library**
Tempus 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 Tempus app.
### Now Playing Screen
**TODO**
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
@@ -137,8 +157,8 @@ This app works with any service that implements the Subsonic API, including:
### 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.
- Question? Start a [Discussion](https://github.com/eddyizm/tempus/discussions)
- Open an [issue](https://github.com/eddyizm/tempus/issues) if you don't find a discussion solving your issue.
- Consult your Subsonic server's documentation
---

View File

@@ -10,8 +10,8 @@ android {
minSdkVersion 24
targetSdk 35
versionCode 34
versionName '3.16.6'
versionCode 1
versionName '4.0.5'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
javaCompileOptions {
@@ -40,14 +40,14 @@ android {
flavorDimensions += "default"
productFlavors {
tempo {
tempus {
dimension = "default"
applicationId 'com.cappielloantonio.tempo'
applicationId 'com.eddyizm.tempus'
}
notquitemy {
degoogled {
dimension = "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
applicationId "com.eddyizm.degoogled.tempus"
}
}
@@ -110,7 +110,7 @@ dependencies {
implementation 'androidx.media3:media3-exoplayer:1.5.1'
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'
tempusImplementation 'androidx.media3:media3-cast:1.5.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -14,15 +14,19 @@ import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.repository.QueueRepository
import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.AssetLinkUtil
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
import com.cappielloantonio.tempo.util.MappingUtil
import com.cappielloantonio.tempo.util.Preferences
import com.cappielloantonio.tempo.util.ReplayGainUtil
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
@@ -83,6 +87,7 @@ class MediaService : MediaLibraryService() {
initializeCustomCommands()
initializePlayer()
initializeMediaLibrarySession()
restorePlayerFromQueue()
initializePlayerListener()
initializeEqualizerManager()
@@ -118,15 +123,17 @@ class MediaService : MediaLibraryService() {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
shuffleCommands.forEach { commandButton ->
// TODO: Aggiungere i comandi personalizzati
// commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
(shuffleCommands + repeatCommands).forEach { commandButton ->
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
customLayout = buildCustomLayout(session.player)
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(availableSessionCommands.build())
.setAvailablePlayerCommands(connectionResult.availablePlayerCommands)
.setCustomLayout(customLayout)
.build()
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
@@ -225,7 +232,7 @@ class MediaService : MediaLibraryService() {
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(DynamicMediaSourceFactory(this))
.setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK)
@@ -268,6 +275,33 @@ class MediaService : MediaLibraryService() {
}
}
private fun restorePlayerFromQueue() {
if (player.mediaItemCount > 0) return
val queueRepository = QueueRepository()
val storedQueue = queueRepository.media
if (storedQueue.isNullOrEmpty()) return
val mediaItems = MappingUtil.mapMediaItems(storedQueue)
if (mediaItems.isEmpty()) return
val lastIndex = try {
queueRepository.lastPlayedMediaIndex
} catch (_: Exception) {
0
}.coerceIn(0, mediaItems.size - 1)
val lastPosition = try {
queueRepository.lastPlayedMediaTimestamp
} catch (_: Exception) {
0L
}.let { if (it < 0L) 0L else it }
player.setMediaItems(mediaItems, lastIndex, lastPosition)
player.prepare()
updateWidget()
}
private fun initializePlayerListener() {
player.addListener(object : Player.Listener {
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
@@ -398,7 +432,7 @@ class MediaService : MediaLibraryService() {
.build()
}
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
private fun ignoreFuture(@Suppress("UNUSED_PARAMETER") customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
@@ -421,7 +455,14 @@ class MediaService : MediaLibraryService() {
?: mi?.mediaMetadata?.extras?.getString("artist")
val album = mi?.mediaMetadata?.albumTitle?.toString()
?: mi?.mediaMetadata?.extras?.getString("album")
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
val extras = mi?.mediaMetadata?.extras
val coverId = extras?.getString("coverArtId")
val songLink = extras?.getString("assetLinkSong")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
val albumLink = extras?.getString("assetLinkAlbum")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
val artistLink = extras?.getString("assetLinkArtist")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
WidgetUpdateManager.updateFromState(
@@ -434,7 +475,10 @@ class MediaService : MediaLibraryService() {
player.shuffleModeEnabled,
player.repeatMode,
position,
duration
duration,
songLink,
albumLink,
artistLink
)
}
@@ -453,6 +497,7 @@ class MediaService : MediaLibraryService() {
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory(): MediaSource.Factory = DynamicMediaSourceFactory(this)
}
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L

View File

@@ -0,0 +1,54 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.49"
android:scaleY="0.49"
android:translateX="130.56"
android:translateY="130.56">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#8CC152"/> <path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#62A43B"/> <path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#8CC152"/> <path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</group>
</vector>

View File

@@ -0,0 +1,53 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.55"
android:scaleY="0.55"
android:translateX="150.56"
android:translateY="150.56">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#8CC152"/> <path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#62A43B"/> <path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#8CC152"/> <path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</group>
</vector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#626A75</color>
</resources>

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -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());
}
}

View File

@@ -125,7 +125,7 @@ public class CustomGlideRequest {
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);

View File

@@ -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
}
}
}

View File

@@ -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")

View File

@@ -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
@@ -62,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?,
)

View File

@@ -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

View File

@@ -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<>());
}
});

View File

@@ -80,6 +80,33 @@ public class PlaylistRepository {
return listLivePlaylistSongs;
}
public MutableLiveData<Playlist> getPlaylist(String id) {
MutableLiveData<Playlist> playlistLiveData = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.getPlaylist(id)
.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().getPlaylist() != null) {
playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
} else {
playlistLiveData.setValue(null);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
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();

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -38,21 +38,36 @@ public class CacheUtil {
return chain.proceed(request);
};
private boolean isConnected() {
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
Network network = connectivityManager.getActiveNetwork();
if (network != null) {
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities != null) {
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
}
if (connectivityManager == null) {
return false;
}
return false;
Network network = connectivityManager.getActiveNetwork();
if (network == null) {
return false;
}
NetworkCapabilities capabilities = connectivityManager.getNetworkCapabilities(network);
if (capabilities == null) {
return false;
}
boolean hasInternet = capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
if (!hasInternet) {
return false;
}
boolean hasAppropriateTransport = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
if (!hasAppropriateTransport) {
return false;
}
return true;
}
}

View File

@@ -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
}
}

View File

@@ -37,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;
@@ -60,6 +62,8 @@ 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;
@@ -76,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);
@@ -311,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() {
@@ -443,6 +466,7 @@ public class MainActivity extends BaseActivity {
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
pendingDownloadPlaybackIntent = new Intent(intent);
}
handleAssetLinkIntent(intent);
}
private void consumePendingPlaybackIntent() {
@@ -452,6 +476,35 @@ public class MainActivity extends BaseActivity {
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)) {
@@ -500,4 +553,4 @@ public class MainActivity extends BaseActivity {
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
}
}
}

View File

@@ -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 {
@@ -151,13 +152,27 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
}
}
public void setItemsWithoutFilter(List<AlbumID3> albums) {
this.albumsFull = new ArrayList<>(albums);
this.albums = new ArrayList<>(albums);
notifyDataSetChanged();
}
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 +181,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;
}

View File

@@ -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,6 +11,7 @@ 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;
@@ -21,6 +23,11 @@ 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;
@@ -52,6 +59,8 @@ public class TrackInfoDialog extends DialogFragment {
}
private void setTrackInfo() {
genreLink = null;
yearLink = null;
bind.trakTitleInfoTextView.setText(mediaMetadata.title);
bind.trakArtistInfoTextView.setText(
mediaMetadata.artist != null
@@ -61,17 +70,41 @@ public class TrackInfoDialog extends DialogFragment {
: "");
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)));
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(mediaMetadata.extras.getInt("year", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("year", 0)) : getString(R.string.label_placeholder));
bind.genreValueSector.setText(mediaMetadata.extras.getString("genre", 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)));
@@ -83,6 +116,12 @@ public class TrackInfoDialog extends DialogFragment {
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(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);
}
}
@@ -135,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;
});
}
}

View File

@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -32,26 +33,50 @@ 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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@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;
private List<com.cappielloantonio.tempo.subsonic.models.AlbumID3> originalAlbums;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
currentSortOrder = Preferences.getAlbumSortOrder();
initData();
}
@Override
public void onResume() {
super.onResume();
String latestSort = Preferences.getAlbumSortOrder();
if (!latestSort.equals(currentSortOrder)) {
currentSortOrder = latestSort;
}
// Re-apply sort when returning to fragment
if (originalAlbums != null && currentSortOrder != null) {
applySortToAlbums(currentSortOrder);
} else {
Log.d(TAG, "onResume - Cannot re-sort, missing data");
}
}
@Override
public void onDestroy() {
super.onDestroy();
@@ -115,7 +140,12 @@ 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 -> {
originalAlbums = albums;
currentSortOrder = Preferences.getAlbumSortOrder();
applySortToAlbums(currentSortOrder);
updateSortIndicator();
});
bind.albumCatalogueRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
@@ -125,6 +155,16 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
bind.albumListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_album_popup_menu));
}
private void applySortToAlbums(String sortOrder) {
if (originalAlbums == null) {
return;
}
albumAdapter.setItemsWithoutFilter(originalAlbums);
if (sortOrder != null) {
albumAdapter.sort(sortOrder);
}
}
private void initProgressLoader() {
albumCatalogueViewModel.getLoadingStatus().observe(getViewLifecycleOwner(), isLoading -> {
if (isLoading) {
@@ -137,6 +177,37 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
});
}
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 +243,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;
Preferences.setAlbumSortOrder(newSortOrder);
applySortToAlbums(newSortOrder);
updateSortIndicator();
return true;
}

View File

@@ -35,6 +35,7 @@ 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;
@@ -177,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());
@@ -220,6 +248,10 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind.albumDetailView.setVisibility(View.GONE);
}
});
if(Preferences.showAlbumDetail()){
bind.albumDetailView.setVisibility(View.VISIBLE);
}
}
private void initAlbumInfoTextButton() {
@@ -347,4 +379,23 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
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());
}
}

View File

@@ -195,6 +195,7 @@ public class PlayerBottomSheetFragment extends Fragment {
}
}
private void setMediaControllerUI(MediaBrowser mediaBrowser) {
if (mediaBrowser.getMediaMetadata().extras != null) {
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {

View File

@@ -13,9 +13,10 @@ 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.RatingBar;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout;
@@ -41,12 +42,14 @@ 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;
@@ -76,6 +79,10 @@ public class PlayerControllerFragment extends Fragment {
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;
@@ -139,6 +146,10 @@ public class PlayerControllerFragment extends Fragment {
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();
}
@@ -219,6 +230,8 @@ public class PlayerControllerFragment extends Fragment {
|| 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) {
@@ -259,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);
@@ -548,4 +665,4 @@ public class PlayerControllerFragment extends Fragment {
isServiceBound = false;
}
}
}
}

View File

@@ -30,6 +30,7 @@ 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;
@@ -39,6 +40,8 @@ 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;
@@ -56,6 +59,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
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;
@@ -109,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 -> {
@@ -282,6 +297,95 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
}
}
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() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
@@ -293,4 +397,4 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private void refreshShares() {
homeViewModel.refreshShares(requireActivity());
}
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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;
}
}
}

View File

@@ -17,6 +17,9 @@ 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;
@@ -102,35 +105,76 @@ public class ExternalAudioWriter {
ExternalDownloadMetadataStore.remove(metadataKey);
return;
}
String scheme = mediaUri.getScheme();
if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
notifyFailure(context, "Unsupported 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 {
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);
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;
}
String mimeType = connection.getContentType();
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()) {
@@ -146,7 +190,6 @@ public class ExternalAudioWriter {
String fileName = sanitized + "." + extension;
DocumentFile existingFile = findFile(directory, fileName);
long remoteLength = connection.getContentLengthLong();
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
if (existingFile != null && existingFile.exists()) {
long localLength = existingFile.length();
@@ -175,7 +218,7 @@ public class ExternalAudioWriter {
}
Uri targetUri = targetFile.getUri();
try (InputStream in = connection.getInputStream();
try (InputStream in = openInputStream(context, mediaUri, scheme, connection, sourceFile);
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
if (out == null) {
notifyFailure(context, "Cannot open output stream.");
@@ -319,4 +362,32 @@ public class ExternalAudioWriter {
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);
}
}
}

View File

@@ -74,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())

View File

@@ -76,6 +76,9 @@ object Preferences {
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_DETAIL = "album_detail"
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? {
@@ -638,4 +641,19 @@ object Preferences {
if (parts.size < bandCount) return ShortArray(bandCount.toInt())
return ShortArray(bandCount.toInt()) { i -> parts[i].toShortOrNull() ?: 0 }
}
@JvmStatic
fun showAlbumDetail(): Boolean {
return App.getInstance().preferences.getBoolean(ALBUM_DETAIL, false)
}
@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()
}
}

View File

@@ -5,17 +5,20 @@ import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.widget.RemoteViews;
import com.cappielloantonio.tempo.R;
import android.app.TaskStackBuilder;
import android.app.PendingIntent;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import android.util.Log;
import androidx.annotation.Nullable;
public class WidgetProvider extends AppWidgetProvider {
private static final String TAG = "TempoWidget";
public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE";
@@ -28,7 +31,7 @@ public class WidgetProvider extends AppWidgetProvider {
public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
for (int id : ids) {
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
attachIntents(ctx, rv, id);
attachIntents(ctx, rv, id, null, null, null);
mgr.updateAppWidget(id, rv);
}
}
@@ -50,16 +53,23 @@ public class WidgetProvider extends AppWidgetProvider {
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId);
attachIntents(context, rv, appWidgetId);
attachIntents(context, rv, appWidgetId, null, null, null);
appWidgetManager.updateAppWidget(appWidgetId, rv);
WidgetUpdateManager.refreshFromController(context);
}
public static void attachIntents(Context ctx, RemoteViews rv) {
attachIntents(ctx, rv, 0);
attachIntents(ctx, rv, 0, null, null, null);
}
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) {
attachIntents(ctx, rv, requestCodeBase, null, null, null);
}
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase,
String songLink,
String albumLink,
String artistLink) {
PendingIntent playPause = PendingIntent.getBroadcast(
ctx,
requestCodeBase + 0,
@@ -97,9 +107,31 @@ public class WidgetProvider extends AppWidgetProvider {
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
PendingIntent launch = TaskStackBuilder.create(ctx)
.addNextIntentWithParentStack(new Intent(ctx, MainActivity.class))
.getPendingIntent(requestCodeBase + 10, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent launch = buildMainActivityPendingIntent(ctx, requestCodeBase + 10, null);
rv.setOnClickPendingIntent(R.id.root, launch);
PendingIntent songPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 20, songLink);
PendingIntent artistPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 21, artistLink);
PendingIntent albumPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 22, albumLink);
PendingIntent fallback = launch;
rv.setOnClickPendingIntent(R.id.album_art, songPending != null ? songPending : fallback);
rv.setOnClickPendingIntent(R.id.title, songPending != null ? songPending : fallback);
rv.setOnClickPendingIntent(R.id.subtitle,
artistPending != null ? artistPending : (songPending != null ? songPending : fallback));
rv.setOnClickPendingIntent(R.id.album, albumPending != null ? albumPending : fallback);
}
private static PendingIntent buildMainActivityPendingIntent(Context ctx, int requestCode, @Nullable String link) {
Intent intent;
if (!TextUtils.isEmpty(link)) {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link), ctx, MainActivity.class);
} else {
intent = new Intent(ctx, MainActivity.class);
}
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx);
stackBuilder.addNextIntentWithParentStack(intent);
return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
}
}

View File

@@ -4,8 +4,9 @@ import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
@@ -17,6 +18,7 @@ import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -25,6 +27,8 @@ import java.util.concurrent.ExecutionException;
public final class WidgetUpdateManager {
private static final int WIDGET_SAFE_ART_SIZE = 512;
public static void updateFromState(Context ctx,
String title,
String artist,
@@ -34,7 +38,10 @@ public final class WidgetUpdateManager {
boolean shuffleEnabled,
int repeatMode,
long positionMs,
long durationMs) {
long durationMs,
String songLink,
String albumLink,
String artistLink) {
if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing);
if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle);
if (TextUtils.isEmpty(album)) album = "";
@@ -46,7 +53,7 @@ public final class WidgetUpdateManager {
for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing,
timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id);
WidgetProvider.attachIntents(ctx, rv, id);
WidgetProvider.attachIntents(ctx, rv, id, songLink, albumLink, artistLink);
mgr.updateAppWidget(id, rv);
}
}
@@ -56,7 +63,7 @@ public final class WidgetUpdateManager {
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
for (int id : ids) {
android.widget.RemoteViews rv = chooseBuild(ctx, id);
WidgetProvider.attachIntents(ctx, rv, id);
WidgetProvider.attachIntents(ctx, rv, id, null, null, null);
mgr.updateAppWidget(id, rv);
}
}
@@ -70,7 +77,10 @@ public final class WidgetUpdateManager {
boolean shuffleEnabled,
int repeatMode,
long positionMs,
long durationMs) {
long durationMs,
String songLink,
String albumLink,
String artistLink) {
final Context appCtx = ctx.getApplicationContext();
final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title;
final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist;
@@ -79,12 +89,15 @@ public final class WidgetUpdateManager {
final boolean sh = shuffleEnabled;
final int rep = repeatMode;
final TimingInfo timing = createTimingInfo(positionMs, durationMs);
final String songLinkFinal = songLink;
final String albumLinkFinal = albumLink;
final String artistLinkFinal = artistLink;
if (!TextUtils.isEmpty(coverArtId)) {
CustomGlideRequest.loadAlbumArtBitmap(
appCtx,
coverArtId,
com.cappielloantonio.tempo.util.Preferences.getImageSize(),
WIDGET_SAFE_ART_SIZE,
new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> transition) {
@@ -93,7 +106,7 @@ public final class WidgetUpdateManager {
for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p,
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id);
WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv);
}
}
@@ -105,7 +118,7 @@ public final class WidgetUpdateManager {
for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id);
WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv);
}
}
@@ -117,7 +130,7 @@ public final class WidgetUpdateManager {
for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id);
WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv);
}
}
@@ -133,6 +146,7 @@ public final class WidgetUpdateManager {
MediaController c = future.get();
androidx.media3.common.MediaItem mi = c.getCurrentMediaItem();
String title = null, artist = null, album = null, coverId = null;
String songLink = null, albumLink = null, artistLink = null;
if (mi != null && mi.mediaMetadata != null) {
if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString();
if (mi.mediaMetadata.artist != null)
@@ -140,10 +154,26 @@ public final class WidgetUpdateManager {
if (mi.mediaMetadata.albumTitle != null)
album = mi.mediaMetadata.albumTitle.toString();
if (mi.mediaMetadata.extras != null) {
Bundle extras = mi.mediaMetadata.extras;
if (title == null) title = mi.mediaMetadata.extras.getString("title");
if (artist == null) artist = mi.mediaMetadata.extras.getString("artist");
if (album == null) album = mi.mediaMetadata.extras.getString("album");
coverId = mi.mediaMetadata.extras.getString("coverArtId");
coverId = extras.getString("coverArtId");
songLink = extras.getString("assetLinkSong");
if (songLink == null) {
songLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras.getString("id"));
}
albumLink = extras.getString("assetLinkAlbum");
if (albumLink == null) {
albumLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras.getString("albumId"));
}
artistLink = extras.getString("assetLinkArtist");
if (artistLink == null) {
artistLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras.getString("artistId"));
}
}
}
long position = c.getCurrentPosition();
@@ -159,7 +189,10 @@ public final class WidgetUpdateManager {
c.getShuffleModeEnabled(),
c.getRepeatMode(),
position,
duration);
duration,
songLink,
albumLink,
artistLink);
c.release();
} catch (ExecutionException | InterruptedException ignored) {
}
@@ -273,4 +306,4 @@ public final class WidgetUpdateManager {
}
}
}
}

View File

@@ -0,0 +1,56 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.49"
android:scaleY="0.49"
android:translateX="130.56"
android:translateY="130.56">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#DA4453"/>
<path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#ED5564"/>
<path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#DA4453"/>
<path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.66,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</group>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M3.9,12c0,1.71 1.39,3.1 3.1,3.1h3v1.8h-3c-2.7,0 -4.9,-2.2 -4.9,-4.9s2.2,-4.9 4.9,-4.9h3v1.8h-3c-1.71,0 -3.1,1.39 -3.1,3.1zM7,13h10v-2H7v2zM17,6.9h-3v-1.8h3c2.7,0 4.9,2.2 4.9,4.9s-2.2,4.9 -4.9,4.9h-3v-1.8h3c1.71,0 3.1,-1.39 3.1,-3.1s-1.39,-3.1 -3.1,-3.1z" />
</vector>

View File

@@ -1,54 +1,56 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="288dp"
android:height="288dp"
android:viewportWidth="288"
android:viewportHeight="288">
<path
android:pathData="M141.67,131.18h4.67v88.93h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M132.33,138.18h4.67v65.58h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M122.99,145.18h4.67v21.01h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M94.98,145.18h4.67v21.01h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M188.35,145.18h4.67v21.01h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M113.65,138.18h4.67v35.02h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M169.68,138.18h4.67v35.02h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M104.32,131.18h4.67v49.02h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M179.02,131.18h4.67v49.02h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M160.34,145.18h4.67v21.01h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="M151,138.18h4.67v65.58h-4.67z"
android:fillColor="#f24b6a"/>
<path
android:pathData="m114.29,92.75v4.22h-7.13v19.62h-5.01v-19.62h-7.16v-4.22h19.31Z"
android:fillColor="#fff"/>
<path
android:pathData="m126.32,111.41c-0.12,1.05 -0.66,2.11 -1.63,3.19 -1.51,1.71 -3.62,2.57 -6.34,2.57 -2.24,0 -4.22,-0.72 -5.94,-2.17 -1.71,-1.44 -2.57,-3.8 -2.57,-7.05 0,-3.05 0.77,-5.39 2.32,-7.02 1.55,-1.63 3.56,-2.44 6.02,-2.44 1.47,0 2.79,0.27 3.96,0.82 1.18,0.55 2.15,1.42 2.91,2.6 0.69,1.05 1.14,2.26 1.34,3.64 0.12,0.81 0.17,1.97 0.15,3.49h-12.07c0.06,1.77 0.62,3.01 1.67,3.72 0.64,0.44 1.4,0.66 2.3,0.66 0.95,0 1.72,-0.27 2.31,-0.81 0.32,-0.29 0.61,-0.7 0.86,-1.21h4.71ZM121.76,106.01c-0.08,-1.22 -0.44,-2.14 -1.11,-2.77 -0.66,-0.63 -1.49,-0.95 -2.47,-0.95 -1.07,0 -1.9,0.33 -2.48,1 -0.59,0.67 -0.96,1.57 -1.11,2.72h7.16Z"
android:fillColor="#fff"/>
<path
android:pathData="m138.65,103.81c-0.39,-0.85 -1.15,-1.28 -2.28,-1.28 -1.32,0 -2.2,0.43 -2.65,1.28 -0.25,0.49 -0.37,1.21 -0.37,2.17v10.61h-4.67v-17.6h4.48v2.57c0.57,-0.92 1.11,-1.57 1.62,-1.96 0.89,-0.69 2.05,-1.04 3.48,-1.04 1.35,0 2.44,0.3 3.27,0.89 0.67,0.55 1.18,1.26 1.52,2.12 0.6,-1.04 1.35,-1.8 2.25,-2.28 0.95,-0.49 2.01,-0.73 3.17,-0.73 0.78,0 1.54,0.15 2.3,0.45 0.75,0.3 1.44,0.83 2.05,1.58 0.5,0.61 0.83,1.37 1,2.26 0.11,0.59 0.16,1.46 0.16,2.6l-0.03,11.11h-4.72v-11.22c0,-0.67 -0.11,-1.22 -0.32,-1.65 -0.41,-0.82 -1.16,-1.23 -2.26,-1.23 -1.27,0 -2.15,0.53 -2.64,1.58 -0.25,0.56 -0.37,1.23 -0.37,2.02v10.5h-4.64v-10.5c0,-1.05 -0.11,-1.81 -0.32,-2.28Z"
android:fillColor="#fff"/>
<path
android:pathData="m171.26,100.85c1.42,1.52 2.13,3.75 2.13,6.7 0,3.11 -0.7,5.47 -2.09,7.1 -1.4,1.63 -3.19,2.44 -5.39,2.44 -1.4,0 -2.57,-0.35 -3.49,-1.05 -0.51,-0.39 -1,-0.95 -1.49,-1.7v9.19h-4.56v-24.57h4.42v2.6c0.5,-0.77 1.02,-1.37 1.58,-1.81 1.02,-0.79 2.24,-1.18 3.66,-1.18 2.06,0 3.81,0.76 5.24,2.28ZM168.64,107.77c0,-1.36 -0.31,-2.56 -0.93,-3.61 -0.62,-1.05 -1.63,-1.57 -3.02,-1.57 -1.67,0 -2.82,0.79 -3.44,2.38 -0.32,0.84 -0.49,1.91 -0.49,3.2 0,2.05 0.54,3.49 1.63,4.32 0.65,0.49 1.41,0.73 2.3,0.73 1.28,0 2.26,-0.5 2.94,-1.49 0.67,-0.99 1.01,-2.31 1.01,-3.96Z"
android:fillColor="#fff"/>
<path
android:pathData="m190.79,101.19c1.49,1.87 2.23,4.07 2.23,6.61s-0.74,4.8 -2.23,6.64c-1.49,1.84 -3.75,2.76 -6.78,2.76s-5.29,-0.92 -6.78,-2.76c-1.49,-1.84 -2.23,-4.05 -2.23,-6.64s0.74,-4.75 2.23,-6.61c1.49,-1.87 3.75,-2.8 6.78,-2.8s5.29,0.93 6.78,2.8ZM184,102.29c-1.35,0 -2.39,0.48 -3.11,1.43 -0.73,0.95 -1.09,2.32 -1.09,4.08s0.36,3.13 1.09,4.09c0.73,0.96 1.77,1.44 3.11,1.44s2.38,-0.48 3.11,-1.44c0.72,-0.96 1.08,-2.32 1.08,-4.09s-0.36,-3.13 -1.08,-4.08c-0.72,-0.95 -1.76,-1.43 -3.11,-1.43Z"
android:fillColor="#fff"/>
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.55"
android:scaleY="0.55"
android:translateX="150.56"
android:translateY="150.56">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#DA4453"/>
<path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#ED5564"/>
<path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#DA4453"/>
<path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.66,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</group>
</vector>

View File

@@ -1,39 +1,52 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="307.57dp"
android:height="278.96dp"
android:viewportWidth="307.57"
android:viewportHeight="278.96">
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M146.46,0h14.65v278.96h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#DA4453"/>
<path
android:pathData="M117.17,21.97h14.65v205.73h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#ED5564"/>
<path android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#DA4453"/>
<path
android:pathData="M87.88,43.94h14.65v65.91h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M0,43.94h14.65v65.91h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M292.92,43.94h14.65v65.91h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M58.58,21.97h14.65v109.85h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M234.34,21.97h14.65v109.85h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M29.29,0h14.65v153.79h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M263.63,0h14.65v153.79h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.67,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M205.05,43.94h14.65v65.91h-14.65z"
android:fillColor="#f24b6a"/>
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M175.75,21.97h14.65v205.73h-14.65z"
android:fillColor="#f24b6a"/>
</vector>
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</vector>

View File

@@ -0,0 +1,51 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="800dp"
android:height="800dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M512,437.33c0,11.78 -9.56,21.34 -21.34,21.34H21.33C9.55,458.67 0,449.11 0,437.33V96c0,-11.78 9.55,-21.33 21.33,-21.33h469.33c11.78,0 21.34,9.55 21.34,21.33L512,437.33L512,437.33z"
android:fillColor="#DA4453"/>
<path
android:pathData="M512,416.01c0,11.78 -9.56,21.31 -21.34,21.31H21.33C9.55,437.33 0,427.8 0,416.01V74.67c0,-11.78 9.55,-21.34 21.33,-21.34h469.33c11.78,0 21.34,9.56 21.34,21.34L512,416.01L512,416.01z"
android:fillColor="#ED5564"/>
<path
android:pathData="M63.99,160c-5.89,0 -10.66,4.78 -10.66,10.67v149.34c0,5.88 4.77,10.66 10.66,10.66c5.89,0 10.67,-4.78 10.67,-10.66V170.67C74.66,164.78 69.88,160 63.99,160z"
android:fillColor="#DA4453"/>
<path
android:pathData="M74.66,106.67c0,5.89 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.77 -10.66,-10.66S58.1,96 63.99,96C69.88,96 74.66,100.78 74.66,106.67z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M74.66,384.01c0,5.88 -4.78,10.66 -10.67,10.66c-5.89,0 -10.66,-4.78 -10.66,-10.66c0,-5.91 4.77,-10.69 10.66,-10.69C69.88,373.33 74.66,378.11 74.66,384.01z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M448,123.73h-21.34v203.19l-40.31,50.41v0.02c-1.47,1.83 -2.34,4.14 -2.34,6.67c0,5.88 4.78,10.66 10.66,10.66c3.38,0 6.38,-1.56 8.33,-4h0.02l42.66,-53.34l0,0c1.47,-1.81 2.34,-4.13 2.34,-6.66V123.73z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,149.33c-11.77,0 -21.33,-9.56 -21.33,-21.33s9.56,-21.33 21.33,-21.33s21.33,9.56 21.33,21.33S449.09,149.33 437.33,149.33z"
android:fillColor="#E6E9ED"/>
<path
android:pathData="M437.33,96c-17.67,0 -32,14.33 -32,32s14.33,32 32,32s32,-14.33 32,-32S455,96 437.33,96zM437.33,138.67c-5.89,0 -10.67,-4.8 -10.67,-10.67c0,-5.88 4.78,-10.67 10.67,-10.67s10.67,4.8 10.67,10.67C448,133.88 443.22,138.67 437.33,138.67z"
android:fillColor="#CCD1D9"/>
<path
android:pathData="M405.33,245.33c0,82.48 -66.86,149.34 -149.33,149.34c-82.47,0 -149.33,-66.86 -149.33,-149.34C106.66,162.86 173.52,96 255.99,96C338.47,96 405.33,162.86 405.33,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M266.66,149.33c0,-5.89 -4.77,-10.66 -10.67,-10.66c-58.91,0 -106.66,47.75 -106.66,106.65l0,0c0,5.89 4.77,10.67 10.66,10.67s10.67,-4.78 10.67,-10.67l0,0c0,-22.78 8.88,-44.22 24.99,-60.33c16.12,-16.13 37.55,-25 60.34,-25C261.89,160 266.66,155.22 266.66,149.33z"
android:fillColor="#656D78"/>
<path
android:pathData="M352,234.67c-5.9,0 -10.67,4.77 -10.67,10.66l0,0c0,22.8 -8.88,44.23 -24.98,60.34c-16.13,16.13 -37.56,25 -60.35,25c-5.89,0 -10.66,4.78 -10.66,10.66c0,5.91 4.77,10.69 10.66,10.69c58.91,0 106.66,-47.77 106.66,-106.69C362.65,239.44 357.89,234.67 352,234.67z"
android:fillColor="#656D78"/>
<path
android:pathData="M255.99,288.01c-23.52,0 -42.66,-19.16 -42.66,-42.69c0,-23.52 19.14,-42.66 42.66,-42.66c23.54,0 42.66,19.14 42.66,42.66C298.65,268.86 279.53,288.01 255.99,288.01z"
android:fillColor="#FFCE54"/>
<path
android:pathData="M255.99,192c-29.45,0 -53.33,23.88 -53.33,53.33s23.88,53.34 53.33,53.34c29.46,0 53.34,-23.89 53.34,-53.34S285.45,192 255.99,192zM255.99,277.34c-17.64,0 -32,-14.36 -32,-32.02c0,-17.64 14.36,-32 32,-32c17.65,0 32.01,14.36 32.01,32C288,262.98 273.64,277.34 255.99,277.34z"
android:fillColor="#F6BB42"/>
<path
android:pathData="M266.66,245.33c0,5.89 -4.77,10.67 -10.67,10.67c-5.89,0 -10.66,-4.78 -10.66,-10.67s4.77,-10.66 10.66,-10.66C261.89,234.67 266.66,239.44 266.66,245.33z"
android:fillColor="#434A54"/>
<path
android:pathData="M74.66,234.67H53.33c-5.89,0 -10.66,4.77 -10.66,10.66s4.77,10.67 10.66,10.67h21.34c5.89,0 10.66,-4.78 10.66,-10.67S80.56,234.67 74.66,234.67z"
android:fillColor="#434A54"/>
</vector>

View File

@@ -1,14 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="288dp"
android:height="288dp"
android:drawable="@android:color/transparent"
android:gravity="center" />
<item
android:width="220dp"
android:height="220dp"
android:drawable="@drawable/ic_splash_logo"
android:gravity="center" />
</layer-list>

View File

@@ -68,6 +68,14 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/song_asset_link_row"
layout="@layout/view_asset_link_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="12dp" />
<LinearLayout
android:id="@+id/option_linear_layout"
android:layout_width="match_parent"
@@ -209,4 +217,4 @@
android:text="@string/song_bottom_sheet_share"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -41,23 +41,40 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/album_list_sort_image_view"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="52dp"
android:layout_height="52dp"
<LinearLayout
android:id="@+id/sort_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_sort_list"
android:orientation="horizontal"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/albumListSortTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:paddingEnd="8dp"
android:visibility="gone" />
<Button
android:id="@+id/album_list_sort_image_view"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_sort_list" />
</LinearLayout>
<ProgressBar
android:id="@+id/album_list_progress_loader"
@@ -71,7 +88,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
@@ -87,4 +103,3 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View File

@@ -57,6 +57,17 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/player_asset_link_row"
layout="@layout/view_asset_link_row"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/player_media_quality_sector" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/player_media_cover_view_pager"
android:layout_width="0dp"
@@ -66,7 +77,7 @@
app:layout_constraintBottom_toTopOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_media_quality_sector" />
app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
@@ -400,4 +411,4 @@
app:srcCompat="@drawable/ic_eq" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.ChipGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/asset_link_chip_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:singleLine="true"
app:selectionRequired="false"
app:singleSelection="false">
<com.google.android.material.chip.Chip
android:id="@+id/asset_link_song_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:clickable="true"
android:ellipsize="end"
android:maxLines="1"
android:text=""
app:chipIcon="@drawable/ic_link"
app:chipIconTint="?attr/colorOnSurfaceVariant"
app:rippleColor="@color/ripple_material_light" />
<com.google.android.material.chip.Chip
android:id="@+id/asset_link_album_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:clickable="true"
android:ellipsize="end"
android:maxLines="1"
android:text=""
app:chipIcon="@drawable/ic_link"
app:chipIconTint="?attr/colorOnSurfaceVariant"
app:rippleColor="@color/ripple_material_light" />
<com.google.android.material.chip.Chip
android:id="@+id/asset_link_artist_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:clickable="true"
android:ellipsize="end"
android:maxLines="1"
android:text=""
app:chipIcon="@drawable/ic_link"
app:chipIconTint="?attr/colorOnSurfaceVariant"
app:rippleColor="@color/ripple_material_light" />
</com.google.android.material.chip.ChipGroup>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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