157 Commits
3.6.0 ... main

Author SHA1 Message Date
CappielloAntonio
9cf62c8c0c gradle: update gradle build tools 2025-01-31 11:05:39 +01:00
CappielloAntonio
330556ec33 Merge remote-tracking branch 'origin/main' 2025-01-31 10:32:53 +01:00
CappielloAntonio
4c8c5ce120 gradle: dependencies update 2025-01-31 10:32:39 +01:00
CappielloAntonio
55ae9a8442 fix: added missing decorators 2025-01-31 10:32:00 +01:00
CappielloAntonio
f8a53c7db2 Update README.md 2024-12-31 16:55:21 +01:00
CappielloAntonio
b58cae1ecd Update README.md 2024-12-31 16:51:37 +01:00
CappielloAntonio
e305f20811 fix: updated workflow 2024-12-31 12:54:28 +01:00
CappielloAntonio
c8c1bcfd3e fix: null checking 2024-12-30 17:23:55 +01:00
CappielloAntonio
e1c96d278f fix: null checking 2024-12-30 17:20:13 +01:00
CappielloAntonio
3783a2f790 fix: escape character 2024-12-30 17:17:05 +01:00
CappielloAntonio
63e288d147 Merge pull request #316 from potatoenergy/main
feature: update ru-RU translation
2024-12-30 16:53:40 +01:00
CappielloAntonio
d3ca43ed49 fix: delete deprecated code to check connectivity 2024-12-30 16:45:38 +01:00
CappielloAntonio
f0d31e425a fix: temporary opt out edge to edge enforcement 2024-12-30 16:45:07 +01:00
CappielloAntonio
609fc70d33 fix: temporary opt out edge to edge enforcement 2024-12-30 16:38:43 +01:00
CappielloAntonio
0b92c40d51 gradle: dependencies update 2024-12-30 16:37:53 +01:00
ponfertato
eb6a2b609e feat: update Russian localization 2024-12-25 18:07:18 +03:00
ponfertato
20ffc960df feat: fix build Russian localization 2024-12-25 17:56:34 +03:00
CappielloAntonio
48d9022f9a feat: add sorting and search functionality for album and artist list 2024-11-23 16:00:01 +01:00
CappielloAntonio
9e6926fc97 feat: add sorting and search functionality for song list 2024-11-22 21:57:27 +01:00
CappielloAntonio
780f1c3a2e feat: implemented additional sorting options for albums in the catalog screen 2024-11-20 23:02:43 +01:00
CappielloAntonio
0f471a7b9f feat: add search functionality for songs within playlists 2024-11-20 22:20:37 +01:00
CappielloAntonio
4ec1519063 feat: add ALAC codec support via Media3 FFmpeg module 2024-11-20 21:23:56 +01:00
CappielloAntonio
618cf23e6e gradle: dependencies update 2024-11-20 15:10:25 +01:00
CappielloAntonio
e8e24354ec fix: change the server address to the backup one if the first address is unreachable 2024-11-06 17:43:18 +01:00
CappielloAntonio
fd0fd0546c gradle: gradle update 2024-11-06 17:39:42 +01:00
CappielloAntonio
030ca82c3a fix: fixed the pull request for the Italian translation 2024-11-06 17:17:17 +01:00
CappielloAntonio
0e6b860e03 Merge remote-tracking branch 'origin/main' 2024-11-06 16:57:57 +01:00
CappielloAntonio
8c288e8938 Merge pull request #299 from SaregoA/main
feat: Added Italian translation
2024-11-06 16:57:17 +01:00
CappielloAntonio
4e968f3397 Merge pull request #286 from skyline75489/skyline/zh-cn-2
feat: update zh-cn translation
2024-11-06 16:55:46 +01:00
CappielloAntonio
b1e0f49ddb gradle: dependencies update 2024-11-06 15:43:49 +01:00
andrea sarego
73a1ab2330 Traduzione in italiano 2024-11-04 20:31:40 +01:00
Chester Liu
8540348670 Update zh-cn translation part 2 2024-08-31 19:07:40 +08:00
CappielloAntonio
67e4079732 fix: null checking 2024-08-30 17:19:58 +02:00
CappielloAntonio
6c6d56f451 Merge pull request #251 from albertcanales/main
feat: open album when clicking the song's title on player
2024-08-30 16:55:00 +02:00
CappielloAntonio
f967df8ef3 Merge remote-tracking branch 'origin/main' 2024-08-30 16:47:37 +02:00
CappielloAntonio
c5a78bf945 fix: strengthened controls on the presence of server address strings (local and remote) 2024-08-30 16:47:15 +02:00
CappielloAntonio
6f51fd92bb style: code cleanup 2024-08-30 15:51:49 +02:00
CappielloAntonio
3c2ea38f1e fix: further eliminate the use of HTML decode in titles and subtitles 2024-08-30 15:27:16 +02:00
CappielloAntonio
edf140fb5d Merge pull request #285 from skyline75489/skyline/zh-cn-1
Update zh-cn translation
2024-08-30 15:16:31 +02:00
Chester Liu
049ce7713a Update zh-cn translation 2024-08-29 20:27:31 +08:00
CappielloAntonio
8c49ceffdb Update privacy.html 2024-08-29 12:07:59 +02:00
CappielloAntonio
052e9d9068 Merge remote-tracking branch 'origin/main' 2024-08-28 14:25:31 +02:00
CappielloAntonio
4d1213c43d gradle: dependencies update 2024-08-28 14:25:19 +02:00
CappielloAntonio
1ec0c7b99c Merge pull request #264 from dnno/main
feat: update german localization
2024-08-22 16:58:13 +02:00
CappielloAntonio
07f8914a9f Merge pull request #261 from florent4014/patch-1
feat: update strings.xml
2024-08-22 16:57:35 +02:00
CappielloAntonio
965a80462c Merge remote-tracking branch 'origin/main' 2024-08-22 16:12:03 +02:00
CappielloAntonio
349c961f1a repo: add play variant 2024-08-22 16:11:56 +02:00
CappielloAntonio
eb9f824c01 gradle: gradle update 2024-08-22 15:20:51 +02:00
CappielloAntonio
e465892013 repo: add privacy policy page 2024-08-21 14:44:38 +02:00
CappielloAntonio
a49c78b9f1 gradle: dependencies update 2024-08-21 14:42:46 +02:00
Ryan Harg
436ef21a29 Update german localization 2024-07-03 10:12:38 +02:00
florent4014
be8decfac3 Update strings.xml
Corrected some minor mistakes
2024-06-24 13:54:29 +02:00
Albert Canales Ros
7c87ec2cbe feat: clicking the song's title opens the album on player 2024-06-08 23:08:39 +02:00
CappielloAntonio
fb7296b467 feat: added the ability to pin playlists to the home screen 2024-06-08 18:53:58 +02:00
CappielloAntonio
078aa87521 feat: added long press to delete gesture 2024-06-08 16:49:04 +02:00
CappielloAntonio
54a4355793 feat: added release date and original release date to album notes, if available 2024-06-02 20:19:18 +02:00
CappielloAntonio
e84f62220c fix: Implemented continuous playing in com.cappielloantonio.notquitemy.tempo 2024-06-02 19:26:32 +02:00
CappielloAntonio
176db09662 feat: Implemented continuous playing 2024-06-02 19:18:16 +02:00
CappielloAntonio
2c3aebc83b feat: Added API call to retrieve AlbumID3 details in album page as the object passed from search and album list are different 2024-06-02 17:07:40 +02:00
CappielloAntonio
a67571ee4f feat: reduced time to wait before trying to connect to local address 2024-06-02 16:21:29 +02:00
CappielloAntonio
79f5b9b158 feat: reduced time to wait before trying to connect to local address 2024-06-02 16:15:20 +02:00
CappielloAntonio
f6b176a357 feat: added the ability for the user to add a local server address and use that address when available 2024-06-01 15:23:40 +02:00
CappielloAntonio
aa5290c7ee style: code cleanup 2024-05-31 22:41:44 +02:00
CappielloAntonio
0a26f0a7b8 feat: implemented version number control and update dialog for Github flavor 2024-05-31 22:41:01 +02:00
CappielloAntonio
c243fa9edc gradle: dependencies update 2024-05-31 20:28:22 +02:00
CappielloAntonio
f94e5892cd feat: edited the interface of top songs divided by week, month, and year 2024-05-26 19:38:17 +02:00
CappielloAntonio
477331da6f feat: added external memory cache option 2024-05-26 14:49:57 +02:00
CappielloAntonio
c4e8fe5261 gradle: gradle update 2024-05-26 12:44:22 +02:00
CappielloAntonio
92fd6b01e4 feat: read hls data source 2024-05-26 11:05:43 +02:00
CappielloAntonio
263d9ebc5f fix: null checking 2024-05-26 01:13:41 +02:00
CappielloAntonio
f6578afb14 fxi: removed readable string rework 2024-05-26 00:04:42 +02:00
CappielloAntonio
41b374ab23 style: delete unused code 2024-05-25 23:43:46 +02:00
CappielloAntonio
4448a632af fix: order downloaded tracks by artist, album, disc_number and finally track number 2024-05-25 23:27:28 +02:00
CappielloAntonio
b3b1c5b006 feat: updated the horizontal song adapter with the disc name information 2024-05-25 22:33:26 +02:00
CappielloAntonio
25900c848a feat: extended the Albums model according to the OpenSubsonic API project indications 2024-05-25 22:31:30 +02:00
CappielloAntonio
08e9be107b fix: setAllowCrossProtocolRedirects to httpDataSourceFactory 2024-05-25 19:12:10 +02:00
CappielloAntonio
6cbb2ee117 fix: set content description to fab 2024-05-25 18:21:19 +02:00
CappielloAntonio
dacaa03eb7 style: code clean up 2024-05-25 17:55:30 +02:00
CappielloAntonio
a3d8b75d07 Merge remote-tracking branch 'origin/main' 2024-05-25 17:26:08 +02:00
CappielloAntonio
d08c113d99 Merge pull request #190 from kmod-midori/streaming-cache
feat: cache streaming contents
2024-05-25 17:25:23 +02:00
CappielloAntonio
2db716a79c gradle: gradle update 2024-05-25 15:35:38 +02:00
CappielloAntonio
71b913be9b Merge remote-tracking branch 'origin/main'
# Conflicts:
#	build.gradle
2024-05-25 15:26:37 +02:00
CappielloAntonio
fb353a33d9 Merge pull request #220 from Sevinfolds/values-ru
feat: add russian localization
2024-05-25 13:08:54 +02:00
CappielloAntonio
240498c219 gradle: dependencies update 2024-05-25 13:07:26 +02:00
Sevinfolds
1eac053d2d Update strings.xml
Edits and clarifications of the translation
2024-04-16 14:36:35 +03:00
Sevinfolds
160222563c Create arrays.xml
Add arrays.xml
2024-04-16 11:26:46 +03:00
Sevinfolds
4ec3d6bde7 Create strings.xml
adding the Russian language
2024-04-16 11:02:47 +03:00
CappielloAntonio
e0f276dd2a fastlane: update fastlane data 2024-03-25 10:00:30 +01:00
CappielloAntonio
acee7f8fa9 gradle: versionName update 2024-03-25 09:57:01 +01:00
CappielloAntonio
499929ad55 gradle: downgrade buildToolsVersion to fix GitHub workflow build error 2024-03-24 19:59:41 +01:00
CappielloAntonio
7b6d2c62a5 style: code cleanup 2024-03-24 19:45:56 +01:00
CappielloAntonio
ff6bf20c30 style: refined the design of favorite and rating indicators 2024-03-24 18:50:05 +01:00
CappielloAntonio
58d540b939 feat: as an option show the item's rating and whether it is marked as a favorite 2024-03-24 00:45:19 +01:00
CappielloAntonio
4b9eaa8c3d style: code cleanup 2024-03-23 22:49:05 +01:00
CappielloAntonio
03700d9e4c fix: removed placeholders causing stuttering during interface loading 2024-03-23 22:41:44 +01:00
CappielloAntonio
374dbb58bb style: code cleanup 2024-03-23 22:40:37 +01:00
CappielloAntonio
a88658ac8f chore: removed unused placeholders 2024-03-23 22:39:51 +01:00
CappielloAntonio
0e97eab744 feat: implemented customizable home, allowing users to toggle visibility of elements and change their order 2024-03-23 21:33:11 +01:00
CappielloAntonio
309eca0764 gradle: dependencies update 2024-03-23 15:51:06 +01:00
CappielloAntonio
fd85f36411 Merge remote-tracking branch 'origin/main' 2024-03-16 17:44:28 +01:00
CappielloAntonio
b4180afa36 refactor: refactored artist page layout for consistent vertical scrolling 2024-03-16 17:44:18 +01:00
CappielloAntonio
2712b73dac feat: added sorting by newest album added 2024-03-16 16:28:33 +01:00
CappielloAntonio
302458e76b feat: added additional information about the album on the dedicated detail page 2024-03-16 15:50:06 +01:00
CappielloAntonio
dd085a2cdb gradle: Media3 dependencies update 2024-03-16 12:57:57 +01:00
CappielloAntonio
7a58ad5494 Merge pull request #187 from chengyuhui/fix-negative-gain
fix: fix negative replay gain values
2024-03-16 12:52:59 +01:00
CappielloAntonio
84234849a4 Merge pull request #188 from chengyuhui/fix-load-control-fdroid
fix: fix load control for F-Droid builds
2024-03-16 12:48:06 +01:00
CappielloAntonio
6f6f596432 gradle: gradle update 2024-03-16 12:39:36 +01:00
Midori Kochiya
3d3d0fa856 Try to cache streaming contents 2024-03-11 17:21:28 +08:00
Midori Kochiya
4ff2ed38c7 Fix load control for F-Droid builds 2024-03-11 15:13:13 +08:00
Midori Kochiya
321994496a Fix negative replay gain values 2024-03-10 19:16:03 +08:00
CappielloAntonio
3e1c3133ca fix: fix crash with TypeToken and reflection 2024-02-19 21:07:47 +01:00
CappielloAntonio
10b9d7ec76 fix: fix "Now Playing" scrobble implementation 2024-02-18 19:35:49 +01:00
CappielloAntonio
1980e53a27 fix: fix scrolling issue in server registration dialog 2024-02-18 19:15:14 +01:00
CappielloAntonio
54e988b70e feat: added "Recent songs" and "Random" menu items in Android Auto 2024-02-18 19:06:58 +01:00
CappielloAntonio
14d6128df0 feat: added optional information about audio quality for horizontal track adapters 2024-02-18 17:21:52 +01:00
CappielloAntonio
7488346804 feat: added optional information about audio quality for horizontal track adapters 2024-02-18 17:14:41 +01:00
CappielloAntonio
733102a8a4 feat: implemented karaoke mode for synchronized lyrics 2024-02-18 16:29:42 +01:00
CappielloAntonio
28fef53590 fix: disable shuffle button if there isn't a top song list 2024-02-17 23:54:50 +01:00
CappielloAntonio
e35aed9cc4 feat: implemented synchronized lyrics display 2024-02-17 23:44:49 +01:00
CappielloAntonio
111a17350b chore: code cleanup 2024-02-17 23:43:17 +01:00
CappielloAntonio
54be869081 feat: implemented new API getLyricsBySongId for retrieving (synced) song lyrics based on song ID 2024-02-17 23:43:02 +01:00
CappielloAntonio
b9462d7374 feat: check and save usable OpenSubsonic APIs 2024-02-17 23:39:25 +01:00
CappielloAntonio
234c9a10d2 Merge pull request #171 from victoralvesf/portuguese-support
feat: add brazilian portuguese localization
2024-02-17 11:37:09 +01:00
CappielloAntonio
817c3b02e5 gradle: dependencies update 2024-02-17 11:34:43 +01:00
Victor Alves
1f65b4c321 feat: add brazilian portuguese localization 2024-02-15 02:16:02 -03:00
CappielloAntonio
ab0e1ead45 Update README.md 2024-02-04 19:00:17 +01:00
antonio
73b368d202 style: code cleanup 2024-01-29 16:42:09 +01:00
antonio
f293a0116b Merge remote-tracking branch 'origin/main' 2024-01-29 12:29:22 +01:00
CappielloAntonio
ff5bec30c0 Update github_release.yml 2024-01-29 12:29:08 +01:00
antonio
5c66bed477 gradle: code version update 2024-01-29 12:18:34 +01:00
antonio
d2068106e4 gradle: standardized code and name version for every flavors, temporarily downgrade com.google.android.material 2024-01-29 11:45:13 +01:00
antonio
b7b02854d5 style: improved readability by modifying settings summary 2024-01-29 10:35:25 +01:00
antonio
0edafc2d8e gradle: updated gradle 2024-01-29 10:33:20 +01:00
antonio
279302737d fix: sort genres alphabetically as server's default sorting could lead to unpredictable results 2024-01-28 23:40:03 +01:00
antonio
634de67d74 feat: added an option to prevent phone from going into sleep mode if in-app 2024-01-28 23:22:03 +01:00
antonio
cd44368d66 feat: save user's layout choice and always use user preference 2024-01-28 23:03:20 +01:00
antonio
ae8aa56602 style: code cleanup 2024-01-28 19:36:25 +01:00
CappielloAntonio
34d6692dae Merge pull request #122 from DelightLane/duration_crash
fix: null checking for song without duration info
2024-01-28 19:35:56 +01:00
antonio
8d2f0edbab fix: variable removed by accident 2024-01-28 19:19:47 +01:00
antonio
d4d7aaba2b style: code cleanup 2024-01-28 19:15:42 +01:00
CappielloAntonio
d690df86d8 Merge pull request #150 from GallowsDove/album-catalogue-fixes
fix: fix loading and filtering issues with AlbumCatalogue
2024-01-28 19:08:35 +01:00
antonio
e8f3cdbb48 style: code cleanup 2024-01-28 18:30:50 +01:00
CappielloAntonio
33aa38e885 Merge pull request #145 from GallowsDove/new-download-fix
fix: fix new download caching
2024-01-28 18:24:43 +01:00
antonio
2faba71df0 feat: Implemented shuffle feature for downloaded songs based on the set filter 2024-01-28 18:02:48 +01:00
antonio
1d3a32be5d fix: refined scrobbling logic for the NowPlaying feature across all flavors 2024-01-28 15:46:36 +01:00
antonio
5b8e7d1404 style: code cleanup 2024-01-28 15:45:02 +01:00
antonio
85a5d01e72 style: code cleanup 2024-01-28 15:44:55 +01:00
CappielloAntonio
c7b17f2214 Merge pull request #155 from caiocotts/main
feat: send "now playing" scrobbles to server
2024-01-28 15:19:10 +01:00
caiocotts
d8c8a783de Send "now playing" scrobbles to server. 2024-01-22 21:41:54 -05:00
CappielloAntonio
293b0f71c8 Merge pull request #137 from dnno/update-german-localization
feat: Update German localization
2024-01-21 18:54:05 +01:00
antonio
e6e0e399e0 gradle: dependencies update 2024-01-21 18:51:57 +01:00
GallowsDove
3223de5d03 fix: Remove duplicated line in AlbumCatalogueViewModel 2024-01-18 00:06:37 +01:00
GallowsDove
c6d08d6a3f fix: Fix issues with AlbumCatalogue 2024-01-17 19:21:16 +01:00
GallowsDove
375501f282 fix: Fix new download caching 2024-01-16 19:58:45 +01:00
Reinhard Prechtl
0a62f0d81e Upgrade german locale 2024-01-11 11:58:52 +01:00
DelightLane
12f09b2201 fix: null checking for song without duration info 2023-12-21 11:43:22 +09:00
CappielloAntonio
0c2b18326e gradle: build:gradle update 2023-12-10 16:32:40 +01:00
213 changed files with 10071 additions and 1803 deletions

