91 Commits
3.8.0 ... main

Author SHA1 Message Date
CappielloAntonio
9cf62c8c0c gradle: update gradle build tools 2025-01-31 11:05:39 +01:00
CappielloAntonio
330556ec33 Merge remote-tracking branch 'origin/main' 2025-01-31 10:32:53 +01:00
CappielloAntonio
4c8c5ce120 gradle: dependencies update 2025-01-31 10:32:39 +01:00
CappielloAntonio
55ae9a8442 fix: added missing decorators 2025-01-31 10:32:00 +01:00
CappielloAntonio
f8a53c7db2 Update README.md 2024-12-31 16:55:21 +01:00
CappielloAntonio
b58cae1ecd Update README.md 2024-12-31 16:51:37 +01:00
CappielloAntonio
e305f20811 fix: updated workflow 2024-12-31 12:54:28 +01:00
CappielloAntonio
c8c1bcfd3e fix: null checking 2024-12-30 17:23:55 +01:00
CappielloAntonio
e1c96d278f fix: null checking 2024-12-30 17:20:13 +01:00
CappielloAntonio
3783a2f790 fix: escape character 2024-12-30 17:17:05 +01:00
CappielloAntonio
63e288d147 Merge pull request #316 from potatoenergy/main
feature: update ru-RU translation
2024-12-30 16:53:40 +01:00
CappielloAntonio
d3ca43ed49 fix: delete deprecated code to check connectivity 2024-12-30 16:45:38 +01:00
CappielloAntonio
f0d31e425a fix: temporary opt out edge to edge enforcement 2024-12-30 16:45:07 +01:00
CappielloAntonio
609fc70d33 fix: temporary opt out edge to edge enforcement 2024-12-30 16:38:43 +01:00
CappielloAntonio
0b92c40d51 gradle: dependencies update 2024-12-30 16:37:53 +01:00
ponfertato
eb6a2b609e feat: update Russian localization 2024-12-25 18:07:18 +03:00
ponfertato
20ffc960df feat: fix build Russian localization 2024-12-25 17:56:34 +03:00
CappielloAntonio
48d9022f9a feat: add sorting and search functionality for album and artist list 2024-11-23 16:00:01 +01:00
CappielloAntonio
9e6926fc97 feat: add sorting and search functionality for song list 2024-11-22 21:57:27 +01:00
CappielloAntonio
780f1c3a2e feat: implemented additional sorting options for albums in the catalog screen 2024-11-20 23:02:43 +01:00
CappielloAntonio
0f471a7b9f feat: add search functionality for songs within playlists 2024-11-20 22:20:37 +01:00
CappielloAntonio
4ec1519063 feat: add ALAC codec support via Media3 FFmpeg module 2024-11-20 21:23:56 +01:00
CappielloAntonio
618cf23e6e gradle: dependencies update 2024-11-20 15:10:25 +01:00
CappielloAntonio
e8e24354ec fix: change the server address to the backup one if the first address is unreachable 2024-11-06 17:43:18 +01:00
CappielloAntonio
fd0fd0546c gradle: gradle update 2024-11-06 17:39:42 +01:00
CappielloAntonio
030ca82c3a fix: fixed the pull request for the Italian translation 2024-11-06 17:17:17 +01:00
CappielloAntonio
0e6b860e03 Merge remote-tracking branch 'origin/main' 2024-11-06 16:57:57 +01:00
CappielloAntonio
8c288e8938 Merge pull request #299 from SaregoA/main
feat: Added Italian translation
2024-11-06 16:57:17 +01:00
CappielloAntonio
4e968f3397 Merge pull request #286 from skyline75489/skyline/zh-cn-2
feat: update zh-cn translation
2024-11-06 16:55:46 +01:00
CappielloAntonio
b1e0f49ddb gradle: dependencies update 2024-11-06 15:43:49 +01:00
andrea sarego
73a1ab2330 Traduzione in italiano 2024-11-04 20:31:40 +01:00
Chester Liu
8540348670 Update zh-cn translation part 2 2024-08-31 19:07:40 +08:00
CappielloAntonio
67e4079732 fix: null checking 2024-08-30 17:19:58 +02:00
CappielloAntonio
6c6d56f451 Merge pull request #251 from albertcanales/main
feat: open album when clicking the song's title on player
2024-08-30 16:55:00 +02:00
CappielloAntonio
f967df8ef3 Merge remote-tracking branch 'origin/main' 2024-08-30 16:47:37 +02:00
CappielloAntonio
c5a78bf945 fix: strengthened controls on the presence of server address strings (local and remote) 2024-08-30 16:47:15 +02:00
CappielloAntonio
6f51fd92bb style: code cleanup 2024-08-30 15:51:49 +02:00
CappielloAntonio
3c2ea38f1e fix: further eliminate the use of HTML decode in titles and subtitles 2024-08-30 15:27:16 +02:00
CappielloAntonio
edf140fb5d Merge pull request #285 from skyline75489/skyline/zh-cn-1
Update zh-cn translation
2024-08-30 15:16:31 +02:00
Chester Liu
049ce7713a Update zh-cn translation 2024-08-29 20:27:31 +08:00
CappielloAntonio
8c49ceffdb Update privacy.html 2024-08-29 12:07:59 +02:00
CappielloAntonio
052e9d9068 Merge remote-tracking branch 'origin/main' 2024-08-28 14:25:31 +02:00
CappielloAntonio
4d1213c43d gradle: dependencies update 2024-08-28 14:25:19 +02:00
CappielloAntonio
1ec0c7b99c Merge pull request #264 from dnno/main
feat: update german localization
2024-08-22 16:58:13 +02:00
CappielloAntonio
07f8914a9f Merge pull request #261 from florent4014/patch-1
feat: update strings.xml
2024-08-22 16:57:35 +02:00
CappielloAntonio
965a80462c Merge remote-tracking branch 'origin/main' 2024-08-22 16:12:03 +02:00
CappielloAntonio
349c961f1a repo: add play variant 2024-08-22 16:11:56 +02:00
CappielloAntonio
eb9f824c01 gradle: gradle update 2024-08-22 15:20:51 +02:00
CappielloAntonio
e465892013 repo: add privacy policy page 2024-08-21 14:44:38 +02:00
CappielloAntonio
a49c78b9f1 gradle: dependencies update 2024-08-21 14:42:46 +02:00
Ryan Harg
436ef21a29 Update german localization 2024-07-03 10:12:38 +02:00
florent4014
be8decfac3 Update strings.xml
Corrected some minor mistakes
2024-06-24 13:54:29 +02:00
Albert Canales Ros
7c87ec2cbe feat: clicking the song's title opens the album on player 2024-06-08 23:08:39 +02:00
CappielloAntonio
fb7296b467 feat: added the ability to pin playlists to the home screen 2024-06-08 18:53:58 +02:00
CappielloAntonio
078aa87521 feat: added long press to delete gesture 2024-06-08 16:49:04 +02:00
CappielloAntonio
54a4355793 feat: added release date and original release date to album notes, if available 2024-06-02 20:19:18 +02:00
CappielloAntonio
e84f62220c fix: Implemented continuous playing in com.cappielloantonio.notquitemy.tempo 2024-06-02 19:26:32 +02:00
CappielloAntonio
176db09662 feat: Implemented continuous playing 2024-06-02 19:18:16 +02:00
CappielloAntonio
2c3aebc83b feat: Added API call to retrieve AlbumID3 details in album page as the object passed from search and album list are different 2024-06-02 17:07:40 +02:00
CappielloAntonio
a67571ee4f feat: reduced time to wait before trying to connect to local address 2024-06-02 16:21:29 +02:00
CappielloAntonio
79f5b9b158 feat: reduced time to wait before trying to connect to local address 2024-06-02 16:15:20 +02:00
CappielloAntonio
f6b176a357 feat: added the ability for the user to add a local server address and use that address when available 2024-06-01 15:23:40 +02:00
CappielloAntonio
aa5290c7ee style: code cleanup 2024-05-31 22:41:44 +02:00
CappielloAntonio
0a26f0a7b8 feat: implemented version number control and update dialog for Github flavor 2024-05-31 22:41:01 +02:00
CappielloAntonio
c243fa9edc gradle: dependencies update 2024-05-31 20:28:22 +02:00
CappielloAntonio
f94e5892cd feat: edited the interface of top songs divided by week, month, and year 2024-05-26 19:38:17 +02:00
CappielloAntonio
477331da6f feat: added external memory cache option 2024-05-26 14:49:57 +02:00
CappielloAntonio
c4e8fe5261 gradle: gradle update 2024-05-26 12:44:22 +02:00
CappielloAntonio
92fd6b01e4 feat: read hls data source 2024-05-26 11:05:43 +02:00
CappielloAntonio
263d9ebc5f fix: null checking 2024-05-26 01:13:41 +02:00
CappielloAntonio
f6578afb14 fxi: removed readable string rework 2024-05-26 00:04:42 +02:00
CappielloAntonio
41b374ab23 style: delete unused code 2024-05-25 23:43:46 +02:00
CappielloAntonio
4448a632af fix: order downloaded tracks by artist, album, disc_number and finally track number 2024-05-25 23:27:28 +02:00
CappielloAntonio
b3b1c5b006 feat: updated the horizontal song adapter with the disc name information 2024-05-25 22:33:26 +02:00
CappielloAntonio
25900c848a feat: extended the Albums model according to the OpenSubsonic API project indications 2024-05-25 22:31:30 +02:00
CappielloAntonio
08e9be107b fix: setAllowCrossProtocolRedirects to httpDataSourceFactory 2024-05-25 19:12:10 +02:00
CappielloAntonio
6cbb2ee117 fix: set content description to fab 2024-05-25 18:21:19 +02:00
CappielloAntonio
dacaa03eb7 style: code clean up 2024-05-25 17:55:30 +02:00
CappielloAntonio
a3d8b75d07 Merge remote-tracking branch 'origin/main' 2024-05-25 17:26:08 +02:00
CappielloAntonio
d08c113d99 Merge pull request #190 from kmod-midori/streaming-cache
feat: cache streaming contents
2024-05-25 17:25:23 +02:00
CappielloAntonio
2db716a79c gradle: gradle update 2024-05-25 15:35:38 +02:00
CappielloAntonio
71b913be9b Merge remote-tracking branch 'origin/main'
# Conflicts:
#	build.gradle
2024-05-25 15:26:37 +02:00
CappielloAntonio
fb353a33d9 Merge pull request #220 from Sevinfolds/values-ru
feat: add russian localization
2024-05-25 13:08:54 +02:00
CappielloAntonio
240498c219 gradle: dependencies update 2024-05-25 13:07:26 +02:00
Sevinfolds
1eac053d2d Update strings.xml
Edits and clarifications of the translation
2024-04-16 14:36:35 +03:00
Sevinfolds
160222563c Create arrays.xml
Add arrays.xml
2024-04-16 11:26:46 +03:00
Sevinfolds
4ec3d6bde7 Create strings.xml
adding the Russian language
2024-04-16 11:02:47 +03:00
CappielloAntonio
e0f276dd2a fastlane: update fastlane data 2024-03-25 10:00:30 +01:00
CappielloAntonio
acee7f8fa9 gradle: versionName update 2024-03-25 09:57:01 +01:00
Midori Kochiya
3d3d0fa856 Try to cache streaming contents 2024-03-11 17:21:28 +08:00
CappielloAntonio
0c2b18326e gradle: build:gradle update 2023-12-10 16:32:40 +01:00
144 changed files with 7230 additions and 385 deletions

