218 Commits
3.5.7 ... 3.9.0

Author SHA1 Message Date
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
antonio
112e468c7d gradle: bump up code version 2024-01-03 12:47:08 +01:00
antonio
0ccc601f1b gradle: add db migrations 2024-01-03 12:46:45 +01:00
CappielloAntonio
ae82bcd7bf Update github_release.yml 2024-01-03 12:40:21 +01:00
antonio
68512b7e12 feat: Implemented search functionality for Android Auto, "Made for You" section, starred songs, albums, artists, podcasts, and radio 2024-01-03 12:23:46 +01:00
antonio
d6cc4fc028 feat: test: Implemented initial functional version with Android Auto support 2024-01-03 00:45:22 +01:00
antonio
e8c7c065e2 gradle: dependencies update 2023-12-31 15:35:15 +01:00
CappielloAntonio
427394a105 Merge pull request #121 from DelightLane/localization
feat: add korean localization
2023-12-21 09:55:09 +01:00
CappielloAntonio
75fc2cbafa Merge pull request #118 from rkowalsk/french-localization-updates
fix: update French localization and fix previous errors
2023-12-21 09:53:54 +01:00
antonio
b9a3393b39 style: code cleanup 2023-12-21 09:52:10 +01:00
antonio
ed60677608 feat: cleaned up MediaService class, added support for Android Auto repository 2023-12-21 09:52:00 +01:00
antonio
1a407341ec feat: cleaned up MediaService class, added support for Android Auto repository 2023-12-21 09:51:48 +01:00
antonio
567350771c feat: implemented an embryonic version of Android Auto support 2023-12-21 09:49:39 +01:00
antonio
47bef77723 feat: declared Android Auto support 2023-12-21 09:47:39 +01:00
DelightLane
12f09b2201 fix: null checking for song without duration info 2023-12-21 11:43:22 +09:00
DelightLane
77d0b4182e feat: add korean localization 2023-12-21 11:36:57 +09:00
Romain Kowalski
caf64e0fa4 feat: Update French localization and fix previous errors 2023-12-18 13:23:07 +01:00
antonio
ac90b89099 fix: network security configuration 2023-12-12 21:57:53 +01:00
antonio
4523bb8e49 feat: implemented horizontal layout for music player 2023-12-12 21:28:49 +01:00
antonio
06f4898892 refactor/fix: renamed method name to be more descriptive and manually collapse bottomSheet on device state change 2023-12-12 21:28:02 +01:00
antonio
5bbab10485 fix: null checking 2023-12-12 21:24:52 +01:00
antonio
8fe66d058e gradle: build:gradle update 2023-12-10 17:32:03 +01:00
CappielloAntonio
0c2b18326e gradle: build:gradle update 2023-12-10 16:32:40 +01:00
antonio
6a893ac424 Merge remote-tracking branch 'origin/main' 2023-11-29 19:22:41 +01:00
antonio
f184ace301 gradle: dependencies update 2023-11-29 19:22:25 +01:00
antonio
121c2b33da feat: added filter for songs that don't meet a user defined rating threshold 2023-11-29 19:21:42 +01:00
CappielloAntonio
215f1021d6 Merge pull request #108 from ixff/main
feat: add Simplified Chinese localization
2023-11-29 11:39:16 +01:00
ixff
ce6159ad93 feat: add Simplified Chinese localization 2023-11-27 23:08:31 +08:00
ixff
746ea93dbe chore: fix some typos 2023-11-27 22:54:24 +08:00
antonio
612c05fabc fix: increased button's tap area to facilitate easier clicking 2023-11-26 19:28:21 +01:00
antonio
dfc246d079 feat: increased buffering multiplier limits 2023-11-26 19:14:42 +01:00
antonio
0a5983e32b feat: implemented streaming buffering strategy 2023-11-26 19:10:21 +01:00
antonio
9fde629233 fix: network security configuration with cleartext traffic opt-out 2023-11-26 17:36:38 +01:00
antonio
e089c5e466 gradle: bump up code version 2023-11-26 17:27:55 +01:00
antonio
94b0572958 feat: network security configuration 2023-11-26 17:22:38 +01:00
antonio
133b5e4794 feat: implemented settings for enabling or disabling music scrobbling 2023-11-26 16:39:05 +01:00
antonio
6b0ba573de feat: improved live transcoding UI 2023-11-26 16:20:07 +01:00
antonio
6ae7fc2172 gradle: dependencies update 2023-11-26 16:18:47 +01:00
antonio
9cabfb0e2c style: PR code cleanup 2023-11-05 17:10:13 +01:00
antonio
17f05cb3d8 Merge remote-tracking branch 'origin/main' 2023-11-05 16:44:23 +01:00
CappielloAntonio
f2839f4ff9 Merge pull request #100 from kingbluezback/main
style: updated dialog graphics to comply with Material You guidelines
2023-11-05 16:44:05 +01:00
antonio
9aadcb91fb fix: null checking 2023-11-05 16:43:13 +01:00
antonio
5d4dfe1ac7 gradle: gradle and dependencies update 2023-11-05 16:07:12 +01:00
kingbluezback
d3d76dd435 Change Dialogs
Update the dialogs to use material you theme.
2023-10-22 20:42:40 -07:00
kingbluezback
0c3086d68b Update activity_main.xml
Update the apps background color for material you.
2023-10-22 20:35:41 -07:00
kingbluezback
9b3b9771e6 Update strings.xml
When editing a playlist the title is "create playlist" instead of "edit playlist"
2023-10-22 20:33:51 -07:00
antonio
a92403bda4 style: sort translation strings 2023-10-14 16:09:19 +02:00
antonio
67905affd3 feat: added podcast description to the right page of the player 2023-10-12 22:57:38 +02:00
antonio
c20cadff94 clean: cleaned up the podcast listening UI 2023-10-12 22:36:48 +02:00
antonio
0cc45b2b12 clean: code cleanup 2023-10-12 22:36:03 +02:00
antonio
b6063f6b20 fix: forced media type during playback 2023-10-12 22:35:34 +02:00
antonio
678c61e569 clean: removed unused properties of podcast episodes 2023-10-12 22:34:48 +02:00
antonio
e88d6d7844 fix: music player now stays closed on startup if there are no songs in the queue 2023-10-12 21:31:46 +02:00
antonio
bae43cbfe9 gradle: dependencies update 2023-10-12 21:11:24 +02:00
antonio
68c77cefff feat: added songBottomSheet to track files within directories 2023-10-11 22:49:26 +02:00
antonio
75980ee18b feat: show download button only if there are music files in the current directory fragment 2023-10-11 22:20:50 +02:00
antonio
ed30198c8d feat: implemented playlist deletion starting from current track 2023-10-11 21:48:04 +02:00
antonio
185a671f82 fix: resolved an issue with podcast cover image display 2023-10-11 20:55:33 +02:00
antonio
42905819a7 Merge remote-tracking branch 'origin/main' 2023-10-07 16:01:40 +02:00
CappielloAntonio
93d6faa392 github: update crash-report.md 2023-10-07 15:52:28 +02:00
CappielloAntonio
64337775d6 Merge pull request #97 from rkowalsk/main
feat: added french localization
2023-10-07 15:47:29 +02:00
Romain Kowalski
d690cf37fb fix: shortened Wifi transcoding string that was overflowing 2023-10-05 16:56:14 +02:00
Romain Kowalski
32c8ef3b25 Add French localization 2023-10-05 16:23:17 +02:00
antonio
607c68967d gradle: dependencies update 2023-09-29 11:11:30 +02:00
antonio
7fb9f63d1f Merge remote-tracking branch 'origin/main' 2023-09-17 22:33:39 +02:00
antonio
a04a2072f7 feat: implemented a second grid layout for album display on the artist page 2023-09-17 22:33:25 +02:00
antonio
72bd71dea2 fix: error handling 2023-09-17 22:04:07 +02:00
CappielloAntonio
f87328f874 Merge pull request #84 from GallowsDove/templates
style: use proper markdown for issue templates
2023-09-17 21:16:54 +02:00
GallowsDove
4d9b71d48e fix: fix indent in templates 2023-09-17 20:02:43 +02:00
GallowsDove
06f8bc771a feat: Use markdown for github templates 2023-09-17 19:59:27 +02:00
252 changed files with 19997 additions and 2402 deletions