View File

@@ -28,6 +28,13 @@ jobs:
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Setup build tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- name: Build APK
id: build
run: bash ./gradlew assembleTempoRelease
@@ -41,41 +48,15 @@ jobs:
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Make artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: app-release-signed
path: ${{steps.sign_apk.outputs.signedReleaseFile}}
# - name: Build AAB
# run: bash ./gradlew bundleRelease
# - name: Sign AAB
# id: sign_aab
# uses: r0adkll/sign-android-release@v1
# with:
# releaseDirectory: app/build/outputs/bundle/release
# signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
# alias: ${{ secrets.KEY_ALIAS_GITHUB }}
# keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
# keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
# - name: Make artifact
# uses: actions/upload-artifact@v2
# with:
# name: app-release-signed
# path: ${{steps.sign_aab.outputs.signedReleaseFile}}
# - name: Build Changelog
# id: changelog
# uses: ardalanamini/auto-changelog@v3
# with:
# mention-authors: false
# mention-new-contributors: false
# include-compare: false
# semver: false
- name: Create Release
id: create_release
uses: actions/create-release@v1
@@ -83,7 +64,6 @@ jobs:
tag_name: ${{ github.ref }}
release_name: Release v${{ github.ref }}
body: '> Changelog coming soon'
# body: ${{ steps.changelog.outputs.changelog }}
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -96,13 +76,3 @@ jobs:
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
asset_name: app-tempo-release.apk
asset_content_type: application/zip
# - name: Upload AAB
# uses: actions/upload-release-asset@v1
# env:
# GITHUB_TOKEN: ${{ github.token }}
# with:
# upload_url: ${{ steps.create_release.outputs.upload_url }}
# asset_path: ${{steps.sign_aab.outputs.signedReleaseFile}}
# asset_name: app-release.aab
# asset_content_type: application/zip

2
.idea/compiler.xml generated
View File

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

6
.idea/gradle.xml generated
View File

@@ -4,16 +4,16 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="17" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

3
.idea/misc.xml generated
View File

@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
@@ -191,7 +192,7 @@
</map>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@@ -6,12 +6,22 @@
<b>Access your music library on all your android devices</b>
</p>
<p align="center">
<a href="https://github.com/CappielloAntonio/tempo/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
</p>
<p align="center">
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
<a href="https://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.
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.
**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.**
**Use the Github version of the app for full Android Auto and Chromecast support.**
## Features
- **Subsonic Integration**: Tempo 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.
@@ -23,6 +33,7 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
- **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.
- **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.
<p align="center">
<img src="mockup/feat/1_screenshot.png" width=200>

View File

@@ -3,12 +3,15 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
android {
compileSdk = 34
buildToolsVersion = "34.0.0"
compileSdk 35
buildToolsVersion = '35.0.0'
defaultConfig {
minSdkVersion 24
targetSdkVersion 34
targetSdk 35
versionCode 26
versionName '3.9.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -28,15 +31,16 @@ android {
tempo {
dimension = "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 23
versionName '3.6.0'
}
notquitemy {
dimension = "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
versionCode 1
versionName "1.0.0"
}
play {
dimension = "default"
applicationId "com.cappielloantonio.play.tempo"
}
}
@@ -60,44 +64,47 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
namespace 'com.cappielloantonio.tempo'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation files('../libs/lib-decoder-ffmpeg-release.aar')
// AndroidX
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.8.6'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.appcompat:appcompat:1.7.0'
// Android Material
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.android.material:material:1.10.0'
// Glide
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'com.github.bumptech.glide:annotations:4.16.0'
// Media3
implementation 'androidx.media3:media3-session:1.2.0'
implementation 'androidx.media3:media3-common:1.2.0'
implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation 'androidx.media3:media3-ui:1.2.0'
tempoImplementation 'androidx.media3:media3-cast:1.2.0'
implementation 'androidx.media3:media3-session:1.5.1'
implementation 'androidx.media3:media3-common:1.5.1'
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'
playImplementation 'androidx.media3:media3-cast:1.5.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
}

View File