View File

@@ -52,7 +52,7 @@ jobs:
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}}

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>

1
.idea/gradle.xml generated
View File

@@ -4,6 +4,7 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">

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="jbr-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

@@ -8,6 +8,8 @@
<p align="center">
<a href="https://github.com/CappielloAntonio/tempo/releases"><img src="https://i.ibb.co/q0mdc4Z/get-it-on-github.png" width="200"></a>
</p>
<p align="center">
<a href="https://f-droid.org/packages/com.cappielloantonio.notquitemy.tempo"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" width="200"></a>
<a href="https://apt.izzysoft.de/fdroid/index/apk/com.cappielloantonio.tempo"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="200"></a>
</p>
@@ -18,6 +20,8 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
**If you find Tempo useful, please consider starring the project on GitHub. It would mean a lot to me and help promote the app to a wider audience.**
**Use the Github version of the app for full Android Auto and Chromecast support.**
## Features
- **Subsonic Integration**: Tempo seamlessly integrates with your Subsonic server, providing you with easy access to your entire music collection on the go.
- **Sleek and Intuitive UI**: Enjoy a clean and user-friendly interface designed to enhance your music listening experience, tailored to your preferences and listening history.
@@ -29,6 +33,7 @@ Tempo does not rely on magic algorithms to decide what you should listen to. Ins
- **Scrobbling Integration**: Optionally integrate Tempo with Last.fm to scrobble your played tracks, gather music insights, and further personalize your music recommendations, if supported by your Subsonic server.
- **Podcasts and Radio**: If your Subsonic server supports it, listen to podcasts and radio shows directly within Tempo, expanding your audio entertainment options.
- **Transcoding Support**: Activate transcoding of tracks on your Subsonic server, allowing you to set a transcoding profile for optimized streaming directly from the app. This feature requires support from your Subsonic server.
- **Android Auto Support**: Enjoy your favorite music on the go with full Android Auto integration, allowing you to seamlessly control and listen to your tracks directly from your mobile device while driving.
<p align="center">
<img src="mockup/feat/1_screenshot.png" width=200>

View File

@@ -3,15 +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 24
versionName '3.7.0'
versionCode 26
versionName '3.9.0'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
@@ -37,6 +37,11 @@ android {
dimension = "default"
applicationId "com.cappielloantonio.notquitemy.tempo"
}
play {
dimension = "default"
applicationId "com.cappielloantonio.play.tempo"
}
}
buildTypes {
@@ -59,22 +64,25 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
namespace 'com.cappielloantonio.tempo'
}
dependencies {
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.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.8.6'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.appcompat:appcompat:1.6.1"
implementation 'androidx.appcompat:appcompat:1.7.0'
// Android Material
implementation 'com.google.android.material:material:1.10.0'
@@ -84,17 +92,19 @@ dependencies {
implementation 'com.github.bumptech.glide:annotations:4.16.0'
// Media3
implementation 'androidx.media3:media3-session:1.3.0'
implementation 'androidx.media3:media3-common:1.3.0'
implementation 'androidx.media3:media3-exoplayer:1.3.0'
implementation 'androidx.media3:media3-ui:1.3.0'
tempoImplementation 'androidx.media3:media3-cast:1.3.0'
implementation 'androidx.media3:media3-session:1.5.1'
implementation 'androidx.media3:media3-common:1.5.1'
implementation 'androidx.media3:media3-exoplayer:1.5.1'
implementation 'androidx.media3:media3-ui:1.5.1'
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1'
tempoImplementation 'androidx.media3:media3-cast:1.5.1'
playImplementation 'androidx.media3:media3-cast:1.5.1'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.10.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.retrofit2:converter-gson:2.10.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'
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ public interface ChronologyDao {
@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 >= :startDate AND timestamp < :endDate AND server == :server GROUP BY id ORDER BY COUNT(id) DESC LIMIT 9")
@Query("SELECT * FROM chronology WHERE timestamp >= :endDate AND timestamp < :startDate AND server == :server GROUP BY id ORDER BY COUNT(id) DESC LIMIT 20")
LiveData<List<Chronology>> getAllFrom(long startDate, long endDate, String server);
@Insert(onConflict = OnConflictStrategy.REPLACE)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -1,9 +1,12 @@
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;
@@ -58,6 +61,8 @@ public class SystemRepository {
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null) {
pingResult.postValue(response.body().getSubsonicResponse());
} else {
pingResult.postValue(null);
}
}
@@ -92,4 +97,27 @@ public class SystemRepository {
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

@@ -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;
@@ -299,6 +307,30 @@ public class MediaManager {
}
}
@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);
}
});
}
}
public static void saveChronology(MediaItem mediaItem) {
if (mediaItem != null) {
getChronologyRepository().insert(new Chronology(mediaItem));

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

@@ -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;
@@ -71,6 +76,7 @@ public class MainActivity extends BaseActivity {
init();
checkConnectionType();
getOpenSubsonicExtensions();
checkTempoUpdate();
}
@Override
@@ -301,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);
@@ -334,17 +341,37 @@ public class MainActivity extends BaseActivity {
}
private void pingServer() {
if (Preferences.getToken() != null) {
mainViewModel.ping().observe(this, subsonicResponse -> {
if (subsonicResponse == null && Preferences.showServerUnreachableDialog()) {
ServerUnreachableDialog dialog = new ServerUnreachableDialog();
dialog.show(getSupportFragmentManager(), null);
}
if (Preferences.getToken() == null) return;
if (subsonicResponse != null) {
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());
}
});
}
}
}
@@ -358,6 +385,17 @@ public class MainActivity extends BaseActivity {
}
}
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);
}
});
}
}
private void checkConnectionType() {
if (Preferences.isWifiOnly()) {
ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_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

@@ -80,8 +80,8 @@ 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
@@ -169,6 +169,14 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
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

@@ -185,17 +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.downloadedItemTitleTextView.setText(song.getTitle());
holder.item.downloadedItemSubtitleTextView.setText(
holder.itemView.getContext().getString(
R.string.song_subtitle_formatter,
MusicUtil.getReadableString(song.getArtist()),
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
""
)
);
holder.item.downloadedItemPreTextView.setText(MusicUtil.getReadableString(song.getAlbum()));
holder.item.downloadedItemPreTextView.setText(song.getAlbum());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), song.getCoverArtId(), CustomGlideRequest.ResourceType.Song)
@@ -216,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)
@@ -239,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
@@ -255,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

@@ -48,11 +48,11 @@ 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.queueSongTitleTextView.setText(song.getTitle());
holder.item.queueSongSubtitleTextView.setText(
holder.itemView.getContext().getString(
R.string.song_subtitle_formatter,
MusicUtil.getReadableString(song.getArtist()),
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
MusicUtil.getReadableAudioQualityString(song)
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,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.appcompat.content.res.AppCompatResources;
@@ -14,7 +16,9 @@ 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;
@@ -22,21 +26,61 @@ 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
@@ -50,16 +94,14 @@ 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.searchResultSongTitleTextView.setText(song.getTitle());
holder.item.searchResultSongSubtitleTextView.setText(
holder.itemView.getContext().getString(
R.string.song_subtitle_formatter,
MusicUtil.getReadableString(
this.showAlbum ?
song.getAlbum() :
song.getArtist()
),
this.showAlbum ?
song.getAlbum() :
song.getArtist(),
MusicUtil.getReadableDurationString(song.getDuration(), false),
MusicUtil.getReadableAudioQualityString(song)
)
@@ -81,8 +123,28 @@ 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()) {
@@ -111,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();
}
@@ -125,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);
}
@@ -163,4 +231,20 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
return true;
}
}
public void sort(String order) {
switch (order) {
case Constants.MEDIA_BY_TITLE:
songs.sort(Comparator.comparing(Child::getTitle));
break;
case Constants.MEDIA_MOST_RECENTLY_STARRED:
songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.reverseOrder())));
break;
case Constants.MEDIA_LEAST_RECENTLY_STARRED:
songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.naturalOrder())));
break;
}
notifyDataSetChanged();
}
}

View File

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

View File

@@ -7,6 +7,7 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
@@ -81,7 +82,7 @@ public class PlaylistEditorDialog extends DialogFragment {
playlistEditorViewModel.setPlaylistToEdit(requireArguments().getParcelable(Constants.PLAYLIST_OBJECT));
if (playlistEditorViewModel.getPlaylistToEdit() != null) {
bind.playlistNameTextView.setText(MusicUtil.getReadableString(playlistEditorViewModel.getPlaylistToEdit().getName()));
bind.playlistNameTextView.setText(playlistEditorViewModel.getPlaylistToEdit().getName());
}
}
}
@@ -101,9 +102,12 @@ public class PlaylistEditorDialog extends DialogFragment {
}
});
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> Toast.makeText(requireContext(), R.string.playlist_editor_dialog_action_delete_toast, Toast.LENGTH_SHORT).show());
alertDialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL).setOnLongClickListener(v -> {
playlistEditorViewModel.deletePlaylist();
dialogDismiss();
return false;
});
bind.playlistShareButton.setOnClickListener(view -> {

View File

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

View File

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

View File

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

View File

@@ -187,6 +187,12 @@ public class AlbumCatalogueFragment extends Fragment implements ClickCallback {
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_added) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_ADDED);
return true;
} else if (menuItem.getItemId() == R.id.menu_album_sort_recently_played) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_RECENTLY_PLAYED);
return true;
} else if (menuItem.getItemId() == R.id.menu_album_sort_most_played) {
albumAdapter.sort(Constants.ALBUM_ORDER_BY_MOST_PLAYED);
return true;
}
return false;