View File

@@ -7,27 +7,29 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
## Describe the bug
<!-- A clear and concise description of what the bug is. -->
**Expected behavior**
A clear and concise description of what you expected to happen.
## Expected behavior
<!-- A clear and concise description of what you expected to happen. -->
**To Reproduce**
## To Reproduce
<!--
Outline the steps required to reproduce the bug, including any specific actions, inputs, or conditions:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
**Environment**
## Environment
- Android device: [Device Model]
- Android OS version: [Android Version]
- App version: [App Version]
- Other relevant details: [e.g., specific network conditions, external dependencies]
**Logs or Screenshots**
If applicable, add screenshots to help explain your problem.
## Logs or Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
**Additional context**
Add any other context about the problem here.
## Additional context
<!-- Add any other context about the problem here. -->

View File

@@ -7,33 +7,35 @@ assignees: ''
---
**Description**
Provide a clear and concise description of the crash you encountered.
## Description
<!-- Provide a clear and concise description of the crash you encountered. -->
**Steps to Reproduce**
## Steps to Reproduce
<!--
Please provide the steps to reproduce the crash:
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See the crash
-->
**Environment**
## Environment
- Android device: [Device Model]
- Android OS version: [Android Version]
- App version: [App Version]
- Other relevant details: [e.g., specific network conditions, external dependencies]
**Crash Logs/Stack Trace**
If available, please provide the crash log or stack trace related to the crash. Include it inside a code block (surround with triple backticks ```).
## Crash Logs/Stack trace
<!-- If available, please provide the crash log or stack trace related to the crash. Include it inside a code block (surround with triple backticks ```). Please use the unsigned apk (app-tempo-debug.apk), as the logs would be illegible and therefore useless for this purpose. -->
**Screenshots**
If applicable, add screenshots to help explain the problem.
## Screenshots
<!-- If applicable, add screenshots to help explain the problem. -->
**Additional Context**
Add any other context about the problem here.
## Additional Context
<!-- Add any other context about the problem here. -->
**Reproducibility**
Mention the frequency of the crash occurrence (e.g., always, sometimes, occasionally).
## Reproducibility
<!-- Mention the frequency of the crash occurrence (e.g., always, sometimes, occasionally). -->
**Additional Notes**
Include any other notes or details that could be helpful for troubleshooting the crash.
## Additional Notes
<!-- Include any other notes or details that could be helpful for troubleshooting the crash. -->