@@ -22,4 +22,7 @@
-keepattributes SourceFile, LineNumberTable
-keep public class * extends java.lang.Exception
-keep class retrofit2.** { *; }
-keep class retrofit2.** { *; }
-keep class **.reflect.TypeToken { *; }
-keep class * extends **.reflect.TypeToken

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,10 @@ import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import com.cappielloantonio.tempo.github.Github;
import com.cappielloantonio.tempo.helper.ThemeHelper;
import com.cappielloantonio.tempo.subsonic.Subsonic;
import com.cappielloantonio.tempo.subsonic.SubsonicPreferences;
@@ -15,6 +17,7 @@ public class App extends Application {
private static App instance;
private static Context context;
private static Subsonic subsonic;
private static Github github;
private static SharedPreferences preferences;
@Override
@@ -53,6 +56,13 @@ public class App extends Application {
return subsonic;
}
public static Github getGithubClientInstance() {
if (github == null) {
github = new Github();
}
return github;
}
public SharedPreferences getPreferences() {
if (preferences == null) {
preferences = PreferenceManager.getDefaultSharedPreferences(context);
@@ -61,18 +71,12 @@ public class App extends Application {
return preferences;
}
private static Subsonic getSubsonicClient() {
String server = Preferences.getServer();
String username = Preferences.getUser();
String password = Preferences.getPassword();
String token = Preferences.getToken();
String salt = Preferences.getSalt();
boolean isLowSecurity = Preferences.isLowScurity();
public static void refreshSubsonicClient() {
subsonic = getSubsonicClient();
}
SubsonicPreferences preferences = new SubsonicPreferences();
preferences.setServerUrl(server);
preferences.setUsername(username);
preferences.setAuthentication(password, token, salt, isLowSecurity);
private static Subsonic getSubsonicClient() {
SubsonicPreferences preferences = getSubsonicPreferences();
if (preferences.getAuthentication() != null) {
if (preferences.getAuthentication().getPassword() != null)
@@ -85,4 +89,21 @@ public class App extends Application {
return new Subsonic(preferences);
}
@NonNull
private static SubsonicPreferences getSubsonicPreferences() {
String server = Preferences.getInUseServerAddress();
String username = Preferences.getUser();
String password = Preferences.getPassword();
String token = Preferences.getToken();
String salt = Preferences.getSalt();
boolean isLowSecurity = Preferences.isLowScurity();
SubsonicPreferences preferences = new SubsonicPreferences();
preferences.setServerUrl(server);
preferences.setUsername(username);
preferences.setAuthentication(password, token, salt, isLowSecurity);
return preferences;
}
}

View File

@@ -1,5 +1,6 @@
package com.cappielloantonio.tempo.database;
import androidx.media3.common.util.UnstableApi;
import androidx.room.AutoMigration;
import androidx.room.Database;
import androidx.room.Room;
@@ -11,6 +12,7 @@ import com.cappielloantonio.tempo.database.converter.DateConverters;
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
import com.cappielloantonio.tempo.database.dao.DownloadDao;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
import com.cappielloantonio.tempo.database.dao.QueueDao;
import com.cappielloantonio.tempo.database.dao.RecentSearchDao;
import com.cappielloantonio.tempo.database.dao.ServerDao;
@@ -22,11 +24,13 @@ import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.RecentSearch;
import com.cappielloantonio.tempo.model.Server;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import com.cappielloantonio.tempo.subsonic.models.Playlist;
@UnstableApi
@Database(
version = 8,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class},
autoMigrations = {@AutoMigration(from = 7, to = 8)}
version = 10,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class, SessionMediaItem.class, Playlist.class},
autoMigrations = {@AutoMigration(from = 9, to = 10)}
)
@TypeConverters({DateConverters.class})
public abstract class AppDatabase extends RoomDatabase {
@@ -56,4 +60,6 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract FavoriteDao favoriteDao();
public abstract SessionMediaItemDao sessionMediaItemDao();
public abstract PlaylistDao playlistDao();
}

View File

@@ -12,7 +12,10 @@ import java.util.List;
@Dao
public interface ChronologyDao {
@Query("SELECT * FROM chronology WHERE timestamp >= :startDate AND timestamp < :endDate AND server == :server GROUP BY id ORDER BY COUNT(id) DESC LIMIT 9")
@Query("SELECT * FROM chronology WHERE server == :server GROUP BY id ORDER BY timestamp DESC LIMIT :count")
LiveData<List<Chronology>> getLastPlayed(String server, int count);
@Query("SELECT * FROM chronology WHERE timestamp >= :endDate AND timestamp < :startDate AND server == :server GROUP BY id ORDER BY COUNT(id) DESC LIMIT 20")
LiveData<List<Chronology>> getAllFrom(long startDate, long endDate, String server);
@Insert(onConflict = OnConflictStrategy.REPLACE)

View File

@@ -12,7 +12,7 @@ import java.util.List;
@Dao
public interface DownloadDao {
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, track ASC")
@Query("SELECT * FROM download WHERE download_state = 1 ORDER BY artist, album, disc_number, track ASC")
LiveData<List<Download>> getAll();
@Query("SELECT * FROM download WHERE id = :id")

View File

@@ -0,0 +1,29 @@
package com.cappielloantonio.tempo.github;
import com.cappielloantonio.tempo.github.api.release.ReleaseClient;
public class Github {
private static final String OWNER = "CappielloAntonio";
private static final String REPO = "Tempo";
private ReleaseClient releaseClient;
public ReleaseClient getReleaseClient() {
if (releaseClient == null) {
releaseClient = new ReleaseClient(this);
}
return releaseClient;
}
public String getUrl() {
return "https://api.github.com/";
}
public static String getOwner() {
return OWNER;
}
public static String getRepo() {
return REPO;
}
}

View File

@@ -0,0 +1,30 @@
package com.cappielloantonio.tempo.github
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class GithubRetrofitClient(github: Github) {
var retrofit: Retrofit
init {
retrofit = Retrofit.Builder()
.baseUrl(github.url)
.addConverterFactory(GsonConverterFactory.create())
.client(getOkHttpClient())
.build()
}
private fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(getHttpLoggingInterceptor())
.build()
}
private fun getHttpLoggingInterceptor(): HttpLoggingInterceptor {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
return loggingInterceptor
}
}

View File

@@ -0,0 +1,24 @@
package com.cappielloantonio.tempo.github.api.release;
import android.util.Log;
import com.cappielloantonio.tempo.github.Github;
import com.cappielloantonio.tempo.github.GithubRetrofitClient;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import retrofit2.Call;
public class ReleaseClient {
private static final String TAG = "ReleaseClient";
private final ReleaseService releaseService;
public ReleaseClient(Github github) {
this.releaseService = new GithubRetrofitClient(github).getRetrofit().create(ReleaseService.class);
}
public Call<LatestRelease> getLatestRelease() {
Log.d(TAG, "getLatestRelease()");
return releaseService.getLatestRelease(Github.getOwner(), Github.getRepo());
}
}

View File

@@ -0,0 +1,12 @@
package com.cappielloantonio.tempo.github.api.release;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
public interface ReleaseService {
@GET("repos/{owner}/{repo}/releases/latest")
Call<LatestRelease> getLatestRelease(@Path("owner") String owner, @Path("repo") String repo);
}

View File

@@ -0,0 +1,34 @@
package com.cappielloantonio.tempo.github.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class Assets(
@SerializedName("url")
var url: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("name")
var name: String? = null,
@SerializedName("label")
var label: String? = null,
@SerializedName("uploader")
var uploader: Uploader? = Uploader(),
@SerializedName("content_type")
var contentType: String? = null,
@SerializedName("state")
var state: String? = null,
@SerializedName("size")
var size: Int? = null,
@SerializedName("download_count")
var downloadCount: Int? = null,
@SerializedName("created_at")
var createdAt: String? = null,
@SerializedName("updated_at")
var updatedAt: String? = null,
@SerializedName("browser_download_url")
var browserDownloadUrl: String? = null
)

View File

@@ -0,0 +1,44 @@
package com.cappielloantonio.tempo.github.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class Author(
@SerializedName("login")
var login: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("avatar_url")
var avatarUrl: String? = null,
@SerializedName("gravatar_id")
var gravatarId: String? = null,
@SerializedName("url")
var url: String? = null,
@SerializedName("html_url")
var htmlUrl: String? = null,
@SerializedName("followers_url")
var followersUrl: String? = null,
@SerializedName("following_url")
var followingUrl: String? = null,
@SerializedName("gists_url")
var gistsUrl: String? = null,
@SerializedName("starred_url")
var starredUrl: String? = null,
@SerializedName("subscriptions_url")
var subscriptionsUrl: String? = null,
@SerializedName("organizations_url")
var organizationsUrl: String? = null,
@SerializedName("repos_url")
var reposUrl: String? = null,
@SerializedName("events_url")
var eventsUrl: String? = null,
@SerializedName("received_events_url")
var receivedEventsUrl: String? = null,
@SerializedName("type")
var type: String? = null,
@SerializedName("site_admin")
var siteAdmin: Boolean? = null
)

View File

@@ -0,0 +1,46 @@
package com.cappielloantonio.tempo.github.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class LatestRelease(
@SerializedName("url")
var url: String? = null,
@SerializedName("assets_url")
var assetsUrl: String? = null,
@SerializedName("upload_url")
var uploadUrl: String? = null,
@SerializedName("html_url")
var htmlUrl: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("author")
var author: Author? = Author(),
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("tag_name")
var tagName: String? = null,
@SerializedName("target_commitish")
var targetCommitish: String? = null,
@SerializedName("name")
var name: String? = null,
@SerializedName("draft")
var draft: Boolean? = null,
@SerializedName("prerelease")
var prerelease: Boolean? = null,
@SerializedName("created_at")
var createdAt: String? = null,
@SerializedName("published_at")
var publishedAt: String? = null,
@SerializedName("assets")
var assets: ArrayList<Assets> = arrayListOf(),
@SerializedName("tarball_url")
var tarballUrl: String? = null,
@SerializedName("zipball_url")
var zipballUrl: String? = null,
@SerializedName("body")
var body: String? = null,
@SerializedName("reactions")
var reactions: Reactions? = Reactions()
)

View File

@@ -0,0 +1,28 @@
package com.cappielloantonio.tempo.github.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class Reactions(
@SerializedName("url")
var url: String? = null,
@SerializedName("total_count")
var totalCount: Int? = null,
@SerializedName("+1")
var like: Int? = null,
@SerializedName("-1")
var dislike: Int? = null,
@SerializedName("laugh")
var laugh: Int? = null,
@SerializedName("hooray")
var hooray: Int? = null,
@SerializedName("confused")
var confused: Int? = null,
@SerializedName("heart")
var heart: Int? = null,
@SerializedName("rocket")
var rocket: Int? = null,
@SerializedName("eyes")
var eyes: Int? = null
)

View File

@@ -0,0 +1,44 @@
package com.cappielloantonio.tempo.github.models
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class Uploader(
@SerializedName("login")
var login: String? = null,
@SerializedName("id")
var id: Int? = null,
@SerializedName("node_id")
var nodeId: String? = null,
@SerializedName("avatar_url")
var avatarUrl: String? = null,
@SerializedName("gravatar_id")
var gravatarId: String? = null,
@SerializedName("url")
var url: String? = null,
@SerializedName("html_url")
var htmlUrl: String? = null,
@SerializedName("followers_url")
var followersUrl: String? = null,
@SerializedName("following_url")
var followingUrl: String? = null,
@SerializedName("gists_url")
var gistsUrl: String? = null,
@SerializedName("starred_url")
var starredUrl: String? = null,
@SerializedName("subscriptions_url")
var subscriptionsUrl: String? = null,
@SerializedName("organizations_url")
var organizationsUrl: String? = null,
@SerializedName("repos_url")
var reposUrl: String? = null,
@SerializedName("events_url")
var eventsUrl: String? = null,
@SerializedName("received_events_url")
var receivedEventsUrl: String? = null,
@SerializedName("type")
var type: String? = null,
@SerializedName("site_admin")
var siteAdmin: Boolean? = null
)

View File

@@ -0,0 +1,31 @@
package com.cappielloantonio.tempo.github.utils;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.github.models.LatestRelease;
public class UpdateUtil {
public static boolean showUpdateDialog(LatestRelease release) {
if (release.getTagName() == null) return false;
try {
String[] local = BuildConfig.VERSION_NAME.split("\\.");
String[] remote = release.getTagName().split("\\.");
for (int i = 0; i < local.length; i++) {
int localPart = Integer.parseInt(local[i]);
int remotePart = Integer.parseInt(remote[i]);
if (localPart > remotePart) {
return false;
} else if (localPart < remotePart) {
return true;
}
}
} catch (Exception exception) {
return false;
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
package com.cappielloantonio.tempo.model
import androidx.annotation.Keep
@Keep
data class HomeSector(
val id: String,
val sectorTitle: String,
var isVisible: Boolean,
val order: Int,
)

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.model
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.annotation.Nullable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@@ -27,6 +28,9 @@ data class Server(
@ColumnInfo(name = "address")
val address: String,
@ColumnInfo(name = "local_address")
val localAddress: String?,
@ColumnInfo(name = "timestamp")
val timestamp: Long,

View File

@@ -236,12 +236,12 @@ class SessionMediaItem() {
.setMediaId(id!!)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(title))
.setTitle(title)
.setTrackNumber(track ?: 0)
.setDiscNumber(discNumber ?: 0)
.setReleaseYear(year ?: 0)
.setAlbumTitle(MusicUtil.getReadableString(album))
.setArtist(MusicUtil.getReadableString(artist))
.setAlbumTitle(album)
.setArtist(artist)
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)

View File

@@ -8,6 +8,7 @@ import com.cappielloantonio.tempo.interfaces.DecadesCallback;
import com.cappielloantonio.tempo.interfaces.MediaCallback;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.AlbumInfo;
import com.cappielloantonio.tempo.subsonic.models.Child;
import java.util.ArrayList;
@@ -131,9 +132,10 @@ 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().getArtist() != null) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getArtist() != null && response.body().getSubsonicResponse().getArtist().getAlbums() != null) {
List<AlbumID3> albums = response.body().getSubsonicResponse().getArtist().getAlbums();
albums.sort(Comparator.comparing(AlbumID3::getYear));
Collections.reverse(albums);
artistsAlbum.setValue(albums);
}
}
@@ -170,6 +172,29 @@ public class AlbumRepository {
return album;
}
public MutableLiveData<AlbumInfo> getAlbumInfo(String id) {
MutableLiveData<AlbumInfo> albumInfo = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getAlbumInfo2(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().getAlbumInfo() != null) {
albumInfo.setValue(response.body().getSubsonicResponse().getAlbumInfo());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return albumInfo;
}
public void getInstantMix(AlbumID3 album, int count, MediaCallback callback) {
App.getSubsonicClientInstance(false)
.getBrowsingClient()
@@ -250,7 +275,7 @@ public class AlbumRepository {
@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.body().getSubsonicResponse().getAlbumList2().getAlbums().size() > 0 && !response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty()) {
if (!response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty() && !response.body().getSubsonicResponse().getAlbumList2().getAlbums().isEmpty()) {
callback.onLoadYear(response.body().getSubsonicResponse().getAlbumList2().getAlbums().get(0).getYear());
} else {
callback.onLoadYear(-1);

View File

@@ -2,9 +2,13 @@ package com.cappielloantonio.tempo.repository;
import android.net.Uri;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.util.UnstableApi;
@@ -12,9 +16,13 @@ import androidx.media3.session.LibraryResult;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.ChronologyDao;
import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.Artist;
@@ -26,6 +34,7 @@ import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
import com.cappielloantonio.tempo.subsonic.models.MusicFolder;
import com.cappielloantonio.tempo.subsonic.models.Playlist;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
@@ -44,6 +53,7 @@ import retrofit2.Response;
public class AutomotiveRepository {
private final SessionMediaItemDao sessionMediaItemDao = AppDatabase.getInstance().sessionMediaItemDao();
private final ChronologyDao chronologyDao = AppDatabase.getInstance().chronologyDao();
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getAlbums(String prefix, String type, int size) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
@@ -132,6 +142,66 @@ public class AutomotiveRepository {
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getRandomSongs(int count) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
App.getSubsonicClientInstance(false)
.getAlbumSongListClient()
.getRandomSongs(100, null, null)
.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().getRandomSongs() != null && response.body().getSubsonicResponse().getRandomSongs().getSongs() != null) {
List<Child> songs = response.body().getSubsonicResponse().getRandomSongs().getSongs();
setChildrenMetadata(songs);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(songs);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
listenableFuture.setException(t);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getRecentlyPlayedSongs(String server, int count) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();
chronologyDao.getLastPlayed(server, count).observeForever(new Observer<List<Chronology>>() {
@Override
public void onChanged(List<Chronology> chronology) {
if (chronology != null && !chronology.isEmpty()) {
List<Child> songs = new ArrayList<>(chronology);
setChildrenMetadata(songs);
List<MediaItem> mediaItems = MappingUtil.mapMediaItems(songs);
LibraryResult<ImmutableList<MediaItem>> libraryResult = LibraryResult.ofItemList(ImmutableList.copyOf(mediaItems), null);
listenableFuture.set(libraryResult);
} else {
listenableFuture.set(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
}
chronologyDao.getLastPlayed(server, count).removeObserver(this);
}
});
return listenableFuture;
}
public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> getStarredAlbums(String prefix) {
final SettableFuture<LibraryResult<ImmutableList<MediaItem>>> listenableFuture = SettableFuture.create();

View File

@@ -12,28 +12,8 @@ import java.util.List;
public class ChronologyRepository {
private final ChronologyDao chronologyDao = AppDatabase.getInstance().chronologyDao();
public LiveData<List<Chronology>> getThisWeek(String server) {
Calendar calendar = Calendar.getInstance();
Calendar first = (Calendar) calendar.clone();
first.add(Calendar.DAY_OF_WEEK, first.getFirstDayOfWeek() - first.get(Calendar.DAY_OF_WEEK));
Calendar last = (Calendar) first.clone();
last.add(Calendar.DAY_OF_YEAR, 6);
return chronologyDao.getAllFrom(first.getTime().getTime(), last.getTime().getTime(), server);
}
public LiveData<List<Chronology>> getLastWeek(String server) {
Calendar calendar = Calendar.getInstance();
Calendar first = (Calendar) calendar.clone();
first.add(Calendar.DAY_OF_WEEK, first.getFirstDayOfWeek() - first.get(Calendar.DAY_OF_WEEK) - 6);
Calendar last = (Calendar) first.clone();
last.add(Calendar.DAY_OF_YEAR, 6);
return chronologyDao.getAllFrom(first.getTime().getTime(), last.getTime().getTime(), server);
public LiveData<List<Chronology>> getChronology(String server, long start, long end) {
return chronologyDao.getAllFrom(start, end, server);
}
public void insert(Chronology item) {

View File

@@ -8,7 +8,9 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Genre;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import retrofit2.Call;
import retrofit2.Callback;
@@ -39,7 +41,7 @@ public class GenreRepository {
if (size != -1) {
genres.setValue(genreList.subList(0, Math.min(size, genreList.size())));
} else {
genres.setValue(genreList);
genres.setValue(genreList.stream().sorted(Comparator.comparing(Genre::getGenre)).collect(Collectors.toList()));
}
}
}

View File

@@ -0,0 +1,37 @@
package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class OpenRepository {
public MutableLiveData<LyricsList> getLyricsBySongId(String id) {
MutableLiveData<LyricsList> lyricsList = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getOpenClient()
.getLyricsBySongId(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().getLyricsList() != null) {
lyricsList.setValue(response.body().getSubsonicResponse().getLyricsList());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return lyricsList;
}
}

View File

@@ -1,9 +1,12 @@
package com.cappielloantonio.tempo.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.PlaylistDao;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.Playlist;
@@ -17,6 +20,7 @@ import retrofit2.Callback;
import retrofit2.Response;
public class PlaylistRepository {
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
@@ -153,4 +157,50 @@ public class PlaylistRepository {
}
});
}
public LiveData<List<Playlist>> getPinnedPlaylists() {
return playlistDao.getAll();
}
public void insert(Playlist playlist) {
InsertThreadSafe insert = new InsertThreadSafe(playlistDao, playlist);
Thread thread = new Thread(insert);
thread.start();
}
public void delete(Playlist playlist) {
DeleteThreadSafe delete = new DeleteThreadSafe(playlistDao, playlist);
Thread thread = new Thread(delete);
thread.start();
}
private static class InsertThreadSafe implements Runnable {
private final PlaylistDao playlistDao;
private final Playlist playlist;
public InsertThreadSafe(PlaylistDao playlistDao, Playlist playlist) {
this.playlistDao = playlistDao;
this.playlist = playlist;
}
@Override
public void run() {
playlistDao.insert(playlist);
}
}
private static class DeleteThreadSafe implements Runnable {
private final PlaylistDao playlistDao;
private final Playlist playlist;
public DeleteThreadSafe(PlaylistDao playlistDao, Playlist playlist) {
this.playlistDao = playlistDao;
this.playlist = playlist;
}
@Override
public void run() {
playlistDao.delete(playlist);
}
}
}

View File

@@ -1,13 +1,9 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Child;
@@ -54,12 +50,12 @@ public class SongRepository {
return starredSongs;
}
public MutableLiveData<List<Child>> getInstantMix(Child song, int count) {
public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(song.getId(), count)
.getSimilarSongs2(id, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
@@ -104,10 +100,10 @@ public class SongRepository {
return randomSongsSample;
}
public void scrobble(String id) {
public void scrobble(String id, boolean submission) {
App.getSubsonicClientInstance(false)
.getMediaAnnotationClient()
.scrobble(id)
.scrobble(id, submission)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {

View File

@@ -1,12 +1,19 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import com.cappielloantonio.tempo.interfaces.SystemCallback;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension;
import com.cappielloantonio.tempo.subsonic.models.ResponseStatus;
import com.cappielloantonio.tempo.subsonic.models.SubsonicResponse;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
@@ -43,8 +50,8 @@ public class SystemRepository {
});
}
public MutableLiveData<Boolean> ping() {
MutableLiveData<Boolean> pingResult = new MutableLiveData<>();
public MutableLiveData<SubsonicResponse> ping() {
MutableLiveData<SubsonicResponse> pingResult = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getSystemClient()
@@ -53,16 +60,64 @@ public class SystemRepository {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
pingResult.postValue(true);
pingResult.postValue(response.body().getSubsonicResponse());
} else {
pingResult.postValue(null);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
pingResult.postValue(false);
pingResult.postValue(null);
}
});
return pingResult;
}
public MutableLiveData<List<OpenSubsonicExtension>> getOpenSubsonicExtensions() {
MutableLiveData<List<OpenSubsonicExtension>> extensionsResult = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getSystemClient()
.getOpenSubsonicExtensions()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
extensionsResult.postValue(response.body().getSubsonicResponse().getOpenSubsonicExtensions());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
extensionsResult.postValue(null);
}
});
return extensionsResult;
}
public MutableLiveData<LatestRelease> checkTempoUpdate() {
MutableLiveData<LatestRelease> latestRelease = new MutableLiveData<>();
App.getGithubClientInstance()
.getReleaseClient()
.getLatestRelease()
.enqueue(new Callback<LatestRelease>() {
@Override
public void onResponse(@NonNull Call<LatestRelease> call, @NonNull Response<LatestRelease> response) {
if (response.isSuccessful() && response.body() != null) {
latestRelease.postValue(response.body());
}
}
@Override
public void onFailure(@NonNull Call<LatestRelease> call, @NonNull Throwable t) {
latestRelease.postValue(null);
}
});
return latestRelease;
}
}

View File

@@ -31,9 +31,10 @@ public class DownloaderManager {
private final Context context;
private final DataSource.Factory dataSourceFactory;
private final HashMap<String, Download> downloads;
private final DownloadIndex downloadIndex;
private static HashMap<String, Download> downloads;
public DownloaderManager(Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
this.context = context.getApplicationContext();
this.dataSourceFactory = dataSourceFactory;
@@ -61,19 +62,11 @@ public class DownloaderManager {
}
public boolean isDownloaded(MediaItem mediaItem) {
@Nullable Download download = downloads.get(mediaItem.mediaId);
return download != null && download.state != Download.STATE_FAILED;
return isDownloaded(mediaItem.mediaId);
}
public boolean areDownloaded(List<MediaItem> mediaItems) {
for (MediaItem mediaItem : mediaItems) {
@Nullable Download download = downloads.get(mediaItem.mediaId);
if (download != null && download.state != Download.STATE_FAILED) {
return true;
}
}
return false;
return mediaItems.stream().anyMatch(this::isDownloaded);
}
public void download(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) {
@@ -92,6 +85,7 @@ public class DownloaderManager {
public void remove(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) {
DownloadService.sendRemoveDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem).id, false);
deleteDatabase(download.getId());
downloads.remove(download.getId());
}
public void remove(List<MediaItem> mediaItems, List<com.cappielloantonio.tempo.model.Download> downloads) {
@@ -122,23 +116,33 @@ public class DownloaderManager {
return download != null ? download.getTitle() : null;
}
public static void updateRequestDownload(Download download) {
updateDatabase(download.request.id);
downloads.put(download.request.id, download);
}
public static void removeRequestDownload(Download download) {
deleteDatabase(download.request.id);
downloads.remove(download.request.id);
}
private static DownloadRepository getDownloadRepository() {
return new DownloadRepository();
}
public static void insertDatabase(com.cappielloantonio.tempo.model.Download download) {
private static void insertDatabase(com.cappielloantonio.tempo.model.Download download) {
getDownloadRepository().insert(download);
}
public static void deleteDatabase(String id) {
private static void deleteDatabase(String id) {
getDownloadRepository().delete(id);
}
public static void deleteAllDatabase() {
private static void deleteAllDatabase() {
getDownloadRepository().deleteAll();
}
public static void updateDatabase(String id) {
private static void updateDatabase(String id) {
getDownloadRepository().update(id);
}
}
}

View File

@@ -95,7 +95,7 @@ public class DownloaderService extends androidx.media3.exoplayer.offline.Downloa
notification = notificationHelper.buildDownloadCompletedNotification(context, R.drawable.ic_check_circle, null, DownloaderManager.getDownloadNotificationMessage(download.request.id));
notification = Notification.Builder.recoverBuilder(context, notification).setGroup(DownloadUtil.DOWNLOAD_NOTIFICATION_SUCCESSFUL_GROUP).build();
NotificationUtil.setNotification(this.context, successfulDownloadGroupNotificationId, successfulDownloadGroupNotification);
DownloaderManager.updateDatabase(download.request.id);
DownloaderManager.updateRequestDownload(download);
} else if (download.state == Download.STATE_FAILED) {
notification = notificationHelper.buildDownloadFailedNotification(context, R.drawable.ic_error, null, DownloaderManager.getDownloadNotificationMessage(download.request.id));
notification = Notification.Builder.recoverBuilder(context, notification).setGroup(DownloadUtil.DOWNLOAD_NOTIFICATION_FAILED_GROUP).build();
@@ -109,7 +109,7 @@ public class DownloaderService extends androidx.media3.exoplayer.offline.Downloa
@Override
public void onDownloadRemoved(@NonNull DownloadManager downloadManager, Download download) {
DownloaderManager.deleteDatabase(download.request.id);
DownloaderManager.removeRequestDownload(download);
}
}
}

View File

@@ -1,8 +1,16 @@
package com.cappielloantonio.tempo.service;
import androidx.media3.common.MediaItem;
import androidx.media3.session.MediaBrowser;
import android.content.ComponentName;
import androidx.annotation.OptIn;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.repository.ChronologyRepository;
@@ -293,9 +301,33 @@ public class MediaManager {
getQueueRepository().setPlayingPausedTimestamp(mediaItem.mediaId, ms);
}
public static void scrobble(MediaItem mediaItem) {
public static void scrobble(MediaItem mediaItem, boolean submission) {
if (mediaItem != null && Preferences.isScrobblingEnabled()) {
getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"));
getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"), submission);
}
}
@OptIn(markerClass = UnstableApi.class)
public static void continuousPlay(MediaItem mediaItem) {
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media != null) {
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
App.getContext(),
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))
).buildAsync();
enqueue(mediaBrowserListenableFuture, media, true);
}
instantMix.removeObserver(this);
}
});
}
}

View File

@@ -7,6 +7,7 @@ import com.cappielloantonio.tempo.subsonic.api.internetradio.InternetRadioClient
import com.cappielloantonio.tempo.subsonic.api.mediaannotation.MediaAnnotationClient;
import com.cappielloantonio.tempo.subsonic.api.medialibraryscanning.MediaLibraryScanningClient;
import com.cappielloantonio.tempo.subsonic.api.mediaretrieval.MediaRetrievalClient;
import com.cappielloantonio.tempo.subsonic.api.open.OpenClient;
import com.cappielloantonio.tempo.subsonic.api.playlist.PlaylistClient;
import com.cappielloantonio.tempo.subsonic.api.podcast.PodcastClient;
import com.cappielloantonio.tempo.subsonic.api.searching.SearchingClient;
@@ -35,6 +36,7 @@ public class Subsonic {
private BookmarksClient bookmarksClient;
private InternetRadioClient internetRadioClient;
private SharingClient sharingClient;
private OpenClient openClient;
public Subsonic(SubsonicPreferences preferences) {
this.preferences = preferences;
@@ -128,6 +130,13 @@ public class Subsonic {
return sharingClient;
}
public OpenClient getOpenClient() {
if (openClient == null) {
openClient = new OpenClient(this);
}
return openClient;
}
public String getUrl() {
String url = preferences.getServerUrl() + "/rest/";
return url.replace("//rest", "/rest");

View File

@@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import retrofit2.Call;
public class MediaAnnotationClient {
private static final String TAG = "BrowsingClient";
private static final String TAG = "MediaAnnotationClient";
private final Subsonic subsonic;
private final MediaAnnotationService mediaAnnotationService;
@@ -34,8 +34,8 @@ public class MediaAnnotationClient {
return mediaAnnotationService.setRating(subsonic.getParams(), id, rating);
}
public Call<ApiResponse> scrobble(String id) {
public Call<ApiResponse> scrobble(String id, boolean submission) {
Log.d(TAG, "scrobble()");
return mediaAnnotationService.scrobble(subsonic.getParams(), id);
return mediaAnnotationService.scrobble(subsonic.getParams(), id, submission);
}
}

View File

@@ -20,5 +20,5 @@ public interface MediaAnnotationService {
Call<ApiResponse> setRating(@QueryMap Map<String, String> params, @Query("id") String id, @Query("rating") int rating);
@GET("scrobble")
Call<ApiResponse> scrobble(@QueryMap Map<String, String> params, @Query("id") String id);
Call<ApiResponse> scrobble(@QueryMap Map<String, String> params, @Query("id") String id, @Query("submission") Boolean submission);
}

View File

@@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import retrofit2.Call;
public class MediaRetrievalClient {
private static final String TAG = "BrowsingClient";
private static final String TAG = "MediaRetrievalClient";
private final Subsonic subsonic;
private final MediaRetrievalService mediaRetrievalService;

View File

@@ -0,0 +1,26 @@
package com.cappielloantonio.tempo.subsonic.api.open;
import android.util.Log;
import com.cappielloantonio.tempo.subsonic.RetrofitClient;
import com.cappielloantonio.tempo.subsonic.Subsonic;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import retrofit2.Call;
public class OpenClient {
private static final String TAG = "OpenClient";
private final Subsonic subsonic;
private final OpenService openService;
public OpenClient(Subsonic subsonic) {
this.subsonic = subsonic;
this.openService = new RetrofitClient(subsonic).getRetrofit().create(OpenService.class);
}
public Call<ApiResponse> getLyricsBySongId(String id) {
Log.d(TAG, "getLyricsBySongId()");
return openService.getLyricsBySongId(subsonic.getParams(), id);
}
}

View File

@@ -0,0 +1,15 @@
package com.cappielloantonio.tempo.subsonic.api.open;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import java.util.Map;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
public interface OpenService {
@GET("getLyricsBySongId")
Call<ApiResponse> getLyricsBySongId(@QueryMap Map<String, String> params, @Query("id") String id);
}

View File

@@ -28,4 +28,9 @@ public class SystemClient {
Log.d(TAG, "getLicense()");
return systemService.getLicense(subsonic.getParams());
}
public Call<ApiResponse> getOpenSubsonicExtensions() {
Log.d(TAG, "getOpenSubsonicExtensions()");
return systemService.getOpenSubsonicExtensions(subsonic.getParams());
}
}

View File

@@ -14,4 +14,7 @@ public interface SystemService {
@GET("getLicense")
Call<ApiResponse> getLicense(@QueryMap Map<String, String> params);
@GET("getOpenSubsonicExtensions")
Call<ApiResponse> getOpenSubsonicExtensions(@QueryMap Map<String, String> params);
}

View File

@@ -7,5 +7,5 @@ import com.google.gson.annotations.SerializedName
@Keep
class ApiResponse {
@SerializedName("subsonic-response")
lateinit var subsonicResponse: SubsonicResponse;
lateinit var subsonicResponse: SubsonicResponse
}

View File

@@ -4,6 +4,8 @@ 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.*
@Keep
@@ -17,9 +19,23 @@ open class AlbumID3 : Parcelable {
var coverArtId: String? = null
var songCount: Int? = 0
var duration: Int? = 0
var playCount: Long? = null
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
}

View File

@@ -0,0 +1,12 @@
package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
open class DiscTitle : Parcelable {
var disc: Int? = null
var title: String? = null
}

View File

@@ -0,0 +1,25 @@
package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import kotlinx.parcelize.Parcelize
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@Keep
@Parcelize
open class ItemDate : Parcelable {
var year: Int? = null
var month: Int? = null
var day: Int? = null
fun getFormattedDate(): String {
val calendar = Calendar.getInstance()
val dateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
calendar.set(year ?: 0, month ?: 0, day ?: 0)
return dateFormat.format(calendar.time)
}
}

View File

@@ -0,0 +1,11 @@
package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
open class ItemGenre : Parcelable {
var name: String? = null
}

View File

@@ -0,0 +1,9 @@
package com.cappielloantonio.tempo.subsonic.models
import androidx.annotation.Keep
@Keep
class Line {
var start: Int? = null
lateinit var value: String
}

View File

@@ -0,0 +1,8 @@
package com.cappielloantonio.tempo.subsonic.models
import androidx.annotation.Keep
@Keep
class LyricsList {
var structuredLyrics: List<StructuredLyrics>? = null
}

View File

@@ -0,0 +1,9 @@
package com.cappielloantonio.tempo.subsonic.models
import androidx.annotation.Keep
@Keep
class OpenSubsonicExtension {
var name: String? = null
var versions: List<Int>? = null
}

View File

@@ -0,0 +1,11 @@
package com.cappielloantonio.tempo.subsonic.models
import android.os.Parcelable
import androidx.annotation.Keep
import kotlinx.parcelize.Parcelize
@Keep
@Parcelize
open class RecordLabel : Parcelable {
var name: String? = null
}

View File

@@ -0,0 +1,13 @@
package com.cappielloantonio.tempo.subsonic.models
import androidx.annotation.Keep
@Keep
class StructuredLyrics {
var displayArtist: String? = null
var displayTitle: String? = null
var lang: String? = null
var offset: Int = 0
var synced: Boolean = false
var line: List<Line>? = null
}

View File

@@ -51,4 +51,7 @@ class SubsonicResponse {
var version: String? = null
var type: String? = null
var serverVersion: String? = null
var openSubsonic: Boolean? = null
var openSubsonicExtensions: List<OpenSubsonicExtension>? = null
var lyricsList: LyricsList? = null
}

View File

@@ -2,7 +2,8 @@ package com.cappielloantonio.tempo.subsonic.utils;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Network;
import android.net.NetworkCapabilities;
import com.cappielloantonio.tempo.App;
@@ -39,7 +40,19 @@ public class CacheUtil {
private boolean isConnected() {
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
return (netInfo != null && netInfo.isConnected());
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) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
}
}
}
return false;
}
}

View File

@@ -6,6 +6,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
@@ -18,12 +19,16 @@ import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.navigation.ui.NavigationUI;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.broadcast.receiver.ConnectivityStatusBroadcastReceiver;
import com.cappielloantonio.tempo.databinding.ActivityMainBinding;
import com.cappielloantonio.tempo.github.utils.UpdateUtil;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.ui.activity.base.BaseActivity;
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.Constants;
@@ -39,7 +44,7 @@ import java.util.concurrent.ExecutionException;
@UnstableApi
public class MainActivity extends BaseActivity {
private static final String TAG = "MainActivity";
private static final String TAG = "MainActivityLogs";
public ActivityMainBinding bind;
private MainViewModel mainViewModel;
@@ -70,6 +75,8 @@ public class MainActivity extends BaseActivity {
init();
checkConnectionType();
getOpenSubsonicExtensions();
checkTempoUpdate();
}
@Override
@@ -300,10 +307,11 @@ public class MainActivity extends BaseActivity {
Preferences.setToken(null);
Preferences.setPassword(null);
Preferences.setServer(null);
Preferences.setLocalAddress(null);
Preferences.setUser(null);
// TODO Enter all settings to be reset
Preferences.setServerId(null);
Preferences.setOpenSubsonic(false);
Preferences.setPlaybackSpeed(Constants.MEDIA_PLAYBACK_SPEED_100);
Preferences.setSkipSilenceMode(false);
Preferences.setDataSavingMode(false);
@@ -333,10 +341,55 @@ public class MainActivity extends BaseActivity {
}
private void pingServer() {
if (Preferences.getToken() == null) return;
if (Preferences.isInUseServerAddressLocal()) {
mainViewModel.ping().observe(this, subsonicResponse -> {
if (subsonicResponse == null) {
Preferences.setServerSwitchableTimer();
Preferences.switchInUseServerAddress();
App.refreshSubsonicClient();
pingServer();
} else {
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
}
});
} else {
if (Preferences.isServerSwitchable()) {
Preferences.setServerSwitchableTimer();
Preferences.switchInUseServerAddress();
App.refreshSubsonicClient();
pingServer();
} else {
mainViewModel.ping().observe(this, subsonicResponse -> {
if (subsonicResponse == null) {
if (Preferences.showServerUnreachableDialog()) {
ServerUnreachableDialog dialog = new ServerUnreachableDialog();
dialog.show(getSupportFragmentManager(), null);
}
} else {
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
}
});
}
}
}
private void getOpenSubsonicExtensions() {
if (Preferences.getToken() != null) {
mainViewModel.ping().observe(this, isPingSuccessfull -> {
if (!isPingSuccessfull && Preferences.showServerUnreachableDialog()) {
ServerUnreachableDialog dialog = new ServerUnreachableDialog();
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
if (openSubsonicExtensions != null) {
Preferences.setOpenSubsonicExtensions(openSubsonicExtensions);
}
});
}
}
private void checkTempoUpdate() {
if (BuildConfig.FLAVOR.equals("tempo") && Preferences.showTempoUpdateDialog()) {
mainViewModel.checkTempoUpdate().observe(this, latestRelease -> {
if (latestRelease != null && UpdateUtil.showUpdateDialog(latestRelease)) {
GithubTempoUpdateDialog dialog = new GithubTempoUpdateDialog(latestRelease);
dialog.show(getSupportFragmentManager(), null);
}
});

View File

@@ -6,6 +6,7 @@ import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
@@ -37,6 +38,7 @@ public class BaseActivity extends AppCompatActivity {
initializeDownloader();
checkBatteryOptimization();
checkPermission();
checkAlwaysOnDisplay();
}
@Override
@@ -66,6 +68,12 @@ public class BaseActivity extends AppCompatActivity {
}
}
private void checkAlwaysOnDisplay() {
if (Preferences.isDisplayAlwaysOn()) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
private boolean detectBatteryOptimization() {
String packageName = getPackageName();
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);

View File

@@ -38,8 +38,8 @@ public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder>
public void onBindViewHolder(ViewHolder holder, int position) {
AlbumID3 album = albums.get(position);
holder.item.albumNameLabel.setText(MusicUtil.getReadableString(album.getName()));
holder.item.artistNameLabel.setText(MusicUtil.getReadableString(album.getArtist()));
holder.item.albumNameLabel.setText(album.getName());
holder.item.artistNameLabel.setText(album.getArtist());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album)

View File

@@ -38,8 +38,8 @@ public class AlbumArtistPageOrSimilarAdapter extends RecyclerView.Adapter<AlbumA
public void onBindViewHolder(ViewHolder holder, int position) {
AlbumID3 album = albums.get(position);
holder.item.albumNameLabel.setText(MusicUtil.getReadableString(album.getName()));
holder.item.artistNameLabel.setText(MusicUtil.getReadableString(album.getArtist()));
holder.item.albumNameLabel.setText(album.getName());
holder.item.artistNameLabel.setText(album.getArtist());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album)

View File

@@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
@@ -23,6 +24,9 @@ import java.util.List;
public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAdapter.ViewHolder> implements Filterable {
private final ClickCallback click;
private String currentFilter;
private boolean showArtist;
private final Filter filtering = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
@@ -32,6 +36,7 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
filteredList.addAll(albumsFull);
} else {
String filterPattern = constraint.toString().toLowerCase().trim();
currentFilter = filterPattern;
for (AlbumID3 item : albumsFull) {
if (item.getName().toLowerCase().contains(filterPattern)) {
@@ -48,8 +53,7 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
albums.clear();
albums.addAll((List) results.values);
albums = (List<AlbumID3>) results.values;
notifyDataSetChanged();
}
};
@@ -57,9 +61,12 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
private List<AlbumID3> albums;
private List<AlbumID3> albumsFull;
public AlbumCatalogueAdapter(ClickCallback click) {
public AlbumCatalogueAdapter(ClickCallback click, boolean showArtist) {
this.click = click;
this.albums = Collections.emptyList();
this.albumsFull = Collections.emptyList();
this.currentFilter = "";
this.showArtist = showArtist;
}
@NonNull
@@ -73,8 +80,9 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
public void onBindViewHolder(ViewHolder holder, int position) {
AlbumID3 album = albums.get(position);
holder.item.albumNameLabel.setText(MusicUtil.getReadableString(album.getName()));
holder.item.artistNameLabel.setText(MusicUtil.getReadableString(album.getArtist()));
holder.item.albumNameLabel.setText(album.getName());
holder.item.artistNameLabel.setText(album.getArtist());
holder.item.artistNameLabel.setVisibility(showArtist ? View.VISIBLE : View.GONE);
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album)
@@ -92,9 +100,8 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
}
public void setItems(List<AlbumID3> albums) {
this.albums = albums;
this.albumsFull = new ArrayList<>(albums);
notifyDataSetChanged();
filtering.filter(currentFilter);
}
@Override
@@ -158,8 +165,20 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
case Constants.ALBUM_ORDER_BY_RANDOM:
Collections.shuffle(albums);
break;
case Constants.ALBUM_ORDER_BY_RECENTLY_ADDED:
albums.sort(Comparator.comparing(AlbumID3::getCreated));
Collections.reverse(albums);
break;
case Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED:
albums.sort(Comparator.comparing(AlbumID3::getPlayed));
Collections.reverse(albums);
break;
case Constants.ALBUM_ORDER_BY_MOST_PLAYED:
albums.sort(Comparator.comparing(AlbumID3::getPlayCount));
Collections.reverse(albums);
break;
}
notifyDataSetChanged();
}
}
}

View File

@@ -3,6 +3,8 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -11,22 +13,60 @@ import com.cappielloantonio.tempo.databinding.ItemHorizontalAlbumBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class AlbumHorizontalAdapter extends RecyclerView.Adapter<AlbumHorizontalAdapter.ViewHolder> {
public class AlbumHorizontalAdapter extends RecyclerView.Adapter<AlbumHorizontalAdapter.ViewHolder> implements Filterable {
private final ClickCallback click;
private final boolean isOffline;
private List<AlbumID3> albumsFull;
private List<AlbumID3> albums;
private String currentFilter;
private final Filter filtering = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<AlbumID3> filteredList = new ArrayList<>();
if (constraint == null || constraint.length() == 0) {
filteredList.addAll(albumsFull);
} else {
String filterPattern = constraint.toString().toLowerCase().trim();
currentFilter = filterPattern;
for (AlbumID3 item : albumsFull) {
if (item.getName().toLowerCase().contains(filterPattern)) {
filteredList.add(item);
}
}
}
FilterResults results = new FilterResults();
results.values = filteredList;
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
albums = (List<AlbumID3>) results.values;
notifyDataSetChanged();
}
};
public AlbumHorizontalAdapter(ClickCallback click, boolean isOffline) {
this.click = click;
this.isOffline = isOffline;
this.albums = Collections.emptyList();
this.albumsFull = Collections.emptyList();
this.currentFilter = "";
}
@NonNull
@@ -40,8 +80,8 @@ public class AlbumHorizontalAdapter extends RecyclerView.Adapter<AlbumHorizontal
public void onBindViewHolder(ViewHolder holder, int position) {
AlbumID3 album = albums.get(position);
holder.item.albumTitleTextView.setText(MusicUtil.getReadableString(album.getName()));
holder.item.albumArtistTextView.setText(MusicUtil.getReadableString(album.getArtist()));
holder.item.albumTitleTextView.setText(album.getName());
holder.item.albumArtistTextView.setText(album.getArtist());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album)
@@ -55,10 +95,16 @@ public class AlbumHorizontalAdapter extends RecyclerView.Adapter<AlbumHorizontal
}
public void setItems(List<AlbumID3> albums) {
this.albums = albums;
this.albumsFull = albums != null ? albums : Collections.emptyList();
filtering.filter(currentFilter);
notifyDataSetChanged();
}
@Override
public Filter getFilter() {
return filtering;
}
public AlbumID3 getItem(int id) {
return albums.get(id);
}
@@ -95,4 +141,21 @@ public class AlbumHorizontalAdapter extends RecyclerView.Adapter<AlbumHorizontal
return true;
}
}
public void sort(String order) {
switch (order) {
case Constants.ALBUM_ORDER_BY_NAME:
albums.sort(Comparator.comparing(AlbumID3::getName));
break;
case Constants.ALBUM_ORDER_BY_MOST_RECENTLY_STARRED:
albums.sort(Comparator.comparing(AlbumID3::getStarred, Comparator.nullsLast(Comparator.reverseOrder())));
break;
case Constants.ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED:
albums.sort(Comparator.comparing(AlbumID3::getStarred, Comparator.nullsLast(Comparator.naturalOrder())));
break;
}
notifyDataSetChanged();
}
}

View File

@@ -44,7 +44,7 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
public void onBindViewHolder(ViewHolder holder, int position) {
ArtistID3 artist = artists.get(position);
holder.item.artistNameLabel.setText(MusicUtil.getReadableString(artist.getName()));
holder.item.artistNameLabel.setText(artist.getName());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist)

View File

@@ -50,7 +50,7 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
artists.clear();
artists.addAll((List) results.values);
if (results.count > 0) artists.addAll((List) results.values);
notifyDataSetChanged();
}
};
@@ -74,7 +74,7 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
public void onBindViewHolder(ViewHolder holder, int position) {
ArtistID3 artist = artists.get(position);
holder.item.artistNameLabel.setText(MusicUtil.getReadableString(artist.getName()));
holder.item.artistNameLabel.setText(artist.getName());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist)

View File

@@ -4,6 +4,8 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@@ -11,21 +13,59 @@ import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.databinding.ItemHorizontalArtistBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizontalAdapter.ViewHolder> {
public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizontalAdapter.ViewHolder> implements Filterable {
private final ClickCallback click;
private List<ArtistID3> artistsFull;
private List<ArtistID3> artists;
private String currentFilter;
private final Filter filtering = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<ArtistID3> filteredList = new ArrayList<>();
if (constraint == null || constraint.length() == 0) {
filteredList.addAll(artistsFull);
} else {
String filterPattern = constraint.toString().toLowerCase().trim();
currentFilter = filterPattern;
for (ArtistID3 item : artistsFull) {
if (item.getName().toLowerCase().contains(filterPattern)) {
filteredList.add(item);
}
}
}
FilterResults results = new FilterResults();
results.values = filteredList;
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
artists = (List<ArtistID3>) results.values;
notifyDataSetChanged();
}
};
public ArtistHorizontalAdapter(ClickCallback click) {
this.click = click;
this.artists = Collections.emptyList();
this.artistsFull = Collections.emptyList();
this.currentFilter = "";
}
@NonNull
@@ -39,7 +79,7 @@ public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizont
public void onBindViewHolder(ViewHolder holder, int position) {
ArtistID3 artist = artists.get(position);
holder.item.artistNameTextView.setText(MusicUtil.getReadableString(artist.getName()));
holder.item.artistNameTextView.setText(artist.getName());
if (artist.getAlbumCount() > 0) {
holder.item.artistInfoTextView.setText("Album count: " + artist.getAlbumCount());
@@ -59,10 +99,16 @@ public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizont
}
public void setItems(List<ArtistID3> artists) {
this.artists = artists;
this.artistsFull = artists != null ? artists : Collections.emptyList();
filtering.filter(currentFilter);
notifyDataSetChanged();
}
@Override
public Filter getFilter() {
return filtering;
}
public ArtistID3 getItem(int id) {
return artists.get(id);
}
@@ -109,4 +155,21 @@ public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizont
return true;
}
}
public void sort(String order) {
switch (order) {
case Constants.ARTIST_ORDER_BY_NAME:
artists.sort(Comparator.comparing(ArtistID3::getName));
break;
case Constants.ARTIST_ORDER_BY_MOST_RECENTLY_STARRED:
artists.sort(Comparator.comparing(ArtistID3::getStarred, Comparator.nullsLast(Comparator.reverseOrder())));
break;
case Constants.ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED:
artists.sort(Comparator.comparing(ArtistID3::getStarred, Comparator.nullsLast(Comparator.naturalOrder())));
break;
}
notifyDataSetChanged();
}
}