View File

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

View File

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

View File

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

View File

@@ -110,7 +110,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
if (activity.getSupportActionBar() != null)
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
bind.collapsingToolbar.setTitle(MusicUtil.getReadableString(artistPageViewModel.getArtist().getName()));
bind.collapsingToolbar.setTitle(artistPageViewModel.getArtist().getName());
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.collapsingToolbar.setExpandedTitleColor(getResources().getColor(R.color.white, null));
}
@@ -172,7 +172,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private void initTopSongsView() {
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -29,8 +30,8 @@ import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.FragmentHomeTabMusicBinding;
import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper;
import com.cappielloantonio.tempo.helper.recyclerview.DotsIndicatorDecoration;
import com.cappielloantonio.tempo.helper.recyclerview.GridItemDecoration;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.interfaces.PlaylistCallback;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.HomeSector;
import com.cappielloantonio.tempo.service.DownloaderManager;
@@ -44,12 +45,13 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistHorizontalAdapter;
import com.cappielloantonio.tempo.ui.adapter.DiscoverSongAdapter;
import com.cappielloantonio.tempo.ui.adapter.GridTrackAdapter;
import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter;
import com.cappielloantonio.tempo.ui.adapter.ShareHorizontalAdapter;
import com.cappielloantonio.tempo.ui.adapter.SimilarTrackAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.ui.adapter.YearAdapter;
import com.cappielloantonio.tempo.ui.dialog.HomeRearrangementDialog;
import com.cappielloantonio.tempo.ui.dialog.PlaylistEditorDialog;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
@@ -62,6 +64,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@UnstableApi
public class HomeTabMusicFragment extends Fragment implements ClickCallback {
@@ -76,6 +79,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private ArtistAdapter radioArtistAdapter;
private ArtistAdapter bestOfArtistAdapter;
private SongHorizontalAdapter starredSongAdapter;
private SongHorizontalAdapter topSongAdapter;
private AlbumHorizontalAdapter starredAlbumAdapter;
private ArtistHorizontalAdapter starredArtistAdapter;
private AlbumAdapter recentlyAddedAlbumAdapter;
@@ -83,7 +87,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private AlbumAdapter mostPlayedAlbumAdapter;
private AlbumHorizontalAdapter newReleasesAlbumAdapter;
private YearAdapter yearAdapter;
private GridTrackAdapter gridTrackAdapter;
private PlaylistHorizontalAdapter playlistHorizontalAdapter;
private ShareHorizontalAdapter shareHorizontalAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@@ -119,7 +123,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
initNewReleasesView();
initYearSongView();
initRecentAddedAlbumView();
initGridView();
initTopSongsView();
initPinnedPlaylistsView();
initSharesView();
initHomeReorganizer();
@@ -253,6 +258,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
homeViewModel.refreshShares(getViewLifecycleOwner());
return true;
});
bind.gridTracksPreTextView.setOnClickListener(view -> showPopupMenu(view, R.menu.filter_top_songs_popup_menu));
}
private void initSyncStarredView() {
@@ -404,30 +411,42 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
artistRadioSnapHelper.attachToRecyclerView(bind.radioArtistRecyclerView);
}
private void initGridView() {
private void initTopSongsView() {
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_TOP_SONGS)) return;
bind.gridTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 3));
bind.gridTracksRecyclerView.addItemDecoration(new GridItemDecoration(3, 8, false));
bind.gridTracksRecyclerView.setHasFixedSize(true);
bind.topSongsRecyclerView.setHasFixedSize(true);
gridTrackAdapter = new GridTrackAdapter(this);
bind.gridTracksRecyclerView.setAdapter(gridTrackAdapter);
topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
if (chronologies == null || chronologies.isEmpty()) {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
if (bind != null) bind.afterGridDivider.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.VISIBLE);
if (bind != null) bind.afterGridDivider.setVisibility(View.VISIBLE);
if (bind != null)
bind.topSongsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(chronologies.size(), 5), GridLayoutManager.HORIZONTAL, false));
homeViewModel.getDiscoverSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), music -> {
if (music != null) {
homeViewModel.getGridSongSample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
if (chronologies == null || chronologies.isEmpty()) {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
if (bind != null) bind.afterGridDivider.setVisibility(View.GONE);
} else {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.VISIBLE);
if (bind != null) bind.afterGridDivider.setVisibility(View.VISIBLE);
gridTrackAdapter.setItems(chronologies);
}
});
List<Child> topSongs = chronologies.stream()
.map(cronologia -> (Child) cronologia)
.collect(Collectors.toList());
topSongAdapter.setItems(topSongs);
}
});
SnapHelper topTrackSnapHelper = new PagerSnapHelper();
topTrackSnapHelper.attachToRecyclerView(bind.topSongsRecyclerView);
bind.topSongsRecyclerView.addItemDecoration(
new DotsIndicatorDecoration(
getResources().getDimensionPixelSize(R.dimen.radius),
getResources().getDimensionPixelSize(R.dimen.radius) * 4,
getResources().getDimensionPixelSize(R.dimen.dots_height),
requireContext().getResources().getColor(R.color.titleTextColor, null),
requireContext().getResources().getColor(R.color.titleTextColor, null))
);
}
private void initStarredTracksView() {
@@ -435,7 +454,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind.starredTracksRecyclerView.setHasFixedSize(true);
starredSongAdapter = new SongHorizontalAdapter(this, true, false);
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
@@ -656,6 +675,26 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
recentAddedAlbumSnapHelper.attachToRecyclerView(bind.recentlyAddedAlbumsRecyclerView);
}
private void initPinnedPlaylistsView() {
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_PINNED_PLAYLISTS)) return;
bind.pinnedPlaylistsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.pinnedPlaylistsRecyclerView.setHasFixedSize(true);
playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this);
bind.pinnedPlaylistsRecyclerView.setAdapter(playlistHorizontalAdapter);
homeViewModel.getPinnedPlaylists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), playlists -> {
if (playlists == null) {
if (bind != null) bind.pinnedPlaylistsSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.pinnedPlaylistsSector.setVisibility(!playlists.isEmpty() ? View.VISIBLE : View.GONE);
playlistHorizontalAdapter.setItems(playlists);
}
});
}
private void initSharesView() {
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_SHARED)) return;
@@ -693,7 +732,9 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private void initHomeReorganizer() {
final Handler handler = new Handler();
final Runnable runnable = () -> { if (bind != null) bind.homeSectorRearrangementButton.setVisibility(View.VISIBLE); };
final Runnable runnable = () -> {
if (bind != null) bind.homeSectorRearrangementButton.setVisibility(View.VISIBLE);
};
handler.postDelayed(runnable, 5000);
bind.homeSectorRearrangementButton.setOnClickListener(v -> {
@@ -774,6 +815,9 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
case Constants.HOME_SECTOR_RECENTLY_ADDED:
bind.homeLinearLayoutContainer.addView(bind.homeRecentlyAddedAlbumsSector);
break;
case Constants.HOME_SECTOR_PINNED_PLAYLISTS:
bind.homeLinearLayoutContainer.addView(bind.pinnedPlaylistsSector);
break;
case Constants.HOME_SECTOR_SHARED:
bind.homeLinearLayoutContainer.addView(bind.sharesSector);
break;
@@ -784,6 +828,42 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
}
}
private void showPopupMenu(View view, int menuResource) {
PopupMenu popup = new PopupMenu(requireContext(), view);
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
popup.setOnMenuItemClickListener(menuItem -> {
if (menuItem.getItemId() == R.id.menu_last_week_name) {
homeViewModel.changeChronologyPeriod(getViewLifecycleOwner(), 0);
bind.gridTracksPreTextView.setText(getString(R.string.home_title_last_week));
return true;
} else if (menuItem.getItemId() == R.id.menu_last_month_name) {
homeViewModel.changeChronologyPeriod(getViewLifecycleOwner(), 1);
bind.gridTracksPreTextView.setText(getString(R.string.home_title_last_month));
return true;
} else if (menuItem.getItemId() == R.id.menu_last_year_name) {
homeViewModel.changeChronologyPeriod(getViewLifecycleOwner(), 2);
bind.gridTracksPreTextView.setText(getString(R.string.home_title_last_year));
return true;
}
return false;
});
popup.show();
}
private void refreshPlaylistView() {
final Handler handler = new Handler();
final Runnable runnable = () -> {
if (getView() != null && bind != null && homeViewModel != null)
homeViewModel.getPinnedPlaylists(getViewLifecycleOwner());
};
handler.postDelayed(runnable, 100);
}
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
@@ -882,6 +962,24 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
startActivity(intent);
}
@Override
public void onPlaylistClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.playlistPageFragment, bundle);
}
@Override
public void onPlaylistLongClick(Bundle bundle) {
PlaylistEditorDialog dialog = new PlaylistEditorDialog(new PlaylistCallback() {
@Override
public void onDismiss() {
refreshPlaylistView();
}
});
dialog.setArguments(bundle);
dialog.show(activity.getSupportFragmentManager(), null);
}
@Override
public void onShareLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle);

View File