View File

@@ -7,14 +7,14 @@ assignees: ''
---
**Summary**
Provide a concise summary of the feature you are requesting.
## Summary
<!-- Provide a concise summary of the feature you are requesting. -->
**Description**
Please describe in detail the feature you would like to see implemented.
## Description
<!-- Please describe in detail the feature you would like to see implemented. -->
**Use Case**
Explain why this feature is important and how it would improve the user experience.
## Use Case
<!-- Explain why this feature is important and how it would improve the user experience. -->
**Additional context**
Include any additional information, screenshots, or examples that could be helpful in understanding or implementing the feature.
## Additional context
<!-- Include any additional information, screenshots, or examples that could be helpful in understanding or implementing the feature. -->

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup JDK 17
uses: actions/setup-java@v2
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
@@ -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,6 +6,12 @@
<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>
<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.

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 25
versionName '3.8.1'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -22,21 +25,22 @@ android {
}
}
flavorDimensions "default"
flavorDimensions += "default"
productFlavors {
tempo {
dimension "default"
dimension = "default"
applicationId 'com.cappielloantonio.tempo'
versionCode 22
versionName '3.5.7'
}
notquitemy {
dimension "default"
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.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.2'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.8.5'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
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.9.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.1.1'
implementation 'androidx.media3:media3-common:1.1.1'
implementation 'androidx.media3:media3-exoplayer:1.1.1'
implementation 'androidx.media3:media3-ui:1.1.1'
tempoImplementation 'androidx.media3:media3-cast:1.1.1'
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.5.2'
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.11'
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

View File

@@ -0,0 +1,997 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "528d037bee0f0575f8e0670ae1b04e00",
"entities": [
{
"tableName": "queue",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `track_order` INTEGER NOT NULL, `last_play` INTEGER NOT NULL, `playing_changed` INTEGER NOT NULL, `stream_id` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`track_order`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "trackOrder",
"columnName": "track_order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastPlay",
"columnName": "last_play",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "playingChanged",
"columnName": "playing_changed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamId",
"columnName": "stream_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"track_order"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `server_name` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `low_security` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverName",
"columnName": "server_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "address",
"columnName": "address",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isLowSecurity",
"columnName": "low_security",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "false"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "recent_search",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`search` TEXT NOT NULL, PRIMARY KEY(`search`))",
"fields": [
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"search"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlist_id` TEXT, `playlist_name` TEXT, `download_state` INTEGER NOT NULL DEFAULT 1, `download_uri` TEXT DEFAULT '', `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "playlistName",
"columnName": "playlist_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "downloadState",
"columnName": "download_state",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "downloadUri",
"columnName": "download_uri",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "chronology",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `server` TEXT, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "server",
"columnName": "server",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorite",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `songId` TEXT, `albumId` TEXT, `artistId` TEXT, `toStar` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))",
"fields": [
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "toStar",
"columnName": "toStar",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"timestamp"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "session_media_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent_id` TEXT, `is_dir` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `artist` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `cover_art_id` TEXT, `size` INTEGER, `content_type` TEXT, `suffix` TEXT, `transcoding_content_type` TEXT, `transcoded_suffix` TEXT, `duration` INTEGER, `bitrate` INTEGER, `path` TEXT, `is_video` INTEGER NOT NULL, `user_rating` INTEGER, `average_rating` REAL, `play_count` INTEGER, `disc_number` INTEGER, `created` INTEGER, `starred` INTEGER, `album_id` TEXT, `artist_id` TEXT, `type` TEXT, `bookmark_position` INTEGER, `original_width` INTEGER, `original_height` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parentId",
"columnName": "parent_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDir",
"columnName": "is_dir",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArtId",
"columnName": "cover_art_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcoding_content_type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcoded_suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitrate",
"columnName": "bitrate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "is_video",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "user_rating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "average_rating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "playCount",
"columnName": "play_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "disc_number",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "album_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artist_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmark_position",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalWidth",
"columnName": "original_width",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "originalHeight",
"columnName": "original_height",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '528d037bee0f0575f8e0670ae1b04e00')"
]
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -16,11 +16,17 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locale_config"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme.SplashScreen"
android:usesCleartextTraffic="true">
<!-- Declare that this session demo supports Android Auto. -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/auto_app_desc" />
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
@@ -45,7 +51,8 @@
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="androidx.media3.session.MediaBrowserService" />
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
</intent-filter>
</service>

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,20 +12,25 @@ 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;
import com.cappielloantonio.tempo.database.dao.SessionMediaItemDao;
import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Favorite;
import com.cappielloantonio.tempo.model.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 = 3,
entities = {Queue.class, Server.class, RecentSearch.class, Download.class, Chronology.class, Favorite.class},
autoMigrations = {@AutoMigration(from = 2, to = 3)}
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 {
@@ -52,4 +58,8 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract ChronologyDao chronologyDao();
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.database.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.model.SessionMediaItem;
import java.util.List;
@Dao
public interface SessionMediaItemDao {
@Query("SELECT * FROM session_media_item WHERE id = :id")
SessionMediaItem get(String id);
@Query("SELECT * FROM session_media_item WHERE timestamp = :timestamp")
List<SessionMediaItem> get(long timestamp);
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insert(SessionMediaItem sessionMediaItem);
@Insert(onConflict = OnConflictStrategy.IGNORE)
void insertAll(List<SessionMediaItem> sessionMediaItems);
@Query("DELETE FROM session_media_item")
void deleteAll();
}

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,32 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
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,42 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
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,44 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
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,26 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
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,42 @@
package com.cappielloantonio.tempo.github.models
import com.google.gson.annotations.SerializedName
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

@@ -0,0 +1,281 @@
package com.cappielloantonio.tempo.model
import android.net.Uri
import android.os.Bundle
import androidx.annotation.Keep
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.RequestMetadata
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.cappielloantonio.tempo.glide.CustomGlideRequest
import com.cappielloantonio.tempo.subsonic.models.Child
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode
import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.MusicUtil
import com.cappielloantonio.tempo.util.Preferences.getImageSize
import java.util.Date
@UnstableApi
@Keep
@Entity(tableName = "session_media_item")
class SessionMediaItem() {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "index")
var index: Int = 0
@ColumnInfo(name = "id")
var id: String? = null
@ColumnInfo(name = "parent_id")
var parentId: String? = null
@ColumnInfo(name = "is_dir")
var isDir: Boolean = false
@ColumnInfo
var title: String? = null
@ColumnInfo
var album: String? = null
@ColumnInfo
var artist: String? = null
@ColumnInfo
var track: Int? = null
@ColumnInfo
var year: Int? = null
@ColumnInfo
var genre: String? = null
@ColumnInfo(name = "cover_art_id")
var coverArtId: String? = null
@ColumnInfo
var size: Long? = null
@ColumnInfo(name = "content_type")
var contentType: String? = null
@ColumnInfo
var suffix: String? = null
@ColumnInfo("transcoding_content_type")
var transcodedContentType: String? = null
@ColumnInfo(name = "transcoded_suffix")
var transcodedSuffix: String? = null
@ColumnInfo
var duration: Int? = null
@ColumnInfo("bitrate")
var bitrate: Int? = null
@ColumnInfo
var path: String? = null
@ColumnInfo(name = "is_video")
var isVideo: Boolean = false
@ColumnInfo(name = "user_rating")
var userRating: Int? = null
@ColumnInfo(name = "average_rating")
var averageRating: Double? = null
@ColumnInfo(name = "play_count")
var playCount: Long? = null
@ColumnInfo(name = "disc_number")
var discNumber: Int? = null
@ColumnInfo
var created: Date? = null
@ColumnInfo
var starred: Date? = null
@ColumnInfo(name = "album_id")
var albumId: String? = null
@ColumnInfo(name = "artist_id")
var artistId: String? = null
@ColumnInfo
var type: String? = null
@ColumnInfo(name = "bookmark_position")
var bookmarkPosition: Long? = null
@ColumnInfo(name = "original_width")
var originalWidth: Int? = null
@ColumnInfo(name = "original_height")
var originalHeight: Int? = null
@ColumnInfo(name = "stream_id")
var streamId: String? = null
@ColumnInfo(name = "stream_url")
var streamUrl: String? = null
@ColumnInfo(name = "timestamp")
var timestamp: Long? = null
constructor(child: Child) : this() {
id = child.id
parentId = child.parentId
isDir = child.isDir
title = child.title
album = child.album
artist = child.artist
track = child.track
year = child.year
genre = child.genre
coverArtId = child.coverArtId
size = child.size
contentType = child.contentType
suffix = child.suffix
transcodedContentType = child.transcodedContentType
transcodedSuffix = child.transcodedSuffix
duration = child.duration
bitrate = child.bitrate
path = child.path
isVideo = child.isVideo
userRating = child.userRating
averageRating = child.averageRating
playCount = child.playCount
discNumber = child.discNumber
created = child.created
starred = child.starred
albumId = child.albumId
artistId = child.artistId
type = Constants.MEDIA_TYPE_MUSIC
bookmarkPosition = child.bookmarkPosition
originalWidth = child.originalWidth
originalHeight = child.originalHeight
}
constructor(podcastEpisode: PodcastEpisode) : this() {
id = podcastEpisode.id
parentId = podcastEpisode.parentId
isDir = podcastEpisode.isDir
title = podcastEpisode.title
album = podcastEpisode.album
artist = podcastEpisode.artist
year = podcastEpisode.year
genre = podcastEpisode.genre
coverArtId = podcastEpisode.coverArtId
size = podcastEpisode.size
contentType = podcastEpisode.contentType
suffix = podcastEpisode.suffix
duration = podcastEpisode.duration
bitrate = podcastEpisode.bitrate
path = podcastEpisode.path
isVideo = podcastEpisode.isVideo
created = podcastEpisode.created
artistId = podcastEpisode.artistId
streamId = podcastEpisode.streamId
type = Constants.MEDIA_TYPE_PODCAST
}
constructor(internetRadioStation: InternetRadioStation) : this() {
id = internetRadioStation.id
title = internetRadioStation.name
streamUrl = internetRadioStation.streamUrl
type = Constants.MEDIA_TYPE_RADIO
}
fun getMediaItem(): MediaItem {
val uri: Uri = getStreamUri()
val artworkUri = Uri.parse(CustomGlideRequest.createUrl(coverArtId, getImageSize()))
val bundle = Bundle()
bundle.putString("id", id)
bundle.putString("parentId", parentId)
bundle.putBoolean("isDir", isDir)
bundle.putString("title", title)
bundle.putString("album", album)
bundle.putString("artist", artist)
bundle.putInt("track", track ?: 0)
bundle.putInt("year", year ?: 0)
bundle.putString("genre", genre)
bundle.putString("coverArtId", coverArtId)
bundle.putLong("size", size ?: 0)
bundle.putString("contentType", contentType)
bundle.putString("suffix", suffix)
bundle.putString("transcodedContentType", transcodedContentType)
bundle.putString("transcodedSuffix", transcodedSuffix)
bundle.putInt("duration", duration ?: 0)
bundle.putInt("bitrate", bitrate ?: 0)
bundle.putString("path", path)
bundle.putBoolean("isVideo", isVideo)
bundle.putInt("userRating", userRating ?: 0)
bundle.putDouble("averageRating", averageRating ?: .0)
bundle.putLong("playCount", playCount ?: 0)
bundle.putInt("discNumber", discNumber ?: 0)
bundle.putLong("created", created?.time ?: 0)
bundle.putLong("starred", starred?.time ?: 0)
bundle.putString("albumId", albumId)
bundle.putString("artistId", artistId)
bundle.putString("type", Constants.MEDIA_TYPE_MUSIC)
bundle.putLong("bookmarkPosition", bookmarkPosition ?: 0)
bundle.putInt("originalWidth", originalWidth ?: 0)
bundle.putInt("originalHeight", originalHeight ?: 0)
bundle.putString("uri", uri.toString())
return MediaItem.Builder()
.setMediaId(id!!)
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(title)
.setTrackNumber(track ?: 0)
.setDiscNumber(discNumber ?: 0)
.setReleaseYear(year ?: 0)
.setAlbumTitle(album)
.setArtist(artist)
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
)
.setRequestMetadata(
RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(bundle)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build()
}
private fun getStreamUri(): Uri {
return when (type) {
Constants.MEDIA_TYPE_MUSIC -> {
MusicUtil.getStreamUri(id)
}
Constants.MEDIA_TYPE_PODCAST -> {
MusicUtil.getStreamUri(streamId)
}
Constants.MEDIA_TYPE_RADIO -> {
Uri.parse(streamUrl)
}
else -> {
MusicUtil.getStreamUri(id)
}
}
}
}

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

File diff suppressed because it is too large Load Diff

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,11 +1,12 @@
package com.cappielloantonio.tempo.repository;
import android.util.Log;
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;
@@ -19,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<>());
@@ -155,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

@@ -49,12 +49,14 @@ public class SharingRepository {
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getShares() != null && response.body().getSubsonicResponse().getShares().getShares() != null && response.body().getSubsonicResponse().getShares().getShares().get(0) != null) {
share.setValue(response.body().getSubsonicResponse().getShares().getShares().get(0));
} else {
share.setValue(null);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
share.setValue(null);
}
});

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;
@@ -12,6 +20,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation;
import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@@ -254,6 +263,21 @@ public class MediaManager {
}
}
public static void removeRange(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, int fromItem, int toItem) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().removeMediaItems(fromItem, toItem);
removeRangeDatabase(media, fromItem, toItem);
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, MoreExecutors.directExecutor());
}
}
public static void getCurrentIndex(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, MediaIndexCallback callback) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
@@ -277,9 +301,33 @@ public class MediaManager {
getQueueRepository().setPlayingPausedTimestamp(mediaItem.mediaId, ms);
}
public static void scrobble(MediaItem mediaItem) {
if (mediaItem != null) {
getSongRepository().scrobble(mediaItem.mediaMetadata.extras.getString("id"));
public static void scrobble(MediaItem mediaItem, boolean submission) {
if (mediaItem != null && Preferences.isScrobblingEnabled()) {
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);
}
});
}
}
@@ -320,6 +368,14 @@ public class MediaManager {
}
}
private static void removeRangeDatabase(List<Child> media, int fromItem, int toItem) {
List<Child> toRemove = media.subList(fromItem, toItem);
media.removeAll(toRemove);
getQueueRepository().insertAll(media, true, 0);
}
public static void clearDatabase() {
getQueueRepository().deleteAll();
}

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 MediaLibraryScanningClient {
private static final String TAG = "SystemClient";
private static final String TAG = "MediaLibraryScanningClient";
private final Subsonic subsonic;
private final MediaLibraryScanningService mediaLibraryScanningService;

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

@@ -9,7 +9,7 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import retrofit2.Call;
public class PodcastClient {
private static final String TAG = "SystemClient";
private static final String TAG = "PodcastClient";
private final Subsonic subsonic;
private final PodcastService podcastService;

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

@@ -14,6 +14,7 @@ class PodcastChannel : Parcelable {
var url: String? = null
var title: String? = null
var description: String? = null
@SerializedName("coverArt")
var coverArtId: String? = null
var originalImageUrl: String? = null
var status: String? = null

View File

@@ -12,12 +12,10 @@ class PodcastEpisode : Parcelable {
var id: String? = null
@SerializedName("parent")
var parentId: String? = null
@SerializedName("isDir")
var isDir = false
var title: String? = null
var album: String? = null
var artist: String? = null
var track: Int? = null
var year: Int? = null
var genre: String? = null
@SerializedName("coverArt")
@@ -25,26 +23,14 @@ class PodcastEpisode : Parcelable {
var size: Long? = null
var contentType: String? = null
var suffix: String? = null
var transcodedContentType: String? = null
var transcodedSuffix: String? = null
var duration: Int? = null
@SerializedName("bitRate")
var bitrate: Int? = null
var path: String? = null
@SerializedName("isVideo")
var isVideo: Boolean = false
var userRating: Int? = null
var averageRating: Double? = null
var playCount: Long? = null
var discNumber: Int? = null
var created: Date? = null
var starred: Date? = null
var albumId: String? = null
var artistId: String? = null
var type: String? = null
var bookmarkPosition: Long? = null
var originalWidth: Int? = null
var originalHeight: Int? = null
var streamId: String? = null
var channelId: String? = null
var description: String? = 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
@@ -94,7 +101,7 @@ public class MainActivity extends BaseActivity {
@Override
public void onBackPressed() {
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
collapseBottomSheet();
collapseBottomSheetDelayed();
else
super.onBackPressed();
}
@@ -118,9 +125,7 @@ public class MainActivity extends BaseActivity {
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
fragmentManager.beginTransaction().replace(R.id.player_bottom_sheet, new PlayerBottomSheetFragment(), "PlayerBottomSheet").commit();
setBottomSheetInPeek(mainViewModel.isQueueLoaded());
collapseBottomSheet();
checkBottomSheetAfterStateChanged();
}
public void setBottomSheetInPeek(Boolean isVisible) {
@@ -139,7 +144,13 @@ public class MainActivity extends BaseActivity {
}
}
public void collapseBottomSheet() {
private void checkBottomSheetAfterStateChanged() {
final Handler handler = new Handler();
final Runnable runnable = () -> setBottomSheetInPeek(mainViewModel.isQueueLoaded());
handler.postDelayed(runnable, 100);
}
public void collapseBottomSheetDelayed() {
final Handler handler = new Handler();
final Runnable runnable = () -> bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
handler.postDelayed(runnable, 100);
@@ -163,7 +174,7 @@ public class MainActivity extends BaseActivity {
switch (state) {
case BottomSheetBehavior.STATE_HIDDEN:
hideMusicSession();
resetMusicSession();
break;
case BottomSheetBehavior.STATE_COLLAPSED:
if (playerBottomSheetFragment != null)
@@ -296,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);
@@ -329,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

@@ -77,6 +77,8 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
item.musicDirectoryTitleTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.musicDirectoryMoreButton.setOnClickListener(v -> onClick());
}
@@ -92,5 +94,18 @@ public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAd
click.onMediaClick(bundle);
}
}
private boolean onLongClick() {
if (!children.get(getBindingAdapterPosition()).isDir()) {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.TRACK_OBJECT, children.get(getBindingAdapterPosition()));
click.onMediaLongClick(bundle);
return true;
} else {
return false;
}
}
}
}

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,11 +38,11 @@ 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
.from(holder.itemView.getContext(), podcastChannel.getOriginalImageUrl(), CustomGlideRequest.ResourceType.Podcast)
.from(holder.itemView.getContext(), podcastChannel.getCoverArtId(), CustomGlideRequest.ResourceType.Podcast)
.build()
.into(holder.item.podcastChannelCoverImageView);
}

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

@@ -1,6 +1,5 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
@@ -14,38 +13,23 @@ import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogBatteryOptimizationBinding;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@OptIn(markerClass = UnstableApi.class)
public class BatteryOptimizationDialog extends DialogFragment {
private static final String TAG = "BatteryOptimizationDialog";
private DialogBatteryOptimizationBinding bind;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogBatteryOptimizationBinding.inflate(getLayoutInflater());
DialogBatteryOptimizationBinding bind = DialogBatteryOptimizationBinding.inflate(getLayoutInflater());
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(bind.getRoot())
return new MaterialAlertDialogBuilder(requireContext())
.setView(bind.getRoot())
.setTitle(R.string.activity_battery_optimizations_title)
.setNeutralButton(R.string.battery_optimization_neutral_button, (dialog, id) -> Preferences.dontAskForOptimization())
.setPositiveButton(R.string.battery_optimization_positive_button, (dialog, listener) -> openPowerSettings())
.setNeutralButton(R.string.battery_optimization_neutral_button, (dialog, listener) -> Preferences.dontAskForOptimization())
.setNegativeButton(R.string.battery_optimization_negative_button, null)
.setPositiveButton(R.string.battery_optimization_positive_button, (dialog, id) -> openPowerSettings());
AlertDialog popup = builder.create();
popup.setCancelable(false);
popup.setCanceledOnTouchOutside(false);
return popup;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
.create();
}
private void openPowerSettings() {

View File

@@ -1,6 +1,5 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.os.Bundle;
@@ -10,22 +9,18 @@ import androidx.fragment.app.DialogFragment;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogConnectionAlertBinding;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.Objects;
public class ConnectionAlertDialog extends DialogFragment {
private static final String TAG = "ServerUnreachableDialog";
private DialogConnectionAlertBinding bind;
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
bind = DialogConnectionAlertBinding.inflate(getLayoutInflater());
DialogConnectionAlertBinding bind = DialogConnectionAlertBinding.inflate(getLayoutInflater());
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(bind.getRoot())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
.setView(bind.getRoot())
.setTitle(R.string.connection_alert_dialog_title)
.setPositiveButton(R.string.connection_alert_dialog_positive_button, (dialog, id) -> dialog.cancel())
.setNegativeButton(R.string.connection_alert_dialog_negative_button, (dialog, id) -> dialog.cancel());
@@ -45,16 +40,12 @@ public class ConnectionAlertDialog extends DialogFragment {
setButtonAction();
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void setButtonAction() {
((AlertDialog) Objects.requireNonNull(getDialog())).getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
androidx.appcompat.app.AlertDialog alertDialog = (androidx.appcompat.app.AlertDialog) Objects.requireNonNull(getDialog());
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
Preferences.setDataSavingMode(true);
Objects.requireNonNull(getDialog()).dismiss();
});
}
}
}

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