View File

@@ -38,7 +38,7 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
public void onBindViewHolder(ViewHolder holder, int position) {
SimilarArtistID3 artist = artists.get(position);
holder.item.artistNameLabel.setText(MusicUtil.getReadableString(artist.getName()));
holder.item.artistNameLabel.setText(artist.getName());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist)

View File

@@ -39,8 +39,8 @@ public class DiscoverSongAdapter extends RecyclerView.Adapter<DiscoverSongAdapte
public void onBindViewHolder(ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.titleDiscoverSongLabel.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.albumDiscoverSongLabel.setText(MusicUtil.getReadableString(song.getAlbum()));
holder.item.titleDiscoverSongLabel.setText(song.getTitle());
holder.item.albumDiscoverSongLabel.setText(song.getAlbum());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)

View File

@@ -33,6 +33,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
private String filterValue;
private List<Child> songs;
private List<Child> shuffling;
private List<Child> grouped;
public DownloadHorizontalAdapter(ClickCallback click) {
@@ -82,6 +83,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
this.songs = songs;
this.grouped = groupSong(songs);
this.shuffling = shufflingSong(new ArrayList<>(songs));
notifyDataSetChanged();
}
@@ -90,6 +92,10 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
return grouped.get(id);
}
public List<Child> getShuffling() {
return shuffling;
}
@Override
public int getItemViewType(int position) {
return position;
@@ -136,6 +142,27 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
return songs;
}
private List<Child> shufflingSong(List<Child> songs) {
if (filterValue == null) {
return songs;
}
switch (filterKey) {
case Constants.DOWNLOAD_TYPE_TRACK:
return songs.stream().filter(child -> child.getId().equals(filterValue)).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_ALBUM:
return songs.stream().filter(child -> Objects.equals(child.getAlbumId(), filterValue)).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_GENRE:
return songs.stream().filter(child -> Objects.equals(child.getGenre(), filterValue)).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_YEAR:
return songs.stream().filter(child -> Objects.equals(child.getYear(), Integer.valueOf(filterValue))).collect(Collectors.toList());
case Constants.DOWNLOAD_TYPE_ARTIST:
return songs.stream().filter(child -> Objects.equals(child.getArtistId(), filterValue)).collect(Collectors.toList());
default:
return songs;
}
}
private String countSong(String filterKey, String filterValue, List<Child> songs) {
if (filterValue != null) {
switch (filterKey) {
@@ -158,9 +185,17 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
private void initTrackLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration(), false)));
holder.item.downloadedItemPreTextView.setText(MusicUtil.getReadableString(song.getAlbum()));
holder.item.downloadedItemTitleTextView.setText(song.getTitle());
holder.item.downloadedItemSubtitleTextView.setText(
holder.itemView.getContext().getString(
R.string.song_subtitle_formatter,
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
""
)
);
holder.item.downloadedItemPreTextView.setText(song.getAlbum());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)
@@ -181,9 +216,9 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
private void initAlbumLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getAlbum()));
holder.item.downloadedItemTitleTextView.setText(song.getAlbum());
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_ALBUM, song.getAlbumId(), songs)));
holder.item.downloadedItemPreTextView.setText(MusicUtil.getReadableString(song.getArtist()));
holder.item.downloadedItemPreTextView.setText(song.getArtist());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)
@@ -204,7 +239,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
private void initArtistLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getArtist()));
holder.item.downloadedItemTitleTextView.setText(song.getArtist());
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_ARTIST, song.getArtistId(), songs)));
CustomGlideRequest.Builder
@@ -220,7 +255,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
private void initGenreLayout(ViewHolder holder, int position) {
Child song = grouped.get(position);
holder.item.downloadedItemTitleTextView.setText(MusicUtil.getReadableString(song.getGenre()));
holder.item.downloadedItemTitleTextView.setText(song.getGenre());
holder.item.downloadedItemSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.download_item_single_subtitle_formatter, countSong(Constants.DOWNLOAD_TYPE_GENRE, song.getGenre(), songs)));
holder.item.itemCoverImageView.setVisibility(View.GONE);