@@ -117,12 +117,13 @@ public class LoginFragment extends Fragment implements ClickCallback {
@Override
public void onServerClick(Bundle bundle) {
Server server = bundle.getParcelable("server_object");
saveServerPreference(server.getServerId(), server.getAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity());
saveServerPreference(server.getServerId(), server.getAddress(), server.getLocalAddress(), server.getUsername(), server.getPassword(), server.isLowSecurity());
SystemRepository systemRepository = new SystemRepository();
systemRepository.checkUserCredential(new SystemCallback() {
@Override
public void onError(Exception exception) {
Preferences.switchInUseServerAddress();
resetServerPreference();
Toast.makeText(requireContext(), exception.getMessage(), Toast.LENGTH_SHORT).show();
}
@@ -141,9 +142,10 @@ public class LoginFragment extends Fragment implements ClickCallback {
dialog.show(activity.getSupportFragmentManager(), null);
}
private void saveServerPreference(String serverId, String server, String user, String password, boolean isLowSecurity) {
private void saveServerPreference(String serverId, String server, String localAddress, String user, String password, boolean isLowSecurity) {
Preferences.setServerId(serverId);
Preferences.setServer(server);
Preferences.setLocalAddress(localAddress);
Preferences.setUser(user);
Preferences.setPassword(password);
Preferences.setLowSecurity(isLowSecurity);

View File

@@ -156,11 +156,12 @@ public class PlayerBottomSheetFragment extends Fragment {
private void setMetadata(MediaMetadata mediaMetadata) {
if (mediaMetadata.extras != null) {
playerBottomSheetViewModel.setLiveMedia(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("id"));
playerBottomSheetViewModel.setLiveAlbum(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("albumId"));
playerBottomSheetViewModel.setLiveArtist(getViewLifecycleOwner(), mediaMetadata.extras.getString("type"), mediaMetadata.extras.getString("artistId"));
playerBottomSheetViewModel.setLiveDescription(mediaMetadata.extras.getString("description", null));
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(MusicUtil.getReadableString(mediaMetadata.extras.getString("title")));
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(MusicUtil.getReadableString(mediaMetadata.extras.getString("artist")));
bind.playerHeaderLayout.playerHeaderMediaTitleLabel.setText(mediaMetadata.extras.getString("title"));
bind.playerHeaderLayout.playerHeaderMediaArtistLabel.setText(mediaMetadata.extras.getString("artist"));
CustomGlideRequest.Builder
.from(requireContext(), mediaMetadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)

View File

@@ -76,6 +76,7 @@ public class PlayerControllerFragment extends Fragment {
initQuickActionView();
initCoverLyricsSlideView();
initMediaListenable();
initMediaLabelButton();
initArtistLabelButton();
return view;
@@ -163,8 +164,8 @@ public class PlayerControllerFragment extends Fragment {
}
private void setMetadata(MediaMetadata mediaMetadata) {
playerMediaTitleLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.title)));
playerArtistNameLabel.setText(MusicUtil.getReadableString(String.valueOf(mediaMetadata.artist)));
playerMediaTitleLabel.setText(String.valueOf(mediaMetadata.title));
playerArtistNameLabel.setText(String.valueOf(mediaMetadata.artist));
playerMediaTitleLabel.setSelected(true);
playerArtistNameLabel.setSelected(true);
@@ -299,6 +300,19 @@ public class PlayerControllerFragment extends Fragment {
});
}
private void initMediaLabelButton() {
playerBottomSheetViewModel.getLiveAlbum().observe(getViewLifecycleOwner(), album -> {
if (album != null) {
playerMediaTitleLabel.setOnClickListener(view -> {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.ALBUM_OBJECT, album);
NavHostFragment.findNavController(this).navigate(R.id.albumPageFragment, bundle);
activity.collapseBottomSheetDelayed();
});
}
});
}
private void initArtistLabelButton() {
playerBottomSheetViewModel.getLiveArtist().observe(getViewLifecycleOwner(), artist -> {
if (artist != null) {

View File

@@ -1,6 +1,7 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -8,6 +9,9 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -58,8 +62,29 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.playlist_page_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchView.clearFocus();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
songHorizontalAdapter.getFilter().filter(newText);
return false;
}
});
searchView.setPadding(-32, 0, 0, 0);
initMenuOption(menu);
}
@Override
@@ -115,6 +140,12 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
}
});
return true;
} else if (item.getItemId() == R.id.action_pin_playlist) {
playlistPageViewModel.setPinned(true);
return true;
} else if (item.getItemId() == R.id.action_unpin_playlist) {
playlistPageViewModel.setPinned(false);
return true;
}
return false;
@@ -124,6 +155,13 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
playlistPageViewModel.setPlaylist(requireArguments().getParcelable(Constants.PLAYLIST_OBJECT));
}
private void initMenuOption(Menu menu) {
playlistPageViewModel.isPinned(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), isPinned -> {
menu.findItem(R.id.action_unpin_playlist).setVisible(isPinned);
menu.findItem(R.id.action_pin_playlist).setVisible(!isPinned);
});
}
private void initAppBar() {
activity.setSupportActionBar(bind.animToolbar);
@@ -132,17 +170,25 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.animToolbar.setTitle(MusicUtil.getReadableString(playlistPageViewModel.getPlaylist().getName()));
bind.animToolbar.setTitle(playlistPageViewModel.getPlaylist().getName());
bind.playlistNameLabel.setText(MusicUtil.getReadableString(playlistPageViewModel.getPlaylist().getName()));
bind.playlistNameLabel.setText(playlistPageViewModel.getPlaylist().getName());
bind.playlistSongCountLabel.setText(getString(R.string.playlist_song_count, playlistPageViewModel.getPlaylist().getSongCount()));
bind.playlistDurationLabel.setText(getString(R.string.playlist_duration, MusicUtil.getReadableDurationString(playlistPageViewModel.getPlaylist().getDuration(), false)));
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.animToolbar.setNavigationOnClickListener(v -> {
hideKeyboard(v);
activity.navController.navigateUp();
});
Objects.requireNonNull(bind.animToolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
}
private void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
private void initMusicButton() {
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
if (bind != null) {
@@ -200,7 +246,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));

View File

@@ -90,9 +90,9 @@ public class PodcastChannelPageFragment extends Fragment implements ClickCallbac
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.toolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle()));
bind.toolbar.setTitle(podcastChannelPageViewModel.getPodcastChannel().getTitle());
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.toolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle()));
bind.toolbar.setTitle(podcastChannelPageViewModel.getPodcastChannel().getTitle());
}
private void initPodcastChannelInfo() {

View File

@@ -112,7 +112,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
}

View File