View File

@@ -37,7 +37,7 @@ public class GenreAdapter extends RecyclerView.Adapter<GenreAdapter.ViewHolder>
public void onBindViewHolder(ViewHolder holder, int position) {
Genre genre = genres.get(position);
holder.item.genreLabel.setText(MusicUtil.getReadableString(genre.getGenre()));
holder.item.genreLabel.setText(genre.getGenre());
}
@Override

View File

@@ -13,7 +13,6 @@ import com.cappielloantonio.tempo.databinding.ItemLibraryCatalogueGenreBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.Genre;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import java.util.ArrayList;
import java.util.Collections;
@@ -49,7 +48,7 @@ public class GenreCatalogueAdapter extends RecyclerView.Adapter<GenreCatalogueAd
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
genres.clear();
genres.addAll((List) results.values);
if (results.count > 0) genres.addAll((List) results.values);
notifyDataSetChanged();
}
};
@@ -73,7 +72,7 @@ public class GenreCatalogueAdapter extends RecyclerView.Adapter<GenreCatalogueAd
public void onBindViewHolder(ViewHolder holder, int position) {
Genre genre = genres.get(position);
holder.item.genreLabel.setText(MusicUtil.getReadableString(genre.getGenre()));
holder.item.genreLabel.setText(genre.getGenre());
}
@Override

View File

@@ -0,0 +1,76 @@
package com.cappielloantonio.tempo.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.databinding.ItemHorizontalHomeSectorBinding;
import com.cappielloantonio.tempo.databinding.ItemHorizontalPlaylistDialogTrackBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.HomeSector;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import java.util.Collections;
import java.util.List;
public class HomeSectorHorizontalAdapter extends RecyclerView.Adapter<HomeSectorHorizontalAdapter.ViewHolder> {
private List<HomeSector> sectors;
public HomeSectorHorizontalAdapter() {
this.sectors = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemHorizontalHomeSectorBinding view = ItemHorizontalHomeSectorBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
HomeSector sector = sectors.get(position);
holder.item.homeSectorTitleCheckBox.setText(sector.getSectorTitle());
holder.item.homeSectorTitleCheckBox.setChecked(sector.isVisible());
}
@Override
public int getItemCount() {
return sectors.size();
}
public List<HomeSector> getItems() {
return this.sectors;
}
public void setItems(List<HomeSector> sectors) {
this.sectors = sectors;
notifyDataSetChanged();
}
public HomeSector getItem(int id) {
return sectors.get(id);
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemHorizontalHomeSectorBinding item;
ViewHolder(ItemHorizontalHomeSectorBinding item) {
super(item.getRoot());
this.item = item;
this.item.homeSectorTitleCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> onCheck(isChecked));
}
private void onCheck(boolean isChecked) {
sectors.get(getBindingAdapterPosition()).setVisible(isChecked);
}
}
}

View File

@@ -2,9 +2,11 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.media3.session.MediaBrowser;
import androidx.recyclerview.widget.RecyclerView;
@@ -17,6 +19,7 @@ import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
@@ -45,8 +48,15 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
public void onBindViewHolder(ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.queueSongTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.queueSongSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration(), false)));
holder.item.queueSongTitleTextView.setText(song.getTitle());
holder.item.queueSongSubtitleTextView.setText(
holder.itemView.getContext().getString(
R.string.song_subtitle_formatter,
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
MusicUtil.getReadableAudioQualityString(song)
)
);
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)
@@ -59,12 +69,33 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
if (position < index) {
holder.item.queueSongTitleTextView.setAlpha(0.2f);
holder.item.queueSongSubtitleTextView.setAlpha(0.2f);
holder.item.ratingIndicatorImageView.setAlpha(0.2f);
} else {
holder.item.queueSongTitleTextView.setAlpha(1.0f);
holder.item.queueSongSubtitleTextView.setAlpha(1.0f);
holder.item.ratingIndicatorImageView.setAlpha(1.0f);
}
}
});
if (Preferences.showItemRating()) {
if (song.getStarred() == null && song.getUserRating() == null) {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
holder.item.preferredIcon.setVisibility(song.getStarred() != null ? View.VISIBLE : View.GONE);
holder.item.ratingBarLayout.setVisibility(song.getUserRating() != null ? View.VISIBLE : View.GONE);
if (song.getUserRating() != null) {
holder.item.oneStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 1 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.twoStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 2 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.threeStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 3 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.fourStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 4 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.fiveStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 5 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
}
} else {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
}
public List<Child> getItems() {

View File

@@ -38,7 +38,7 @@ public class PlaylistDialogHorizontalAdapter extends RecyclerView.Adapter<Playli
public void onBindViewHolder(ViewHolder holder, int position) {
Playlist playlist = playlists.get(position);
holder.item.playlistDialogTitleTextView.setText(MusicUtil.getReadableString(playlist.getName()));
holder.item.playlistDialogTitleTextView.setText(playlist.getName());
holder.item.playlistDialogCountTextView.setText(holder.itemView.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false)));
}

View File

@@ -32,8 +32,8 @@ public class PlaylistDialogSongHorizontalAdapter extends RecyclerView.Adapter<Pl
public void onBindViewHolder(ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.playlistDialogSongTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.playlistDialogAlbumArtistTextView.setText(MusicUtil.getReadableString(song.getArtist()));
holder.item.playlistDialogSongTitleTextView.setText(song.getTitle());
holder.item.playlistDialogAlbumArtistTextView.setText(song.getArtist());
holder.item.playlistDialogSongDurationTextView.setText(MusicUtil.getReadableDurationString(song.getDuration(), false));
CustomGlideRequest.Builder

View File

@@ -54,7 +54,7 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
playlists.clear();
playlists.addAll((List) results.values);
if (results.count > 0) playlists.addAll((List) results.values);
notifyDataSetChanged();
}
};
@@ -75,7 +75,7 @@ public class PlaylistHorizontalAdapter extends RecyclerView.Adapter<PlaylistHori
public void onBindViewHolder(ViewHolder holder, int position) {
Playlist playlist = playlists.get(position);
holder.item.playlistTitleTextView.setText(MusicUtil.getReadableString(playlist.getName()));
holder.item.playlistTitleTextView.setText(playlist.getName());
holder.item.playlistSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false)));
CustomGlideRequest.Builder

View File

@@ -48,7 +48,7 @@ public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter<Podcast
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
podcastChannels.clear();
podcastChannels.addAll((List) results.values);
if (results.count > 0) podcastChannels.addAll((List) results.values);
notifyDataSetChanged();
}
};
@@ -72,7 +72,7 @@ public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter<Podcast
public void onBindViewHolder(ViewHolder holder, int position) {
PodcastChannel podcastChannel = podcastChannels.get(position);
holder.item.podcastChannelTitleLabel.setText(MusicUtil.getReadableString(podcastChannel.getTitle()));
holder.item.podcastChannelTitleLabel.setText(podcastChannel.getTitle());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), podcastChannel.getCoverArtId(), CustomGlideRequest.ResourceType.Podcast)

View File

@@ -38,7 +38,7 @@ public class PodcastChannelHorizontalAdapter extends RecyclerView.Adapter<Podcas
public void onBindViewHolder(ViewHolder holder, int position) {
PodcastChannel podcastChannel = podcastChannels.get(position);
holder.item.podcastChannelTitleTextView.setText(MusicUtil.getReadableString(podcastChannel.getTitle()));
holder.item.podcastChannelTitleTextView.setText(podcastChannel.getTitle());
holder.item.podcastChannelDescriptionTextView.setText(MusicUtil.getReadableString(podcastChannel.getDescription()));
CustomGlideRequest.Builder

View File

@@ -45,8 +45,8 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
PodcastEpisode podcastEpisode = podcastEpisodes.get(position);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MMM d");
holder.item.podcastTitleLabel.setText(MusicUtil.getReadableString(podcastEpisode.getTitle()));
holder.item.podcastSubtitleLabel.setText(MusicUtil.getReadableString(podcastEpisode.getArtist()));
holder.item.podcastTitleLabel.setText(podcastEpisode.getTitle());
holder.item.podcastSubtitleLabel.setText(podcastEpisode.getArtist());
holder.item.podcastReleasesAndDurationLabel.setText(holder.itemView.getContext().getString(R.string.podcast_release_date_duration_formatter, simpleDateFormat.format(podcastEpisode.getPublishDate()), MusicUtil.getReadablePodcastDurationString(podcastEpisode.getDuration())));
holder.item.podcastDescriptionText.setText(MusicUtil.getReadableString(podcastEpisode.getDescription()));

View File

@@ -40,7 +40,7 @@ public class ShareHorizontalAdapter extends RecyclerView.Adapter<ShareHorizontal
public void onBindViewHolder(ViewHolder holder, int position) {
Share share = shares.get(position);
holder.item.shareTitleTextView.setText(MusicUtil.getReadableString(share.getDescription()));
holder.item.shareTitleTextView.setText(share.getDescription());
holder.item.shareSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.share_subtitle_item, UIUtil.getReadableDate(share.getExpires())));
if (share.getEntries() != null && !share.getEntries().isEmpty()) CustomGlideRequest.Builder

View File

@@ -38,7 +38,7 @@ public class SimilarTrackAdapter extends RecyclerView.Adapter<SimilarTrackAdapte
public void onBindViewHolder(ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.titleTrackLabel.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.titleTrackLabel.setText(song.getTitle());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)

View File

@@ -4,8 +4,11 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
@@ -13,28 +16,71 @@ import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.ItemHorizontalTrackBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@UnstableApi
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> {
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> implements Filterable {
private final ClickCallback click;
private final boolean showCoverArt;
private final boolean showAlbum;
private final AlbumID3 album;
private List<Child> songsFull;
private List<Child> songs;
private String currentFilter;
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum) {
private final Filter filtering = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<Child> filteredList = new ArrayList<>();
if (constraint == null || constraint.length() == 0) {
filteredList.addAll(songsFull);
} else {
String filterPattern = constraint.toString().toLowerCase().trim();
currentFilter = filterPattern;
for (Child item : songsFull) {
if (item.getTitle().toLowerCase().contains(filterPattern)) {
filteredList.add(item);
}
}
}
FilterResults results = new FilterResults();
results.values = filteredList;
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
songs = (List<Child>) results.values;
notifyDataSetChanged();
}
};
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
this.click = click;
this.showCoverArt = showCoverArt;
this.showAlbum = showAlbum;
this.songs = Collections.emptyList();
this.songsFull = Collections.emptyList();
this.currentFilter = "";
this.album = album;
}
@NonNull
@@ -48,14 +94,25 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
public void onBindViewHolder(ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.searchResultSongTitleTextView.setText(MusicUtil.getReadableString(song.getTitle()));
holder.item.searchResultSongSubtitleTextView.setText(holder.itemView.getContext().getString(R.string.song_subtitle_formatter, MusicUtil.getReadableString(this.showAlbum ? song.getAlbum() : song.getArtist()), MusicUtil.getReadableDurationString(song.getDuration() != null ? song.getDuration() : 0, false)));
holder.item.searchResultSongTitleTextView.setText(song.getTitle());
holder.item.searchResultSongSubtitleTextView.setText(
holder.itemView.getContext().getString(
R.string.song_subtitle_formatter,
this.showAlbum ?
song.getAlbum() :
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
MusicUtil.getReadableAudioQualityString(song)
)
);
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
holder.item.searchResultDowanloadIndicatorImageView.setVisibility(View.VISIBLE);
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
} else {
holder.item.searchResultDowanloadIndicatorImageView.setVisibility(View.GONE);
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
}
if (showCoverArt) CustomGlideRequest.Builder
@@ -66,8 +123,47 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
holder.item.trackNumberTextView.setVisibility(showCoverArt ? View.INVISIBLE : View.VISIBLE);
holder.item.songCoverImageView.setVisibility(showCoverArt ? View.VISIBLE : View.INVISIBLE);
if (!showCoverArt && (position > 0 && songs.get(position - 1) != null && songs.get(position - 1).getDiscNumber() != null && songs.get(position).getDiscNumber() != null && songs.get(position - 1).getDiscNumber() < songs.get(position).getDiscNumber())) {
holder.item.differentDiskDivider.setVisibility(View.VISIBLE);
if (!showCoverArt &&
(position == 0 ||
(position > 0 && songs.get(position - 1) != null &&
songs.get(position - 1).getDiscNumber() != null &&
songs.get(position).getDiscNumber() != null &&
songs.get(position - 1).getDiscNumber() < songs.get(position).getDiscNumber()
)
)
) {
holder.item.differentDiskDividerSector.setVisibility(View.VISIBLE);
if (songs.get(position).getDiscNumber() != null && !Objects.requireNonNull(songs.get(position).getDiscNumber()).toString().isBlank()) {
holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titleless, songs.get(position).getDiscNumber().toString()));
}
if (album.getDiscTitles() != null) {
Optional<DiscTitle> discTitle = album.getDiscTitles().stream().filter(title -> Objects.equals(title.getDisc(), songs.get(position).getDiscNumber())).findFirst();
if (discTitle.isPresent() && discTitle.get().getDisc() != null && discTitle.get().getTitle() != null && !discTitle.get().getTitle().isEmpty()) {
holder.item.discTitleTextView.setText(holder.itemView.getContext().getString(R.string.disc_titlefull, discTitle.get().getDisc().toString() , discTitle.get().getTitle()));
}
}
}
if (Preferences.showItemRating()) {
if (song.getStarred() == null && song.getUserRating() == null) {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
holder.item.preferredIcon.setVisibility(song.getStarred() != null ? View.VISIBLE : View.GONE);
holder.item.ratingBarLayout.setVisibility(song.getUserRating() != null ? View.VISIBLE : View.GONE);
if (song.getUserRating() != null) {
holder.item.oneStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 1 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.twoStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 2 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.threeStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 3 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.fourStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 4 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
holder.item.fiveStarIcon.setImageDrawable(AppCompatResources.getDrawable(holder.itemView.getContext(), song.getUserRating() >= 5 ? R.drawable.ic_star : R.drawable.ic_star_outlined));
}
} else {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
}
@@ -77,7 +173,8 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
}
public void setItems(List<Child> songs) {
this.songs = songs != null ? songs : Collections.emptyList();
this.songsFull = songs != null ? songs : Collections.emptyList();
filtering.filter(currentFilter);
notifyDataSetChanged();
}
@@ -91,6 +188,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
return position;
}
@Override
public Filter getFilter() {
return filtering;
}
public Child getItem(int id) {
return songs.get(id);
}
@@ -129,4 +231,20 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
return true;
}
}
public void sort(String order) {
switch (order) {
case Constants.MEDIA_BY_TITLE:
songs.sort(Comparator.comparing(Child::getTitle));
break;
case Constants.MEDIA_MOST_RECENTLY_STARRED:
songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.reverseOrder())));
break;
case Constants.MEDIA_LEAST_RECENTLY_STARRED:
songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.naturalOrder())));
break;
}
notifyDataSetChanged();
}
}

View File

@@ -0,0 +1,73 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogGithubTempoUpdateBinding;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Objects;
public class GithubTempoUpdateDialog extends DialogFragment {
private final LatestRelease latestRelease;
public GithubTempoUpdateDialog(LatestRelease latestRelease) {
this.latestRelease = latestRelease;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
DialogGithubTempoUpdateBinding bind = DialogGithubTempoUpdateBinding.inflate(getLayoutInflater());
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(bind.getRoot())
.setTitle(R.string.github_update_dialog_title)
.setPositiveButton(R.string.github_update_dialog_positive_button, (dialog, id) -> { })
.setNegativeButton(R.string.github_update_dialog_negative_button, (dialog, id) -> { })
.setNeutralButton(R.string.github_update_dialog_neutral_button, (dialog, id) -> { });
return builder.create();
}
@Override
public void onStart() {
super.onStart();
setButtonAction();
}
private void setButtonAction() {
AlertDialog alertDialog = (AlertDialog) Objects.requireNonNull(getDialog());
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
openLink(latestRelease.getHtmlUrl());
Objects.requireNonNull(getDialog()).dismiss();
});
alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v -> {
Preferences.setTempoUpdateReminder();
Objects.requireNonNull(getDialog()).dismiss();
});
alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
openLink(getString(R.string.support_url));
Objects.requireNonNull(getDialog()).dismiss();
});
}
private void openLink(String link) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}

View File