@@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.Intent;
import android.media.audiofx.AudioEffect;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,6 +31,8 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
@@ -82,14 +85,17 @@ public class SettingsFragment extends PreferenceFragmentCompat {
super.onResume();
checkEqualizer();
checkCacheStorage();
checkStorage();
setStreamingCacheSize();
setAppLanguage();
setVersion();
actionLogout();
actionScan();
actionSyncStarredTracks();
actionChangeStreamingCacheStorage();
actionChangeDownloadStorage();
actionDeleteDownloadStorage();
actionKeepScreenOn();
@@ -132,6 +138,22 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
private void checkCacheStorage() {
Preference storage = findPreference("streaming_cache_storage");
if (storage == null) return;
try {
if (requireContext().getExternalFilesDirs(null)[1] == null) {
storage.setVisible(false);
} else {
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
}
} catch (Exception exception) {
storage.setVisible(false);
}
}
private void checkStorage() {
Preference storage = findPreference("download_storage");
@@ -148,6 +170,26 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
private void setStreamingCacheSize() {
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
if (streamingCachePreference != null) {
streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider<ListPreference>() {
@Nullable
@Override
public CharSequence provideSummary(@NonNull ListPreference preference) {
CharSequence entry = preference.getEntry();
if (entry == null) return null;
long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024);
return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb));
}
});
}
}
private void setAppLanguage() {
ListPreference localePref = (ListPreference) findPreference("language");
@@ -190,7 +232,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onSuccess(boolean isScanning, long count) {
getScanStatus();
findPreference("scan_library").setSummary("Scanning: counting " + count + " tracks");
if (isScanning) getScanStatus();
}
});
@@ -210,6 +253,24 @@ public class SettingsFragment extends PreferenceFragmentCompat {
});
}
private void actionChangeStreamingCacheStorage() {
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
@Override
public void onPositiveClick() {
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button);
}
@Override
public void onNegativeClick() {
findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button);
}
});
dialog.show(activity.getSupportFragmentManager(), null);
return true;
});
}
private void actionChangeDownloadStorage() {
findPreference("download_storage").setOnPreferenceClickListener(preference -> {
DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() {

View File

@@ -1,12 +1,22 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.PopupMenu;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
@@ -22,14 +32,15 @@ import com.cappielloantonio.tempo.helper.recyclerview.PaginationScrollListener;
import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.List;
@UnstableApi
public class SongListPageFragment extends Fragment implements ClickCallback {
@@ -45,6 +56,12 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
private boolean isLoading = true;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@@ -95,13 +112,13 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
} else if (requireArguments().getString(Constants.MEDIA_BY_GENRE) != null) {
songListPageViewModel.title = Constants.MEDIA_BY_GENRE;
songListPageViewModel.genre = requireArguments().getParcelable(Constants.GENRE_OBJECT);
songListPageViewModel.toolbarTitle = MusicUtil.getReadableString(songListPageViewModel.genre.getGenre());
bind.pageTitleLabel.setText(MusicUtil.getReadableString(songListPageViewModel.genre.getGenre()));
songListPageViewModel.toolbarTitle = songListPageViewModel.genre.getGenre();
bind.pageTitleLabel.setText(songListPageViewModel.genre.getGenre());
} else if (requireArguments().getString(Constants.MEDIA_BY_ARTIST) != null) {
songListPageViewModel.title = Constants.MEDIA_BY_ARTIST;
songListPageViewModel.artist = requireArguments().getParcelable(Constants.ARTIST_OBJECT);
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_top, MusicUtil.getReadableString(songListPageViewModel.artist.getName()));
bind.pageTitleLabel.setText(getString(R.string.song_list_page_top, MusicUtil.getReadableString(songListPageViewModel.artist.getName())));
songListPageViewModel.toolbarTitle = getString(R.string.song_list_page_top, songListPageViewModel.artist.getName());
bind.pageTitleLabel.setText(getString(R.string.song_list_page_top, songListPageViewModel.artist.getName()));
} else if (requireArguments().getString(Constants.MEDIA_BY_GENRES) != null) {
songListPageViewModel.title = Constants.MEDIA_BY_GENRES;
songListPageViewModel.filters = requireArguments().getStringArrayList("filters_list");
@@ -124,8 +141,8 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
} else if (requireArguments().getParcelable(Constants.ALBUM_OBJECT) != null) {
songListPageViewModel.album = requireArguments().getParcelable(Constants.ALBUM_OBJECT);
songListPageViewModel.title = Constants.MEDIA_FROM_ALBUM;
songListPageViewModel.toolbarTitle = MusicUtil.getReadableString(songListPageViewModel.album.getName());
bind.pageTitleLabel.setText(MusicUtil.getReadableString(songListPageViewModel.album.getName()));
songListPageViewModel.toolbarTitle = songListPageViewModel.album.getName();
bind.pageTitleLabel.setText(songListPageViewModel.album.getName());
}
}
@@ -138,7 +155,10 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
}
if (bind != null)
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.toolbar.setNavigationOnClickListener(v -> {
hideKeyboard(v);
activity.navController.navigateUp();
});
if (bind != null)
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
@@ -153,6 +173,8 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
private void initButtons() {
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
if (bind != null) {
setSongListPageSorter();
bind.songListShuffleImageView.setOnClickListener(v -> {
Collections.shuffle(songs);
MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(25, songs.size())), 0);
@@ -162,15 +184,17 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
});
}
@SuppressLint("ClickableViewAccessibility")
private void initSongListView() {
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songListRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
isLoading = false;
songHorizontalAdapter.setItems(songs);
setSongListPageSubtitle(songs);
});
bind.songListRecyclerView.addOnScrollListener(new PaginationScrollListener((LinearLayoutManager) bind.songListRecyclerView.getLayoutManager()) {
@@ -185,6 +209,101 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
return isLoading;
}
});
bind.songListRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
return false;
});
bind.songListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_song_popup_menu));
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchView.clearFocus();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
songHorizontalAdapter.getFilter().filter(newText);
return false;
}
});
searchView.setPadding(-32, 0, 0, 0);
}
private void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
private void showPopupMenu(View view, int menuResource) {
PopupMenu popup = new PopupMenu(requireContext(), view);
popup.getMenuInflater().inflate(menuResource, popup.getMenu());
popup.setOnMenuItemClickListener(menuItem -> {
if (menuItem.getItemId() == R.id.menu_song_sort_name) {
songHorizontalAdapter.sort(Constants.MEDIA_BY_TITLE);
return true;
} else if (menuItem.getItemId() == R.id.menu_song_sort_most_recently_starred) {
songHorizontalAdapter.sort(Constants.MEDIA_MOST_RECENTLY_STARRED);
return true;
} else if (menuItem.getItemId() == R.id.menu_song_sort_least_recently_starred) {
songHorizontalAdapter.sort(Constants.MEDIA_LEAST_RECENTLY_STARRED);
return true;
}
return false;
});
popup.show();
}
private void setSongListPageSubtitle(List<Child> children) {
switch (songListPageViewModel.title) {
case Constants.MEDIA_BY_GENRE:
bind.pageSubtitleLabel.setText(children.size() < songListPageViewModel.maxNumberByGenre ?
getString(R.string.generic_list_page_count, children.size()) :
getString(R.string.generic_list_page_count_unknown, songListPageViewModel.maxNumberByGenre)
);
break;
case Constants.MEDIA_BY_YEAR:
bind.pageSubtitleLabel.setText(children.size() < songListPageViewModel.maxNumberByYear ?
getString(R.string.generic_list_page_count, children.size()) :
getString(R.string.generic_list_page_count_unknown, songListPageViewModel.maxNumberByYear)
);
break;
case Constants.MEDIA_BY_ARTIST:
case Constants.MEDIA_BY_GENRES:
case Constants.MEDIA_STARRED:
bind.pageSubtitleLabel.setText(getString(R.string.generic_list_page_count, children.size()));
break;
}
}
private void setSongListPageSorter() {
switch (songListPageViewModel.title) {
case Constants.MEDIA_BY_GENRE:
case Constants.MEDIA_BY_YEAR:
bind.songListSortImageView.setVisibility(View.GONE);
break;
case Constants.MEDIA_BY_ARTIST:
case Constants.MEDIA_BY_GENRES:
case Constants.MEDIA_STARRED:
bind.songListSortImageView.setVisibility(View.VISIBLE);
break;
}
}
private void initializeMediaBrowser() {
@@ -197,6 +316,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
@Override
public void onMediaClick(Bundle bundle) {
hideKeyboard(requireView());
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
activity.setBottomSheetInPeek(true);
}

View File

@@ -92,11 +92,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
.into(coverAlbum);
TextView titleAlbum = view.findViewById(R.id.album_title_text_view);
titleAlbum.setText(MusicUtil.getReadableString(albumBottomSheetViewModel.getAlbum().getName()));
titleAlbum.setText(albumBottomSheetViewModel.getAlbum().getName());
titleAlbum.setSelected(true);
TextView artistAlbum = view.findViewById(R.id.album_artist_text_view);
artistAlbum.setText(MusicUtil.getReadableString(albumBottomSheetViewModel.getAlbum().getArtist()));
artistAlbum.setText(albumBottomSheetViewModel.getAlbum().getArtist());
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(albumBottomSheetViewModel.getAlbum().getStarred() != null);

View File

@@ -75,7 +75,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
.into(coverArtist);
TextView nameArtist = view.findViewById(R.id.song_title_text_view);
nameArtist.setText(MusicUtil.getReadableString(artistBottomSheetViewModel.getArtist().getName()));
nameArtist.setText(artistBottomSheetViewModel.getArtist().getName());
nameArtist.setSelected(true);
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);

View File

@@ -82,11 +82,11 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
CustomGlideRequest.Builder.from(requireContext(), songs.get(new Random().nextInt(songs.size())).getCoverArtId(), CustomGlideRequest.ResourceType.Unknown).build().into(coverAlbum);
TextView groupTitleView = view.findViewById(R.id.group_title_text_view);
groupTitleView.setText(MusicUtil.getReadableString(this.groupTitle));
groupTitleView.setText(this.groupTitle);
groupTitleView.setSelected(true);
TextView groupSubtitleView = view.findViewById(R.id.group_subtitle_text_view);
groupSubtitleView.setText(MusicUtil.getReadableString(this.groupSubtitle));
groupSubtitleView.setText(this.groupSubtitle);
groupSubtitleView.setSelected(true);
TextView playRandom = view.findViewById(R.id.play_random_text_view);

View File

@@ -68,7 +68,7 @@ public class PodcastChannelBottomSheetDialog extends BottomSheetDialogFragment i
.into(coverPodcast);
TextView titlePodcast = view.findViewById(R.id.podcast_title_text_view);
titlePodcast.setText(MusicUtil.getReadableString(podcastChannelBottomSheetViewModel.getPodcastChannel().getTitle()));
titlePodcast.setText(podcastChannelBottomSheetViewModel.getPodcastChannel().getTitle());
TextView delete = view.findViewById(R.id.delete_text_view);
delete.setOnClickListener(v -> {

View File

@@ -70,7 +70,7 @@ public class PodcastEpisodeBottomSheetDialog extends BottomSheetDialogFragment i
.into(coverPodcast);
TextView titlePodcast = view.findViewById(R.id.podcast_title_text_view);
titlePodcast.setText(MusicUtil.getReadableString(podcastEpisodeBottomSheetViewModel.getPodcastEpisode().getTitle()));
titlePodcast.setText(podcastEpisodeBottomSheetViewModel.getPodcastEpisode().getTitle());
titlePodcast.setSelected(true);

View File

@@ -84,12 +84,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
.into(coverSong);
TextView titleSong = view.findViewById(R.id.song_title_text_view);
titleSong.setText(MusicUtil.getReadableString(songBottomSheetViewModel.getSong().getTitle()));
titleSong.setText(songBottomSheetViewModel.getSong().getTitle());
titleSong.setSelected(true);
TextView artistSong = view.findViewById(R.id.song_artist_text_view);
artistSong.setText(MusicUtil.getReadableString(songBottomSheetViewModel.getSong().getArtist()));
artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);

View File

@@ -31,11 +31,17 @@ object Constants {
const val ALBUM_ORDER_BY_YEAR = "ALBUM_ORDER_BY_YEAR"
const val ALBUM_ORDER_BY_RANDOM = "ALBUM_ORDER_BY_RANDOM"
const val ALBUM_ORDER_BY_RECENTLY_ADDED = "ALBUM_ORDER_BY_RECENTLY_ADDED"
const val ALBUM_ORDER_BY_RECENTLY_PLAYED = "ALBUM_ORDER_BY_RECENTLY_PLAYED"
const val ALBUM_ORDER_BY_MOST_PLAYED = "ALBUM_ORDER_BY_MOST_PLAYED"
const val ALBUM_ORDER_BY_MOST_RECENTLY_STARRED = "ALBUM_ORDER_BY_MOST_RECENTLY_STARRED"
const val ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED = "ALBUM_ORDER_BY_LEAST_RECENTLY_STARRED"
const val ARTIST_DOWNLOADED = "ARTIST_DOWNLOADED"
const val ARTIST_STARRED = "ARTIST_STARRED"
const val ARTIST_ORDER_BY_NAME = "ARTIST_ORDER_BY_NAME"
const val ARTIST_ORDER_BY_RANDOM = "ARTIST_ORDER_BY_RANDOM"
const val ARTIST_ORDER_BY_MOST_RECENTLY_STARRED = "ARTIST_ORDER_BY_MOST_RECENTLY_STARRED"
const val ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED = "ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED"
const val GENRE_ORDER_BY_NAME = "GENRE_ORDER_BY_NAME"
const val GENRE_ORDER_BY_RANDOM = "GENRE_ORDER_BY_RANDOM"
@@ -74,6 +80,9 @@ object Constants {
const val MEDIA_MIX = "MEDIA_MIX"
const val MEDIA_CHRONOLOGY = "MEDIA_CHRONOLOGY"
const val MEDIA_BEST_OF = "MEDIA_BEST_OF"
const val MEDIA_BY_TITLE = "MEDIA_BY_TITLE"
const val MEDIA_MOST_RECENTLY_STARRED = "MEDIA_MOST_RECENTLY_STARRED"
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
const val DOWNLOAD_URI = "rest/download"
@@ -105,5 +114,6 @@ object Constants {
const val HOME_SECTOR_MOST_PLAYED = "HOME_SECTOR_MOST_PLAYED"
const val HOME_SECTOR_LAST_PLAYED = "HOME_SECTOR_LAST_PLAYED"
const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED"
const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS"
const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED"
}

View File

@@ -8,10 +8,13 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.StandaloneDatabaseProvider;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.ResolvingDataSource;
import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor;
import androidx.media3.datasource.cache.NoOpCacheEvictor;
import androidx.media3.datasource.cache.SimpleCache;
import androidx.media3.exoplayer.DefaultRenderersFactory;
@@ -35,20 +38,22 @@ public final class DownloadUtil {
public static final String DOWNLOAD_NOTIFICATION_SUCCESSFUL_GROUP = "com.cappielloantonio.tempo.SuccessfulDownload";
public static final String DOWNLOAD_NOTIFICATION_FAILED_GROUP = "com.cappielloantonio.tempo.FailedDownload";
private static final String STREAMING_CACHE_CONTENT_DIRECTORY = "streaming_cache";
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static DataSource.Factory dataSourceFactory;
private static DataSource.Factory httpDataSourceFactory;
private static DatabaseProvider databaseProvider;
private static File streamingCacheDirectory;
private static File downloadDirectory;
private static Cache downloadCache;
private static SimpleCache streamingCache;
private static DownloadManager downloadManager;
private static DownloaderManager downloaderManager;
private static DownloadNotificationHelper downloadNotificationHelper;
public static boolean useExtensionRenderers() {
// return true;
return false;
return true;
}
public static RenderersFactory buildRenderersFactory(Context context, boolean preferExtensionRenderer) {
@@ -65,7 +70,9 @@ public final class DownloadUtil {
CookieManager cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
httpDataSourceFactory = new DefaultHttpDataSource.Factory();
httpDataSourceFactory = new DefaultHttpDataSource
.Factory()
.setAllowCrossProtocolRedirects(true);
}
return httpDataSourceFactory;
@@ -74,8 +81,27 @@ public final class DownloadUtil {
public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
if (dataSourceFactory == null) {
context = context.getApplicationContext();
DefaultDataSource.Factory upstreamFactory = new DefaultDataSource.Factory(context, getHttpDataSourceFactory());
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
if (Preferences.getStreamingCacheSize() > 0) {
CacheDataSource.Factory streamCacheFactory = new CacheDataSource.Factory()
.setCache(getStreamingCache(context))
.setUpstreamDataSourceFactory(upstreamFactory);
ResolvingDataSource.Factory resolvingFactory = new ResolvingDataSource.Factory(
new StreamingCacheDataSource.Factory(streamCacheFactory),
dataSpec -> {
DataSpec.Builder builder = dataSpec.buildUpon();
builder.setFlags(dataSpec.flags & ~DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN);
return builder.build();
}
);
dataSourceFactory = buildReadOnlyCacheDataSource(resolvingFactory, getDownloadCache(context));
} else {
dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
}
}
return dataSourceFactory;
@@ -108,6 +134,20 @@ public final class DownloadUtil {
return downloadCache;
}
private static synchronized SimpleCache getStreamingCache(Context context) {
if (streamingCache == null) {
File streamingCacheDirectory = new File(getStreamingCacheDirectory(context), STREAMING_CACHE_CONTENT_DIRECTORY);
streamingCache = new SimpleCache(
streamingCacheDirectory,
new LeastRecentlyUsedCacheEvictor(Preferences.getStreamingCacheSize() * 1024 * 1024),
getDatabaseProvider(context)
);
}
return streamingCache;
}
private static synchronized void ensureDownloadManagerInitialized(Context context) {
if (downloadManager == null) {
downloadManager = new DownloadManager(
@@ -130,6 +170,27 @@ public final class DownloadUtil {
return databaseProvider;
}
private static synchronized File getStreamingCacheDirectory(Context context) {
if (streamingCacheDirectory == null) {
if (Preferences.getStreamingCacheStoragePreference() == 0) {
streamingCacheDirectory = context.getExternalFilesDirs(null)[0];
if (streamingCacheDirectory == null) {
streamingCacheDirectory = context.getFilesDir();
}
} else {
try {
streamingCacheDirectory = context.getExternalFilesDirs(null)[1];
} catch (Exception exception) {
streamingCacheDirectory = context.getExternalFilesDirs(null)[0];
Preferences.setStreamingCacheStoragePreference(0);
}
}
}
return streamingCacheDirectory;
}
private static synchronized File getDownloadDirectory(Context context) {
if (downloadDirectory == null) {
if (Preferences.getDownloadStoragePreference() == 0) {
@@ -187,6 +248,10 @@ public final class DownloadUtil {
return files;
}
public static synchronized long getStreamingCacheSize(Context context) {
return getStreamingCache(context).getCacheSpace();
}
public static Notification buildGroupSummaryNotification(Context context, String channelId, String groupId, int icon, String title) {
return new NotificationCompat.Builder(context, channelId)
.setContentTitle(title)

View File

@@ -74,12 +74,12 @@ public class MappingUtil {
.setMediaId(media.getId())
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(media.getTitle()))
.setTitle(media.getTitle())
.setTrackNumber(media.getTrack() != null ? media.getTrack() : 0)
.setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0)
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.setAlbumTitle(media.getAlbum())
.setArtist(media.getArtist())
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)
@@ -112,12 +112,12 @@ public class MappingUtil {
.setMediaId(media.getId())
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(media.getTitle()))
.setTitle(media.getTitle())
.setTrackNumber(media.getTrack() != null ? media.getTrack() : 0)
.setDiscNumber(media.getDiscNumber() != null ? media.getDiscNumber() : 0)
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(media.getAlbum()))
.setArtist(MusicUtil.getReadableString(media.getArtist()))
.setAlbumTitle(media.getAlbum())
.setArtist(media.getArtist())
.setIsBrowsable(false)
.setIsPlayable(true)
.build()
@@ -159,7 +159,7 @@ public class MappingUtil {
.setExtras(bundle)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
// .setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build();
}
@@ -193,10 +193,10 @@ public class MappingUtil {
.setMediaId(podcastEpisode.getId())
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(podcastEpisode.getTitle()))
.setTitle(podcastEpisode.getTitle())
.setReleaseYear(podcastEpisode.getYear() != null ? podcastEpisode.getYear() : 0)
.setAlbumTitle(MusicUtil.getReadableString(podcastEpisode.getAlbum()))
.setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist()))
.setAlbumTitle(podcastEpisode.getAlbum())
.setArtist(podcastEpisode.getArtist())
.setArtworkUri(artworkUri)
.setExtras(bundle)
.setIsBrowsable(false)

View File

@@ -157,7 +157,7 @@ public class MusicUtil {
}
public static String getReadableAudioQualityString(Child child) {
if (!Preferences.showAudioQuality()) return "";
if (!Preferences.showAudioQuality() || child.getBitrate() == null) return "";
return "" +
" " +
@@ -179,14 +179,6 @@ public class MusicUtil {
}
}
public static String getReadableString(String string) {
if (string != null) {
return Html.fromHtml(string, Html.FROM_HTML_MODE_COMPACT).toString();
}
return "";
}
public static String getReadableTrackNumber(Context context, Integer trackNumber) {
if (trackNumber != null) {
return String.valueOf(trackNumber);
@@ -195,6 +187,14 @@ public class MusicUtil {
return context.getString(R.string.label_placeholder);
}
public static String getReadableString(String string) {
if (string != null) {
return Html.fromHtml(string, Html.FROM_HTML_MODE_COMPACT).toString();
}
return "";
}
public static String forceReadableString(String string) {
if (string != null) {
return getReadableString(string)
@@ -219,20 +219,6 @@ public class MusicUtil {
return "";
}
public static List<String> getReadableStrings(List<String> strings) {
List<String> readableStrings = new ArrayList<>();
if (!strings.isEmpty()) {
for (String string : strings) {
if (string != null) {
readableStrings.add(Html.fromHtml(string, Html.FROM_HTML_MODE_COMPACT).toString());
}
}
}
return readableStrings;
}
public static String getReadableByteCount(long bytes) {
long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);

View File

@@ -2,6 +2,8 @@ package com.cappielloantonio.tempo.util;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import com.cappielloantonio.tempo.App;
@@ -11,9 +13,15 @@ public class NetworkUtil {
ConnectivityManager connectivityManager = (ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
Network network = connectivityManager.getActiveNetwork();
return networkInfo == null || !networkInfo.isConnected();
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 true;

View File

@@ -1,13 +1,12 @@
package com.cappielloantonio.tempo.util
import android.util.Log
import com.cappielloantonio.tempo.App
import com.cappielloantonio.tempo.model.HomeSector
import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension
import com.google.gson.Gson
object Preferences {
const val THEME = "theme"
private const val SERVER = "server"
@@ -20,9 +19,13 @@ object Preferences {
private const val SERVER_ID = "server_id"
private const val OPEN_SUBSONIC = "open_subsonic"
private const val OPEN_SUBSONIC_EXTENSIONS = "open_subsonic_extensions"
private const val LOCAL_ADDRESS = "local_address"
private const val IN_USE_SERVER_ADDRESS = "in_use_server_address"
private const val NEXT_SERVER_SWITCH = "next_server_switch"
private const val PLAYBACK_SPEED = "playback_speed"
private const val SKIP_SILENCE = "skip_silence"
private const val IMAGE_CACHE_SIZE = "image_cache_size"
private const val STREAMING_CACHE_SIZE = "streaming_cache_size"
private const val IMAGE_SIZE = "image_size"
private const val MAX_BITRATE_WIFI = "max_bitrate_wifi"
private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile"
@@ -41,6 +44,7 @@ object Preferences {
private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility"
private const val REPLAY_GAIN_MODE = "replay_gain_mode"
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
private const val DOWNLOAD_STORAGE = "download_storage"
private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
@@ -57,6 +61,9 @@ object Preferences {
private const val AUDIO_QUALITY_PER_ITEM = "audio_quality_per_item"
private const val HOME_SECTOR_LIST = "home_sector_list"
private const val RATING_PER_ITEM = "rating_per_item"
private const val NEXT_UPDATE_CHECK = "next_update_check"
private const val CONTINUOUS_PLAY = "continuous_play"
private const val LAST_INSTANT_MIX = "last_instant_mix"
@JvmStatic
@@ -149,6 +156,46 @@ object Preferences {
App.getInstance().preferences.edit().putString(OPEN_SUBSONIC_EXTENSIONS, Gson().toJson(extension)).apply()
}
@JvmStatic
fun getLocalAddress(): String? {
return App.getInstance().preferences.getString(LOCAL_ADDRESS, null)
}
@JvmStatic
fun setLocalAddress(address: String?) {
App.getInstance().preferences.edit().putString(LOCAL_ADDRESS, address).apply()
}
@JvmStatic
fun getInUseServerAddress(): String? {
return App.getInstance().preferences.getString(IN_USE_SERVER_ADDRESS, null)
?.takeIf { it.isNotBlank() }
?: getServer()
}
@JvmStatic
fun isInUseServerAddressLocal(): Boolean {
return getInUseServerAddress() == getLocalAddress()
}
@JvmStatic
fun switchInUseServerAddress() {
val inUseAddress = if (getInUseServerAddress() == getServer()) getLocalAddress() else getServer()
App.getInstance().preferences.edit().putString(IN_USE_SERVER_ADDRESS, inUseAddress).apply()
}
@JvmStatic
fun isServerSwitchable(): Boolean {
return App.getInstance().preferences.getLong(
NEXT_SERVER_SWITCH, 0
) + 15000 < System.currentTimeMillis() && !getServer().isNullOrEmpty() && !getLocalAddress().isNullOrEmpty()
}
@JvmStatic
fun setServerSwitchableTimer() {
App.getInstance().preferences.edit().putLong(NEXT_SERVER_SWITCH, System.currentTimeMillis()).apply()
}
@JvmStatic
fun askForOptimization(): Boolean {
return App.getInstance().preferences.getBoolean(BATTERY_OPTIMIZATION, true)
@@ -189,6 +236,11 @@ object Preferences {
return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt()
}
@JvmStatic
fun getStreamingCacheSize(): Long {
return App.getInstance().preferences.getString(STREAMING_CACHE_SIZE, "256")!!.toLong()
}
@JvmStatic
fun getMaxBitrateWifi(): String {
return App.getInstance().preferences.getString(MAX_BITRATE_WIFI, "0")!!
@@ -222,7 +274,7 @@ object Preferences {
@JvmStatic
fun setDataSavingMode(isDataSavingModeEnabled: Boolean) {
App.getInstance().preferences.edit().putBoolean(DATA_SAVING_MODE, isDataSavingModeEnabled)
.apply()
.apply()
}
@JvmStatic
@@ -233,20 +285,20 @@ object Preferences {
@JvmStatic
fun setStarredSyncEnabled(isStarredSyncEnabled: Boolean) {
App.getInstance().preferences.edit().putBoolean(
SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, isStarredSyncEnabled
SYNC_STARRED_TRACKS_FOR_OFFLINE_USE, isStarredSyncEnabled
).apply()
}
@JvmStatic
fun showServerUnreachableDialog(): Boolean {
return App.getInstance().preferences.getLong(
SERVER_UNREACHABLE, 0
) + 360000 < System.currentTimeMillis()
SERVER_UNREACHABLE, 0
) + 86400000 < System.currentTimeMillis()
}
@JvmStatic
fun setServerUnreachableDatetime(datetime: Long) {
App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, datetime).apply()
fun setServerUnreachableDatetime() {
App.getInstance().preferences.edit().putLong(SERVER_UNREACHABLE, System.currentTimeMillis()).apply()
}
@JvmStatic
@@ -304,6 +356,19 @@ object Preferences {
return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_PRIORITY, false)
}
@JvmStatic
fun getStreamingCacheStoragePreference(): Int {
return App.getInstance().preferences.getString(STREAMING_CACHE_STORAGE, "0")!!.toInt()
}
@JvmStatic
fun setStreamingCacheStoragePreference(streamingCachePreference: Int) {
return App.getInstance().preferences.edit().putString(
STREAMING_CACHE_STORAGE,
streamingCachePreference.toString()
).apply()
}
@JvmStatic
fun getDownloadStoragePreference(): Int {
return App.getInstance().preferences.getString(DOWNLOAD_STORAGE, "0")!!.toInt()
@@ -312,24 +377,24 @@ object Preferences {
@JvmStatic
fun setDownloadStoragePreference(storagePreference: Int) {
return App.getInstance().preferences.edit().putString(
DOWNLOAD_STORAGE,
storagePreference.toString()
DOWNLOAD_STORAGE,
storagePreference.toString()
).apply()
}
@JvmStatic
fun getDefaultDownloadViewType(): String {
return App.getInstance().preferences.getString(
DEFAULT_DOWNLOAD_VIEW_TYPE,
Constants.DOWNLOAD_TYPE_TRACK
DEFAULT_DOWNLOAD_VIEW_TYPE,
Constants.DOWNLOAD_TYPE_TRACK
)!!
}
@JvmStatic
fun setDefaultDownloadViewType(viewType: String) {
return App.getInstance().preferences.edit().putString(
DEFAULT_DOWNLOAD_VIEW_TYPE,
viewType
DEFAULT_DOWNLOAD_VIEW_TYPE,
viewType
).apply()
}
@@ -402,4 +467,33 @@ object Preferences {
fun showItemRating(): Boolean {
return App.getInstance().preferences.getBoolean(RATING_PER_ITEM, false)
}
@JvmStatic
fun showTempoUpdateDialog(): Boolean {
return App.getInstance().preferences.getLong(
NEXT_UPDATE_CHECK, 0
) + 86400000 < System.currentTimeMillis()
}
@JvmStatic
fun setTempoUpdateReminder() {
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
}
@JvmStatic
fun isContinuousPlayEnabled(): Boolean {
return App.getInstance().preferences.getBoolean(CONTINUOUS_PLAY, true)
}
@JvmStatic
fun setLastInstantMix() {
App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()).apply()
}
@JvmStatic
fun isInstantMixUsable(): Boolean {
return App.getInstance().preferences.getLong(
LAST_INSTANT_MIX, 0
) + 5000 < System.currentTimeMillis()
}
}

View File

@@ -0,0 +1,62 @@
package com.cappielloantonio.tempo.util
import android.net.Uri
import android.util.Log
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.TransferListener
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.ContentMetadata
@UnstableApi
class StreamingCacheDataSource private constructor(
private val cacheDataSource: CacheDataSource,
): DataSource {
private val TAG = "StreamingCacheDataSource"
private var currentDataSpec: DataSpec? = null
class Factory(private val cacheDatasourceFactory: CacheDataSource.Factory): DataSource.Factory {
override fun createDataSource(): DataSource {
return StreamingCacheDataSource(cacheDatasourceFactory.createDataSource())
}
}
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
return cacheDataSource.read(buffer, offset, length)
}
override fun addTransferListener(transferListener: TransferListener) {
return cacheDataSource.addTransferListener(transferListener)
}
override fun open(dataSpec: DataSpec): Long {
val ret = cacheDataSource.open(dataSpec)
currentDataSpec = dataSpec
return ret
}
override fun getUri(): Uri? {
return cacheDataSource.uri
}
override fun close() {
cacheDataSource.close()
val dataSpec = currentDataSpec
if (dataSpec != null) {
val cacheKey = cacheDataSource.cacheKeyFactory.buildCacheKey(dataSpec)
val contentLength = ContentMetadata.getContentLength(cacheDataSource.cache.getContentMetadata(cacheKey));
if (contentLength == C.LENGTH_UNSET.toLong()) {
Log.d(TAG, "Removing partial cache for $cacheKey")
cacheDataSource.cache.removeResource(cacheKey)
} else {
Log.d(TAG, "Key $cacheKey has been fully cached")
}
}
}
}

View File

@@ -28,6 +28,8 @@ public class AlbumListPageViewModel extends AndroidViewModel {
private MutableLiveData<List<AlbumID3>> albumList;
public int maxNumber = 500;
public AlbumListPageViewModel(@NonNull Application application) {
super(application);
@@ -40,20 +42,20 @@ public class AlbumListPageViewModel extends AndroidViewModel {
switch (title) {
case Constants.ALBUM_RECENTLY_PLAYED:
albumRepository.getAlbums("recent", 500, null, null).observe(owner, albums -> albumList.setValue(albums));
albumRepository.getAlbums("recent", maxNumber, null, null).observe(owner, albums -> albumList.setValue(albums));
break;
case Constants.ALBUM_MOST_PLAYED:
albumRepository.getAlbums("frequent", 500, null, null).observe(owner, albums -> albumList.setValue(albums));
albumRepository.getAlbums("frequent", maxNumber, null, null).observe(owner, albums -> albumList.setValue(albums));
break;
case Constants.ALBUM_RECENTLY_ADDED:
albumRepository.getAlbums("newest", 500, null, null).observe(owner, albums -> albumList.setValue(albums));
albumRepository.getAlbums("newest", maxNumber, null, null).observe(owner, albums -> albumList.setValue(albums));
break;
case Constants.ALBUM_STARRED:
albumList = albumRepository.getStarredAlbums(false, -1);
break;
case Constants.ALBUM_NEW_RELEASES:
int currentYear = Calendar.getInstance().get(Calendar.YEAR);
albumRepository.getAlbums("byYear", 500, currentYear, currentYear).observe(owner, albums -> {
albumRepository.getAlbums("byYear", maxNumber, currentYear, currentYear).observe(owner, albums -> {
albums.sort(Comparator.comparing(AlbumID3::getCreated).reversed());
albumList.postValue(albums.subList(0, Math.min(20, albums.size())));
});

View File

@@ -4,7 +4,9 @@ import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
@@ -18,8 +20,8 @@ import java.util.List;
public class AlbumPageViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository;
private AlbumID3 album;
private String albumId;
private final MutableLiveData<AlbumID3> album = new MutableLiveData<>(null);
public AlbumPageViewModel(@NonNull Application application) {
super(application);
@@ -29,22 +31,27 @@ public class AlbumPageViewModel extends AndroidViewModel {
}
public LiveData<List<Child>> getAlbumSongLiveList() {
return albumRepository.getAlbumTracks(album.getId());
return albumRepository.getAlbumTracks(albumId);
}
public AlbumID3 getAlbum() {
public MutableLiveData<AlbumID3> getAlbum() {
return album;
}
public void setAlbum(AlbumID3 album) {
this.album = album;
public void setAlbum(LifecycleOwner owner, AlbumID3 album) {
this.albumId = album.getId();
this.album.postValue(album);
albumRepository.getAlbum(album.getId()).observe(owner, albums -> {
if (albums != null) this.album.setValue(albums);
});
}
public LiveData<ArtistID3> getArtist() {
return artistRepository.getArtistInfo(album.getArtistId());
return artistRepository.getArtistInfo(albumId);
}
public LiveData<AlbumInfo> getAlbumInfo() {
return albumRepository.getAlbumInfo(album.getId());
return albumRepository.getAlbumInfo(albumId);
}
}

View File

@@ -70,7 +70,8 @@ public class HomeRearrangementViewModel extends AndroidViewModel {
sectors.add(new HomeSector(Constants.HOME_SECTOR_MOST_PLAYED, getApplication().getString(R.string.home_title_most_played), true, 11));
sectors.add(new HomeSector(Constants.HOME_SECTOR_LAST_PLAYED, getApplication().getString(R.string.home_title_last_played), true, 12));
sectors.add(new HomeSector(Constants.HOME_SECTOR_RECENTLY_ADDED, getApplication().getString(R.string.home_title_recently_added), true, 13));
sectors.add(new HomeSector(Constants.HOME_SECTOR_SHARED, getApplication().getString(R.string.home_title_shares), true, 14));
sectors.add(new HomeSector(Constants.HOME_SECTOR_PINNED_PLAYLISTS, getApplication().getString(R.string.home_title_pinned_playlists), true, 14));
sectors.add(new HomeSector(Constants.HOME_SECTOR_SHARED, getApplication().getString(R.string.home_title_shares), true, 15));
return sectors;
}

View File

@@ -16,11 +16,13 @@ import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.ChronologyRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.PlaylistRepository;
import com.cappielloantonio.tempo.repository.SharingRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.Playlist;
import com.cappielloantonio.tempo.subsonic.models.Share;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.reflect.TypeToken;
@@ -32,6 +34,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
public class HomeViewModel extends AndroidViewModel {
private static final String TAG = "HomeViewModel";
@@ -41,6 +44,7 @@ public class HomeViewModel extends AndroidViewModel {
private final ArtistRepository artistRepository;
private final ChronologyRepository chronologyRepository;
private final FavoriteRepository favoriteRepository;
private final PlaylistRepository playlistRepository;
private final SharingRepository sharingRepository;
private final MutableLiveData<List<Child>> dicoverSongSample = new MutableLiveData<>(null);
@@ -60,6 +64,7 @@ public class HomeViewModel extends AndroidViewModel {
private final MutableLiveData<List<Child>> mediaInstantMix = new MutableLiveData<>(null);
private final MutableLiveData<List<Child>> artistInstantMix = new MutableLiveData<>(null);
private final MutableLiveData<List<Child>> artistBestOf = new MutableLiveData<>(null);
private final MutableLiveData<List<Playlist>> pinnedPlaylists = new MutableLiveData<>(null);
private final MutableLiveData<List<Share>> shares = new MutableLiveData<>(null);
private List<HomeSector> sectors;
@@ -74,6 +79,7 @@ public class HomeViewModel extends AndroidViewModel {
artistRepository = new ArtistRepository();
chronologyRepository = new ChronologyRepository();
favoriteRepository = new FavoriteRepository();
playlistRepository = new PlaylistRepository();
sharingRepository = new SharingRepository();
setOfflineFavorite();
@@ -91,9 +97,17 @@ public class HomeViewModel extends AndroidViewModel {
return songRepository.getRandomSample(100, null, null);
}
public LiveData<List<Chronology>> getGridSongSample(LifecycleOwner owner) {
public LiveData<List<Chronology>> getChronologySample(LifecycleOwner owner) {
Calendar cal = Calendar.getInstance();
String server = Preferences.getServerId();
chronologyRepository.getLastWeek(server).observe(owner, thisGridTopSong::postValue);
int currentWeek = cal.get(Calendar.WEEK_OF_YEAR);
long start = cal.getTimeInMillis();
cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 1);
long end = cal.getTimeInMillis();
chronologyRepository.getChronology(server, start, end).observe(owner, thisGridTopSong::postValue);
return thisGridTopSong;
}
@@ -195,7 +209,7 @@ public class HomeViewModel extends AndroidViewModel {
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
mediaInstantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, mediaInstantMix::postValue);
songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
return mediaInstantMix;
}
@@ -216,6 +230,24 @@ public class HomeViewModel extends AndroidViewModel {
return artistBestOf;
}
public LiveData<List<Playlist>> getPinnedPlaylists(LifecycleOwner owner) {
pinnedPlaylists.setValue(Collections.emptyList());
playlistRepository.getPlaylists(false, -1).observe(owner, remotes -> {
playlistRepository.getPinnedPlaylists().observe(owner, locals -> {
if (remotes != null && locals != null) {
List<Playlist> toReturn = remotes.stream()
.filter(remote -> locals.stream().anyMatch(local -> local.getId().equals(remote.getId())))
.collect(Collectors.toList());
pinnedPlaylists.setValue(toReturn);
}
});
});
return pinnedPlaylists;
}
public LiveData<List<Share>> getShares(LifecycleOwner owner) {
if (shares.getValue() == null) {
sharingRepository.getShares().observe(owner, shares::postValue);
@@ -228,6 +260,31 @@ public class HomeViewModel extends AndroidViewModel {
return songRepository.getStarredSongs(false, -1);
}
public void changeChronologyPeriod(LifecycleOwner owner, int period) {
Calendar cal = Calendar.getInstance();
String server = Preferences.getServerId();
int currentWeek = cal.get(Calendar.WEEK_OF_YEAR);
long start = 0;
long end = 0;
if (period == 0) {
start = cal.getTimeInMillis();
cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 1);
end = cal.getTimeInMillis();
} else if (period == 1) {
start = cal.getTimeInMillis();
cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 4);
end = cal.getTimeInMillis();
} else if (period == 2) {
start = cal.getTimeInMillis();
cal.set(Calendar.WEEK_OF_YEAR, currentWeek - 52);
end = cal.getTimeInMillis();
}
chronologyRepository.getChronology(server, start, end).observe(owner, thisGridTopSong::postValue);
}
public void refreshDiscoverySongSample(LifecycleOwner owner) {
songRepository.getRandomSample(10, null, null).observe(owner, dicoverSongSample::postValue);
}

View File

@@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import com.cappielloantonio.tempo.github.models.LatestRelease;
import com.cappielloantonio.tempo.repository.QueueRepository;
import com.cappielloantonio.tempo.repository.SystemRepository;
import com.cappielloantonio.tempo.subsonic.models.OpenSubsonicExtension;
@@ -36,4 +37,8 @@ public class MainViewModel extends AndroidViewModel {
public LiveData<List<OpenSubsonicExtension>> getOpenSubsonicExtensions() {
return systemRepository.getOpenSubsonicExtensions();
}
public LiveData<LatestRelease> checkTempoUpdate() {
return systemRepository.checkTempoUpdate();
}
}

View File

@@ -14,11 +14,13 @@ import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.model.Queue;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.FavoriteRepository;
import com.cappielloantonio.tempo.repository.OpenRepository;
import com.cappielloantonio.tempo.repository.QueueRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
@@ -40,6 +42,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
private static final String TAG = "PlayerBottomSheetViewModel";
private final SongRepository songRepository;
private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository;
private final QueueRepository queueRepository;
private final FavoriteRepository favoriteRepository;
@@ -48,6 +51,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
private final MutableLiveData<LyricsList> lyricsListLiveData = new MutableLiveData<>(null);
private final MutableLiveData<String> descriptionLiveData = new MutableLiveData<>(null);
private final MutableLiveData<Child> liveMedia = new MutableLiveData<>(null);
private final MutableLiveData<AlbumID3> liveAlbum = new MutableLiveData<>(null);
private final MutableLiveData<ArtistID3> liveArtist = new MutableLiveData<>(null);
private final MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(null);
private boolean lyricsSyncState = true;
@@ -57,6 +61,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
super(application);
songRepository = new SongRepository();
albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository();
queueRepository = new QueueRepository();
favoriteRepository = new FavoriteRepository();
@@ -162,6 +167,23 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
}
}
public LiveData<AlbumID3> getLiveAlbum() {
return liveAlbum;
}
public void setLiveAlbum(LifecycleOwner owner, String mediaType, String AlbumId) {
if (mediaType != null) {
switch (mediaType) {
case Constants.MEDIA_TYPE_MUSIC:
albumRepository.getAlbum(AlbumId).observe(owner, liveAlbum::postValue);
break;
case Constants.MEDIA_TYPE_PODCAST:
liveAlbum.postValue(null);
break;
}
}
}
public LiveData<ArtistID3> getLiveArtist() {
return liveArtist;
}
@@ -190,7 +212,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
instantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, instantMix::postValue);
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
return instantMix;
}

View File

@@ -4,7 +4,9 @@ import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.repository.PlaylistRepository;
import com.cappielloantonio.tempo.subsonic.models.Child;
@@ -35,4 +37,22 @@ public class PlaylistPageViewModel extends AndroidViewModel {
public void setPlaylist(Playlist playlist) {
this.playlist = playlist;
}
public LiveData<Boolean> isPinned(LifecycleOwner owner) {
MutableLiveData<Boolean> isPinnedLive = new MutableLiveData<>();
playlistRepository.getPinnedPlaylists().observe(owner, playlists -> {
isPinnedLive.postValue(playlists.stream().anyMatch(obj -> obj.getId().equals(playlist.getId())));
});
return isPinnedLive;
}
public void setPinned(boolean isNowPinned) {
if (isNowPinned) {
playlistRepository.insert(playlist);
} else {
playlistRepository.delete(playlist);
}
}
}

View File

@@ -128,7 +128,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
instantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, instantMix::postValue);
songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
return instantMix;
}

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