@@ -0,0 +1,115 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogHomeRearrangementBinding;
import com.cappielloantonio.tempo.ui.adapter.HomeSectorHorizontalAdapter;
import com.cappielloantonio.tempo.viewmodel.HomeRearrangementViewModel;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Collections;
import java.util.Objects;
public class HomeRearrangementDialog extends DialogFragment {
private DialogHomeRearrangementBinding bind;
private HomeRearrangementViewModel homeRearrangementViewModel;
private HomeSectorHorizontalAdapter homeSectorHorizontalAdapter;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogHomeRearrangementBinding.inflate(getLayoutInflater());
homeRearrangementViewModel = new ViewModelProvider(requireActivity()).get(HomeRearrangementViewModel.class);
return new MaterialAlertDialogBuilder(requireContext())
.setView(bind.getRoot())
.setTitle(R.string.home_rearrangement_dialog_title)
.setPositiveButton(R.string.home_rearrangement_dialog_positive_button, (dialog, id) -> { })
.setNeutralButton(R.string.home_rearrangement_dialog_neutral_button, (dialog, id) -> { })
.setNegativeButton(R.string.home_rearrangement_dialog_negative_button, (dialog, id) -> dialog.cancel())
.create();
}
@Override
public void onStart() {
super.onStart();
setButtonAction();
initSectorView();
}
@Override
public void onDestroyView() {
super.onDestroyView();
homeRearrangementViewModel.closeDialog();
bind = null;
}
private void setButtonAction() {
androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog());
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
homeRearrangementViewModel.saveHomeSectorList(homeSectorHorizontalAdapter.getItems());
dismiss();
});
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
homeRearrangementViewModel.resetHomeSectorList();
dismiss();
});
}
private void initSectorView() {
bind.homeSectorItemRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.homeSectorItemRecyclerView.setHasFixedSize(true);
homeSectorHorizontalAdapter = new HomeSectorHorizontalAdapter();
bind.homeSectorItemRecyclerView.setAdapter(homeSectorHorizontalAdapter);
homeSectorHorizontalAdapter.setItems(homeRearrangementViewModel.getHomeSectorList());
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
int originalPosition = -1;
int fromPosition = -1;
int toPosition = -1;
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
if (originalPosition == -1) originalPosition = viewHolder.getBindingAdapterPosition();
fromPosition = viewHolder.getBindingAdapterPosition();
toPosition = target.getBindingAdapterPosition();
Collections.swap(homeSectorHorizontalAdapter.getItems(), fromPosition, toPosition);
Objects.requireNonNull(recyclerView.getAdapter()).notifyItemMoved(fromPosition, toPosition);
return false;
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
homeRearrangementViewModel.orderSectorLiveListAfterSwap(homeSectorHorizontalAdapter.getItems());
originalPosition = -1;
fromPosition = -1;
toPosition = -1;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
}
}
).attachToRecyclerView(bind.homeSectorItemRecyclerView);
}
}

View File

@@ -83,7 +83,7 @@ public class PlaylistChooserDialog extends DialogFragment implements ClickCallba
playlistChooserViewModel.getPlaylistList(requireActivity()).observe(requireActivity(), playlists -> {
if (playlists != null) {
if (playlists.size() > 0) {
if (!playlists.isEmpty()) {
if (bind != null) bind.noPlaylistsCreatedTextView.setVisibility(View.GONE);
if (bind != null) bind.playlistDialogRecyclerView.setVisibility(View.VISIBLE);
playlistDialogHorizontalAdapter.setItems(playlists);

View File

@@ -7,6 +7,7 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
@@ -31,7 +32,8 @@ import java.util.Objects;
public class PlaylistEditorDialog extends DialogFragment {
private DialogPlaylistEditorBinding bind;
private PlaylistEditorViewModel playlistEditorViewModel;
private PlaylistCallback playlistCallback;
private final PlaylistCallback playlistCallback;
private String playlistName;
private PlaylistDialogSongHorizontalAdapter playlistDialogSongHorizontalAdapter;
@@ -80,7 +82,7 @@ public class PlaylistEditorDialog extends DialogFragment {
playlistEditorViewModel.setPlaylistToEdit(requireArguments().getParcelable(Constants.PLAYLIST_OBJECT));
if (playlistEditorViewModel.getPlaylistToEdit() != null) {
bind.playlistNameTextView.setText(MusicUtil.getReadableString(playlistEditorViewModel.getPlaylistToEdit().getName()));
bind.playlistNameTextView.setText(playlistEditorViewModel.getPlaylistToEdit().getName());
}
}
}
@@ -100,9 +102,12 @@ public class PlaylistEditorDialog extends DialogFragment {
}
});
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> Toast.makeText(requireContext(), R.string.playlist_editor_dialog_action_delete_toast, Toast.LENGTH_SHORT).show());
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnLongClickListener(v -> {
playlistEditorViewModel.deletePlaylist();
dialogDismiss();
return false;
});
bind.playlistShareButton.setOnClickListener(view -> {

View File

@@ -20,7 +20,8 @@ import java.util.Objects;
public class PodcastChannelEditorDialog extends DialogFragment {
private DialogPodcastChannelEditorBinding bind;
private PodcastChannelEditorViewModel podcastChannelEditorViewModel;
private PodcastCallback podcastCallback;
private final PodcastCallback podcastCallback;
private String channelUrl;

View File

@@ -21,7 +21,8 @@ import java.util.Objects;
public class RadioEditorDialog extends DialogFragment {
private DialogRadioEditorBinding bind;
private RadioEditorViewModel radioEditorViewModel;
private RadioCallback radioCallback;
private final RadioCallback radioCallback;
private String radioName;
private String radioStreamURL;

View File

@@ -3,6 +3,8 @@ package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
@@ -28,6 +30,7 @@ public class ServerSignupDialog extends DialogFragment {
private String username;
private String password;
private String server;
private String localAddress;
private boolean lowSecurity = false;
@NonNull
@@ -69,6 +72,7 @@ public class ServerSignupDialog extends DialogFragment {
bind.usernameTextView.setText(loginViewModel.getServerToEdit().getUsername());
bind.passwordTextView.setText("");
bind.serverTextView.setText(loginViewModel.getServerToEdit().getAddress());
bind.localAddressTextView.setText(loginViewModel.getServerToEdit().getLocalAddress());
bind.lowSecurityCheckbox.setChecked(loginViewModel.getServerToEdit().isLowSecurity());
}
} else {
@@ -86,9 +90,12 @@ public class ServerSignupDialog extends DialogFragment {
}
});
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> Toast.makeText(requireContext(), R.string.server_signup_dialog_action_delete_toast, Toast.LENGTH_SHORT).show());
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnLongClickListener(v -> {
loginViewModel.deleteServer(null);
Objects.requireNonNull(getDialog()).dismiss();
return true;
});
}
@@ -96,7 +103,8 @@ public class ServerSignupDialog extends DialogFragment {
serverName = Objects.requireNonNull(bind.serverNameTextView.getText()).toString().trim();
username = Objects.requireNonNull(bind.usernameTextView.getText()).toString().trim();
password = bind.lowSecurityCheckbox.isChecked() ? MusicUtil.passwordHexEncoding(Objects.requireNonNull(bind.passwordTextView.getText()).toString()) : Objects.requireNonNull(bind.passwordTextView.getText()).toString();
server = Objects.requireNonNull(bind.serverTextView.getText()).toString().trim();
server = bind.serverTextView.getText() != null && !bind.serverTextView.getText().toString().trim().isBlank() ? bind.serverTextView.getText().toString().trim() : null;
localAddress = bind.localAddressTextView.getText() != null && !bind.localAddressTextView.getText().toString().trim().isBlank() ? bind.localAddressTextView.getText().toString().trim() : null;
lowSecurity = bind.lowSecurityCheckbox.isChecked();
if (TextUtils.isEmpty(serverName)) {
@@ -114,6 +122,11 @@ public class ServerSignupDialog extends DialogFragment {
return false;
}
if (!TextUtils.isEmpty(localAddress) && !localAddress.matches("^https?://(.*)")) {
bind.localAddressTextView.setError(getString(R.string.error_server_prefix));
return false;
}
if (!server.matches("^https?://(.*)")) {
bind.serverTextView.setError(getString(R.string.error_server_prefix));
return false;
@@ -124,6 +137,6 @@ public class ServerSignupDialog extends DialogFragment {
private void saveServerPreference() {
String serverID = loginViewModel.getServerToEdit() != null ? loginViewModel.getServerToEdit().getServerId() : UUID.randomUUID().toString();
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, System.currentTimeMillis(), this.lowSecurity));
loginViewModel.addServer(new Server(serverID, this.serverName, this.username, this.password, this.server, this.localAddress, System.currentTimeMillis(), this.lowSecurity));
}
}

View File

@@ -56,7 +56,7 @@ public class ServerUnreachableDialog extends DialogFragment {
});
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
Preferences.setServerUnreachableDatetime(System.currentTimeMillis());
Preferences.setServerUnreachableDatetime();
alertDialog.dismiss();
});
}

View File

@@ -0,0 +1,76 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.DialogFragment;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogStreamingCacheStorageBinding;
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@OptIn(markerClass = UnstableApi.class)
public class StreamingCacheStorageDialog extends DialogFragment {
private final DialogClickCallback dialogClickCallback;
public StreamingCacheStorageDialog(DialogClickCallback dialogClickCallback) {
this.dialogClickCallback = dialogClickCallback;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
DialogStreamingCacheStorageBinding bind = DialogStreamingCacheStorageBinding.inflate(getLayoutInflater());
return new MaterialAlertDialogBuilder(getActivity())
.setView(bind.getRoot())
.setTitle(R.string.streaming_cache_storage_dialog_title)
.setPositiveButton(R.string.streaming_cache_storage_external_dialog_positive_button, null)
.setNegativeButton(R.string.streaming_cache_storage_internal_dialog_negative_button, null)
.create();
}
@Override
public void onResume() {
super.onResume();
setButtonAction();
}
private void setButtonAction() {
androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog();
if (dialog != null) {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
int currentPreference = Preferences.getStreamingCacheStoragePreference();
int newPreference = 1;
if (currentPreference != newPreference) {
Preferences.setStreamingCacheStoragePreference(newPreference);
dialogClickCallback.onPositiveClick();
}
dialog.dismiss();
});
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
negativeButton.setOnClickListener(v -> {
int currentPreference = Preferences.getStreamingCacheStoragePreference();
int newPreference = 0;
if (currentPreference != newPreference) {
Preferences.setStreamingCacheStoragePreference(newPreference);
dialogClickCallback.onNegativeClick();
}
dialog.dismiss();
});
}
}
}

View File

@@ -17,7 +17,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class TrackInfoDialog extends DialogFragment {
private DialogTrackInfoBinding bind;
private MediaMetadata mediaMetadata;
private final MediaMetadata mediaMetadata;
public TrackInfoDialog(MediaMetadata mediaMetadata) {
this.mediaMetadata = mediaMetadata;
@@ -28,7 +29,7 @@ public class TrackInfoDialog extends DialogFragment {
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogTrackInfoBinding.inflate(getLayoutInflater());
return new MaterialAlertDialogBuilder(getActivity())
return new MaterialAlertDialogBuilder(requireActivity())
.setView(bind.getRoot())
.setPositiveButton(R.string.track_info_dialog_positive_button, (dialog, id) -> dialog.cancel())
.create();

View File

@@ -52,6 +52,12 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
initData();
}
@Override
public void onDestroy() {
super.onDestroy();
albumCatalogueViewModel.stopLoading();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@@ -61,6 +67,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
initAppBar();
initAlbumCatalogueView();
initProgressLoader();
return view;
}
@@ -73,7 +80,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
private void initData() {
albumCatalogueViewModel = new ViewModelProvider(requireActivity()).get(AlbumCatalogueViewModel.class);
albumCatalogueViewModel.loadAlbums(500);
albumCatalogueViewModel.loadAlbums();
}
private void initAppBar() {
@@ -105,7 +112,7 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
bind.albumCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.albumCatalogueRecyclerView.setHasFixedSize(true);
albumAdapter = new AlbumCatalogueAdapter(this);
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));
@@ -118,6 +125,18 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
bind.albumListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_album_popup_menu));
}
private void initProgressLoader() {
albumCatalogueViewModel.getLoadingStatus().observe(getViewLifecycleOwner(), isLoading -> {
if (isLoading) {
bind.albumListSortImageView.setEnabled(false);
bind.albumListProgressLoader.setVisibility(View.VISIBLE);
} else {
bind.albumListSortImageView.setEnabled(true);
bind.albumListProgressLoader.setVisibility(View.GONE);
}
});
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
@@ -165,6 +184,15 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
} else if (menuItem.getItemId() == R.id.menu_album_sort_random) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RANDOM);
return true;
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_ADDED);
return true;
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED);
return true;
} else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_PLAYED);
return true;
}
return false;

View File

@@ -1,11 +1,21 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.PopupMenu;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
@@ -17,12 +27,14 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentAlbumListPageBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.viewmodel.AlbumListPageViewModel;
import java.util.List;
@OptIn(markerClass = UnstableApi.class)
public class AlbumListPageFragment extends Fragment implements ClickCallback {
private FragmentAlbumListPageBinding bind;
@@ -31,6 +43,12 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
private AlbumListPageViewModel albumListPageViewModel;
private AlbumHorizontalAdapter albumHorizontalAdapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@@ -74,7 +92,7 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
} else if (requireArguments().getParcelable(Constants.ARTIST_OBJECT) != null) {
albumListPageViewModel.artist = requireArguments().getParcelable(Constants.ARTIST_OBJECT);
albumListPageViewModel.title = Constants.ALBUM_FROM_ARTIST;
bind.pageTitleLabel.setText(MusicUtil.getReadableString(albumListPageViewModel.artist.getName()));
bind.pageTitleLabel.setText(albumListPageViewModel.artist.getName());
}
}
@@ -86,7 +104,10 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.toolbar.setNavigationOnClickListener(v -> {
hideKeyboard(v);
activity.navController.navigateUp();
});
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.albumInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
@@ -97,6 +118,7 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
});
}
@SuppressLint("ClickableViewAccessibility")
private void initAlbumListView() {
bind.albumListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.albumListRecyclerView.setHasFixedSize(true);
@@ -107,7 +129,99 @@ public class AlbumListPageFragment extends Fragment implements ClickCallback {
);
bind.albumListRecyclerView.setAdapter(albumHorizontalAdapter);
albumListPageViewModel.getAlbumList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> albumHorizontalAdapter.setItems(albums));
albumListPageViewModel.getAlbumList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
albumHorizontalAdapter.setItems(albums);
setAlbumListPageSubtitle(albums);
setAlbumListPageSorter();
});
bind.albumListRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
return false;
});
bind.albumListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_horizontal_album_popup_menu));
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchView.clearFocus();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
albumHorizontalAdapter.getFilter().filter(newText);
return false;
}
});
searchView.setPadding(-32, 0, 0, 0);
}
private void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
private void showPopupMenu(View view, int menuResource) {
PopupMenu popup = new PopupMenu(requireContext(), view);
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
popup.setOnMenuItemClickListener(menuItem -> {
if (menuItem.getItemId() == R.id.menu_horizontal_album_sort_name) {
albumHorizontalAdapter.sort(Constants.ALBUM_ORDER_BY_NAME);
return true;
} else if (menuItem.getItemId() == R.id.menu_horizontal_album_sort_most_recently_starred) {
albumHorizontalAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_RECENTLY_STARRED);
return true;
} else if (menuItem.getItemId() == R.id.menu_horizontal_album_sort_least_recently_starred) {
albumHorizontalAdapter.sort(Constants.ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED);
return true;
}
return false;
});
popup.show();
}
private void setAlbumListPageSubtitle(List<AlbumID3> albums) {
switch (albumListPageViewModel.title) {
case Constants.ALBUM_RECENTLY_PLAYED:
case Constants.ALBUM_MOST_PLAYED:
case Constants.ALBUM_RECENTLY_ADDED:
bind.pageSubtitleLabel.setText(albums.size() < albumListPageViewModel.maxNumber ?
getString(R.string.generic_list_page_count, albums.size()) :
getString(R.string.generic_list_page_count_unknown, albumListPageViewModel.maxNumber)
);
break;
case Constants.ALBUM_STARRED:
bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, albums.size()));
break;
}
}
private void setAlbumListPageSorter() {
switch (albumListPageViewModel.title) {
case Constants.ALBUM_RECENTLY_PLAYED:
case Constants.ALBUM_MOST_PLAYED:
case Constants.ALBUM_RECENTLY_ADDED:
bind.albumListSortImageView.setVisibility(View.GONE);
break;
case Constants.ALBUM_STARRED:
bind.albumListSortImageView.setVisibility(View.VISIBLE);
break;
}
}
@Override

View File

@@ -1,6 +1,8 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -45,9 +47,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
private FragmentAlbumPageBinding bind;
private MainActivity activity;
private AlbumPageViewModel albumPageViewModel;
private SongHorizontalAdapter songHorizontalAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
@@ -73,6 +73,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
init();
initAppBar();
initAlbumInfoTextButton();
initAlbumNotes();
initMusicButton();
initBackCover();
initSongsView();
@@ -103,10 +104,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_download_album) {
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));
});
return true;
}
@@ -115,7 +113,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
}
private void init() {
albumPageViewModel.setAlbum(requireArguments().getParcelable(Constants.ALBUM_OBJECT));
albumPageViewModel.setAlbum(getViewLifecycleOwner(), requireArguments().getParcelable(Constants.ALBUM_OBJECT));
}
private void initAppBar() {
@@ -124,17 +122,48 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.animToolbar.setTitle(MusicUtil.getReadableString(albumPageViewModel.getAlbum().getName()));
albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> {
if (bind != null && album != null) {
bind.animToolbar.setTitle(album.getName());
bind.albumNameLabel.setText(MusicUtil.getReadableString(albumPageViewModel.getAlbum().getName()));
bind.albumArtistLabel.setText(MusicUtil.getReadableString(albumPageViewModel.getAlbum().getArtist()));
bind.albumReleaseYearLabel.setText(albumPageViewModel.getAlbum().getYear() != 0 ? String.valueOf(albumPageViewModel.getAlbum().getYear()) : "");
bind.albumNameLabel.setText(album.getName());
bind.albumArtistLabel.setText(album.getArtist());
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0));
bind.albumGenresTextview.setText(album.getGenre());
if (album.getReleaseDate() != null && album.getOriginalReleaseDate() != null) {
bind.albumReleaseYearsTextview.setVisibility(View.VISIBLE);
if (album.getReleaseDate() == null || album.getOriginalReleaseDate() == null) {
bind.albumReleaseYearsTextview.setText(getString(R.string.album_page_release_date_label, album.getReleaseDate() != null ? album.getReleaseDate().getFormattedDate() : album.getOriginalReleaseDate().getFormattedDate()));
}
if (album.getReleaseDate() != null && album.getOriginalReleaseDate() != null) {
if (Objects.equals(album.getReleaseDate().getYear(), album.getOriginalReleaseDate().getYear()) && Objects.equals(album.getReleaseDate().getMonth(), album.getOriginalReleaseDate().getMonth()) && Objects.equals(album.getReleaseDate().getDay(), album.getOriginalReleaseDate().getDay())) {
bind.albumReleaseYearsTextview.setText(getString(R.string.album_page_release_date_label, album.getReleaseDate().getFormattedDate()));
} else {
bind.albumReleaseYearsTextview.setText(getString(R.string.album_page_release_dates_label, album.getReleaseDate().getFormattedDate(), album.getOriginalReleaseDate().getFormattedDate()));
}
}
}
}
});
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
Objects.requireNonNull(bind.animToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
bind.albumOtherInfoButton.setOnClickListener(v -> {
if (bind.albumDetailView.getVisibility() == View.GONE) {
bind.albumDetailView.setVisibility(View.VISIBLE);
} else if (bind.albumDetailView.getVisibility() == View.VISIBLE) {
bind.albumDetailView.setVisibility(View.GONE);
}
});
}
private void initAlbumInfoTextButton() {
@@ -148,6 +177,26 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
}));
}
private void initAlbumNotes() {
albumPageViewModel.getAlbumInfo().observe(getViewLifecycleOwner(), albumInfo -> {
if (albumInfo != null) {
if (bind != null) bind.albumNotesTextview.setVisibility(View.VISIBLE);
if (bind != null)
bind.albumNotesTextview.setText(MusicUtil.forceReadableString(albumInfo.getNotes()));
if (bind != null && albumInfo.getLastFmUrl() != null && !albumInfo.getLastFmUrl().isEmpty()) {
bind.albumNotesTextview.setOnClickListener(v -> {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(albumInfo.getLastFmUrl()));
startActivity(intent);
});
}
} else {
if (bind != null) bind.albumNotesTextview.setVisibility(View.GONE);
}
});
}
private void initMusicButton() {
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
if (bind != null && !songs.isEmpty()) {
@@ -171,20 +220,25 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
}
private void initBackCover() {
CustomGlideRequest.Builder
.from(requireContext(), albumPageViewModel.getAlbum().getCoverArtId(), CustomGlideRequest.ResourceType.Album)
.build()
.into(bind.albumCoverImageView);
albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> {
if (bind != null && album != null) {
CustomGlideRequest.Builder.from(requireContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album).build().into(bind.albumCoverImageView);
}
});
}
private void initSongsView() {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
albumPageViewModel.getAlbum().observe(getViewLifecycleOwner(), album -> {
if (bind != null && album != null) {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
}
});
}
private void initializeMediaBrowser() {

View File

@@ -1,11 +1,21 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.PopupMenu;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -16,11 +26,15 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentArtistListPageBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.ArtistHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.ArtistListPageViewModel;
import java.util.List;
@UnstableApi
public class ArtistListPageFragment extends Fragment implements ClickCallback {
private FragmentArtistListPageBinding bind;
@@ -30,6 +44,12 @@ public class ArtistListPageFragment extends Fragment implements ClickCallback {
private ArtistHorizontalAdapter artistHorizontalAdapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@@ -69,7 +89,10 @@ public class ArtistListPageFragment extends Fragment implements ClickCallback {
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.toolbar.setNavigationOnClickListener(v -> {
hideKeyboard(v);
activity.navController.navigateUp();
});
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.artistInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
@@ -80,18 +103,100 @@ public class ArtistListPageFragment extends Fragment implements ClickCallback {
});
}
@SuppressLint("ClickableViewAccessibility")
private void initArtistListView() {
bind.artistListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.artistListRecyclerView.setHasFixedSize(true);
artistHorizontalAdapter = new ArtistHorizontalAdapter(this);
bind.artistListRecyclerView.setAdapter(artistHorizontalAdapter);
artistListPageViewModel.getArtistList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> artistHorizontalAdapter.setItems(artists));
artistListPageViewModel.getArtistList(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), artists -> {
artistHorizontalAdapter.setItems(artists);
setArtistListPageSubtitle(artists);
setArtistListPageSorter();
});
bind.artistListRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
return false;
});
bind.artistListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_horizontal_artist_popup_menu));
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchView.clearFocus();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
artistHorizontalAdapter.getFilter().filter(newText);
return false;
}
});
searchView.setPadding(-32, 0, 0, 0);
}
private void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
private void showPopupMenu(View view, int menuResource) {
PopupMenu popup = new PopupMenu(requireContext(), view);
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
popup.setOnMenuItemClickListener(menuItem -> {
if (menuItem.getItemId() == R.id.menu_horizontal_artist_sort_name) {
artistHorizontalAdapter.sort(Constants.ARTIST_ORDER_BY_NAME);
return true;
} else if (menuItem.getItemId() == R.id.menu_horizontal_artist_sort_most_recently_starred) {
artistHorizontalAdapter.sort(Constants.ARTIST_ORDER_BY_MOST_RECENTLY_STARRED);
return true;
} else if (menuItem.getItemId() == R.id.menu_horizontal_artist_sort_least_recently_starred) {
artistHorizontalAdapter.sort(Constants.ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED);
return true;
}
return false;
});
popup.show();
}
private void setArtistListPageSubtitle(List<ArtistID3> artists) {
switch (artistListPageViewModel.title) {
case Constants.ARTIST_STARRED:
case Constants.ARTIST_DOWNLOADED:
bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, artists.size()));
break;
}
}
private void setArtistListPageSorter() {
switch (artistListPageViewModel.title) {
case Constants.ARTIST_STARRED:
case Constants.ARTIST_DOWNLOADED:
bind.artistListSortImageView.setVisibility(View.VISIBLE);
break;
}
}
@Override
public void onArtistClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.albumListPageFragment, bundle);
Navigation.findNavController(requireView()).navigate(R.id.artistPageFragment, bundle);
}
@Override

View File

@@ -27,16 +27,23 @@ import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter;
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@UnstableApi
public class ArtistPageFragment extends Fragment implements ClickCallback {
private FragmentArtistPageBinding bind;
@@ -44,9 +51,8 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private ArtistPageViewModel artistPageViewModel;
private SongHorizontalAdapter songHorizontalAdapter;
private AlbumArtistPageOrSimilarAdapter albumArtistPageOrSimilarAdapter;
private AlbumCatalogueAdapter albumCatalogueAdapter;
private ArtistSimilarAdapter artistSimilarAdapter;
private ArtistCatalogueAdapter artistCatalogueAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@@ -63,8 +69,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
initArtistInfo();
initPlayButtons();
initTopSongsView();
initHorizontalAlbumsView();
initVerticalAlbumsView();
initAlbumsView();
initSimilarArtistsView();
return view;
@@ -98,13 +103,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
bundle.putParcelable(Constants.ARTIST_OBJECT, artistPageViewModel.getArtist());
activity.navController.navigate(R.id.action_artistPageFragment_to_songListPageFragment, bundle);
});
bind.artistPageAlbumsSwitchLayoutTextViewClickable.setOnClickListener(view -> {
boolean isHorizontalRecyclerViewVisible = bind.albumsHorizontalRecyclerView.getVisibility() == View.VISIBLE;
bind.albumsHorizontalRecyclerView.setVisibility(isHorizontalRecyclerViewVisible ? View.GONE : View.VISIBLE);
bind.albumsVerticalRecyclerView.setVisibility(isHorizontalRecyclerViewVisible ? View.VISIBLE : View.GONE);
});
}
private void initAppBar() {
@@ -112,7 +110,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
if (activity.getSupportActionBar() != null)
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
bind.collapsingToolbar.setTitle(MusicUtil.getReadableString(artistPageViewModel.getArtist().getName()));
bind.collapsingToolbar.setTitle(artistPageViewModel.getArtist().getName());
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.collapsingToolbar.setExpandedTitleColor(getResources().getColor(R.color.white, null));
}
@@ -120,8 +118,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private void initArtistInfo() {
artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artistInfo -> {
if (artistInfo == null) {
if (bind != null)
bind.artistPageBioPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.artistPageBioSector.setVisibility(View.GONE);
} else {
String normalizedBio = MusicUtil.forceReadableString(artistInfo.getBiography());
@@ -144,8 +140,6 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
startActivity(intent);
});
if (bind != null)
bind.artistPageBioPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null) bind.artistPageBioSector.setVisibility(View.VISIBLE);
}
});
@@ -154,7 +148,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private void initPlayButtons() {
bind.artistPageShuffleButton.setOnClickListener(v -> {
artistPageViewModel.getArtistShuffleList().observe(getViewLifecycleOwner(), songs -> {
if (songs.size() > 0) {
if (!songs.isEmpty()) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
} else {
@@ -165,7 +159,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
bind.artistPageRadioButton.setOnClickListener(v -> {
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
if (songs.size() > 0) {
if (!songs.isEmpty()) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
} else {
@@ -178,62 +172,33 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private void initTopSongsView() {
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
if (bind != null)
bind.artistPageTopTracksPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.artistPageTopTracksPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE);
if (bind != null)
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
songHorizontalAdapter.setItems(songs);
}
});
}
private void initHorizontalAlbumsView() {
bind.albumsHorizontalRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
private void initAlbumsView() {
bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.albumsRecyclerView.setHasFixedSize(true);
albumArtistPageOrSimilarAdapter = new AlbumArtistPageOrSimilarAdapter(this);
bind.albumsHorizontalRecyclerView.setAdapter(albumArtistPageOrSimilarAdapter);
artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null)
bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
albumArtistPageOrSimilarAdapter.setItems(albums);
}
});
CustomLinearSnapHelper albumSnapHelper = new CustomLinearSnapHelper();
albumSnapHelper.attachToRecyclerView(bind.albumsHorizontalRecyclerView);
}
private void initVerticalAlbumsView() {
bind.albumsVerticalRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.albumsVerticalRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.albumsVerticalRecyclerView.setHasFixedSize(true);
albumCatalogueAdapter = new AlbumCatalogueAdapter(this);
bind.albumsVerticalRecyclerView.setAdapter(albumCatalogueAdapter);
albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false);
bind.albumsRecyclerView.setAdapter(albumCatalogueAdapter);
artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> {
if (albums == null) {
if (bind != null)
bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.artistPageAlbumPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE);
albumCatalogueAdapter.setItems(albums);
@@ -242,22 +207,27 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
}
private void initSimilarArtistsView() {
bind.similarArtistsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.similarArtistsRecyclerView.setHasFixedSize(true);
artistSimilarAdapter = new ArtistSimilarAdapter(this);
bind.similarArtistsRecyclerView.setAdapter(artistSimilarAdapter);
artistCatalogueAdapter = new ArtistCatalogueAdapter(this);
bind.similarArtistsRecyclerView.setAdapter(artistCatalogueAdapter);
artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artist -> {
if (artist == null) {
if (bind != null)
bind.artistPageSimilarArtistPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.similarArtistSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.artistPageSimilarArtistPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
if (bind != null && artist.getSimilarArtists() != null)
bind.similarArtistSector.setVisibility(!artist.getSimilarArtists().isEmpty() ? View.VISIBLE : View.GONE);
artistSimilarAdapter.setItems(artist.getSimilarArtists());
List<ArtistID3> artists = new ArrayList<>();
if (artist.getSimilarArtists() != null) {
artists.addAll(artist.getSimilarArtists());
}
artistCatalogueAdapter.setItems(artists);
}
});

View File

@@ -33,6 +33,7 @@ import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -110,20 +111,14 @@ public class DownloadFragment extends Fragment implements ClickCallback {
if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.VISIBLE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE);
bind.downloadDownloadedPlaceholder.placeholder.setVisibility(View.VISIBLE);
bind.downloadDownloadedSector.setVisibility(View.GONE);
bind.downloadedGroupByImageView.setVisibility(View.GONE);
}
} else {
if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.GONE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE);
bind.downloadDownloadedPlaceholder.placeholder.setVisibility(View.GONE);
bind.downloadDownloadedSector.setVisibility(View.VISIBLE);
bind.downloadedGroupByImageView.setVisibility(View.VISIBLE);
finishDownloadView(songs);
@@ -165,6 +160,19 @@ public class DownloadFragment extends Fragment implements ClickCallback {
bind.downloadedGoBackImageView.setVisibility(stack.size() > 1 ? View.VISIBLE : View.GONE);
setupBackPressing(stack.size());
setupShuffleButton();
});
}
private void setupShuffleButton() {
bind.shuffleDownloadedTextViewClickable.setOnClickListener(view -> {
List<Child> songs = downloadHorizontalAdapter.getShuffling();
if (songs != null && !songs.isEmpty()) {
Collections.shuffle(songs);
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
}
});
}

View File

@@ -91,7 +91,7 @@ public class FilterFragment extends Fragment {
bind.filterContainer.setVisibility(View.VISIBLE);
for (Genre genre : genres) {
Chip chip = (Chip) requireActivity().getLayoutInflater().inflate(R.layout.chip_search_filter_genre, null, false);
chip.setText(MusicUtil.getReadableString(genre.getGenre()));
chip.setText(genre.getGenre());
chip.setChecked(filterViewModel.getFilters().contains(genre.getGenre()));
chip.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked)

View File

@@ -105,7 +105,7 @@ public class GenreCatalogueFragment extends Fragment implements ClickCallback {
genreCatalogueAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
bind.genreCatalogueRecyclerView.setAdapter(genreCatalogueAdapter);
genreCatalogueViewModel.getGenreList().observe(getViewLifecycleOwner(), genres -> genreCatalogueAdapter.setItems(genres));
genreCatalogueViewModel.getGenreList().observe(getViewLifecycleOwner(), genres -> genreCatalogueAdapter.setItems(genres) );
bind.genreCatalogueRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);

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