mirror of
https://github.com/antebudimir/tempus.git
synced 2026-04-16 00:37:25 +00:00
Merge branch 'development' into skip-duplicates
This commit is contained in:
commit
717f95a04a
87 changed files with 6098 additions and 428 deletions
|
|
@ -1,11 +1,14 @@
|
|||
package com.cappielloantonio.tempo.ui.activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
|
|
@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
|
|||
import androidx.core.splashscreen.SplashScreen;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
|
@ -56,6 +62,7 @@ public class MainActivity extends BaseActivity {
|
|||
private BottomSheetBehavior bottomSheetBehavior;
|
||||
|
||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||
private Intent pendingDownloadPlaybackIntent;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
|
@ -77,12 +84,16 @@ public class MainActivity extends BaseActivity {
|
|||
checkConnectionType();
|
||||
getOpenSubsonicExtensions();
|
||||
checkTempoUpdate();
|
||||
|
||||
maybeSchedulePlaybackIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
pingServer();
|
||||
initService();
|
||||
consumePendingPlaybackIntent();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -98,6 +109,14 @@ public class MainActivity extends BaseActivity {
|
|||
bind = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
maybeSchedulePlaybackIntent(intent);
|
||||
consumePendingPlaybackIntent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
||||
|
|
@ -351,6 +370,7 @@ public class MainActivity extends BaseActivity {
|
|||
Preferences.switchInUseServerAddress();
|
||||
App.refreshSubsonicClient();
|
||||
pingServer();
|
||||
resetView();
|
||||
} else {
|
||||
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
|
||||
}
|
||||
|
|
@ -361,6 +381,7 @@ public class MainActivity extends BaseActivity {
|
|||
Preferences.switchInUseServerAddress();
|
||||
App.refreshSubsonicClient();
|
||||
pingServer();
|
||||
resetView();
|
||||
} else {
|
||||
mainViewModel.ping().observe(this, subsonicResponse -> {
|
||||
if (subsonicResponse == null) {
|
||||
|
|
@ -376,6 +397,13 @@ public class MainActivity extends BaseActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private void resetView() {
|
||||
resetViewModel();
|
||||
int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
|
||||
navController.popBackStack(id, true);
|
||||
navController.navigate(id);
|
||||
}
|
||||
|
||||
private void getOpenSubsonicExtensions() {
|
||||
if (Preferences.getToken() != null) {
|
||||
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
|
||||
|
|
@ -408,4 +436,68 @@ public class MainActivity extends BaseActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSchedulePlaybackIntent(Intent intent) {
|
||||
if (intent == null) return;
|
||||
if (Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD.equals(intent.getAction())
|
||||
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
|
||||
pendingDownloadPlaybackIntent = new Intent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private void consumePendingPlaybackIntent() {
|
||||
if (pendingDownloadPlaybackIntent == null) return;
|
||||
Intent intent = pendingDownloadPlaybackIntent;
|
||||
pendingDownloadPlaybackIntent = null;
|
||||
playDownloadedMedia(intent);
|
||||
}
|
||||
|
||||
private void playDownloadedMedia(Intent intent) {
|
||||
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
|
||||
if (TextUtils.isEmpty(uriString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
String mediaId = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID);
|
||||
if (TextUtils.isEmpty(mediaId)) {
|
||||
mediaId = uri.toString();
|
||||
}
|
||||
|
||||
String title = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_TITLE);
|
||||
String artist = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ARTIST);
|
||||
String album = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ALBUM);
|
||||
int duration = intent.getIntExtra(Constants.EXTRA_DOWNLOAD_DURATION, 0);
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString("id", mediaId);
|
||||
extras.putString("title", title);
|
||||
extras.putString("artist", artist);
|
||||
extras.putString("album", album);
|
||||
extras.putString("uri", uri.toString());
|
||||
extras.putString("type", Constants.MEDIA_TYPE_MUSIC);
|
||||
extras.putInt("duration", duration);
|
||||
|
||||
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder()
|
||||
.setExtras(extras)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true);
|
||||
|
||||
if (!TextUtils.isEmpty(title)) metadataBuilder.setTitle(title);
|
||||
if (!TextUtils.isEmpty(artist)) metadataBuilder.setArtist(artist);
|
||||
if (!TextUtils.isEmpty(album)) metadataBuilder.setAlbumTitle(album);
|
||||
|
||||
MediaItem mediaItem = new MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadataBuilder.build())
|
||||
.setUri(uri)
|
||||
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
|
||||
.setRequestMetadata(new MediaItem.RequestMetadata.Builder()
|
||||
.setMediaUri(uri)
|
||||
.setExtras(extras)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
|
|||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -23,17 +24,24 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
|
||||
private static final String TAG = "PlayerSongQueueAdapter";
|
||||
private final ClickCallback click;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
private List<Child> songs;
|
||||
|
||||
private String currentPlayingId;
|
||||
private boolean isPlaying;
|
||||
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||
|
||||
public PlayerSongQueueAdapter(ClickCallback click) {
|
||||
this.click = click;
|
||||
this.songs = Collections.emptyList();
|
||||
|
|
@ -104,6 +112,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
|||
} else {
|
||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
mediaBrowserListenableFuture.addListener(() -> {
|
||||
try {
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
int pos = holder.getBindingAdapterPosition();
|
||||
Child s = songs.get(pos);
|
||||
if (currentPlayingId != null && currentPlayingId.equals(s.getId())) {
|
||||
if (isPlaying) {
|
||||
mediaBrowser.pause();
|
||||
} else {
|
||||
mediaBrowser.play();
|
||||
}
|
||||
} else {
|
||||
mediaBrowser.seekTo(pos, 0);
|
||||
mediaBrowser.play();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "Error obtaining MediaBrowser", e);
|
||||
}
|
||||
}, MoreExecutors.directExecutor());
|
||||
|
||||
});
|
||||
bindPlaybackState(holder, song);
|
||||
}
|
||||
|
||||
private void bindPlaybackState(@NonNull PlayerSongQueueAdapter.ViewHolder holder, @NonNull Child song) {
|
||||
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
|
||||
|
||||
if (isCurrent) {
|
||||
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
|
||||
if (isPlaying) {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
|
||||
} else {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
|
||||
}
|
||||
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
|
||||
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Child> getItems() {
|
||||
|
|
@ -132,6 +180,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
|
|||
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
||||
}
|
||||
|
||||
public void setPlaybackState(String mediaId, boolean playing) {
|
||||
String oldId = this.currentPlayingId;
|
||||
boolean oldPlaying = this.isPlaying;
|
||||
List<Integer> oldPositions = currentPlayingPositions;
|
||||
|
||||
this.currentPlayingId = mediaId;
|
||||
this.isPlaying = playing;
|
||||
|
||||
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
|
||||
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
if (oldPositions.equals(newPositionsCheck)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
|
||||
for (int pos : oldPositions) {
|
||||
if (pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
for (int pos : currentPlayingPositions) {
|
||||
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> findPositionsById(String id) {
|
||||
if (id == null) return Collections.emptyList();
|
||||
List<Integer> positions = new ArrayList<>();
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
if (id.equals(songs.get(i).getId())) {
|
||||
positions.add(i);
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
public Child getItem(int id) {
|
||||
return songs.get(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.cappielloantonio.tempo.ui.adapter;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -9,7 +11,9 @@ import android.widget.Filterable;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
|
|
@ -21,8 +25,11 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
|
@ -30,6 +37,7 @@ import java.util.Comparator;
|
|||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
@UnstableApi
|
||||
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> implements Filterable {
|
||||
|
|
@ -42,6 +50,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
private List<Child> songs;
|
||||
private String currentFilter;
|
||||
|
||||
private String currentPlayingId;
|
||||
private boolean isPlaying;
|
||||
private List<Integer> currentPlayingPositions = Collections.emptyList();
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private final Filter filtering = new Filter() {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence constraint) {
|
||||
|
|
@ -70,10 +83,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
songs = (List<Child>) results.values;
|
||||
notifyDataSetChanged();
|
||||
|
||||
for (int pos : currentPlayingPositions) {
|
||||
if (pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
||||
public SongHorizontalAdapter(LifecycleOwner lifecycleOwner, ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
|
||||
this.click = click;
|
||||
this.showCoverArt = showCoverArt;
|
||||
this.showAlbum = showAlbum;
|
||||
|
|
@ -81,6 +100,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
this.songsFull = Collections.emptyList();
|
||||
this.currentFilter = "";
|
||||
this.album = album;
|
||||
setHasStableIds(false);
|
||||
|
||||
if (lifecycleOwner != null) {
|
||||
MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
@ -91,7 +115,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && payloads.contains("payload_playback")) {
|
||||
bindPlaybackState(holder, songs.get(position));
|
||||
} else {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
Child song = songs.get(position);
|
||||
|
||||
holder.item.searchResultSongTitleTextView.setText(song.getTitle());
|
||||
|
|
@ -109,10 +142,18 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
|
||||
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
|
||||
|
||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||
if (ExternalAudioReader.getUri(song) != null) {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (showCoverArt) CustomGlideRequest.Builder
|
||||
|
|
@ -165,6 +206,39 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
} else {
|
||||
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
bindPlaybackState(holder, song);
|
||||
}
|
||||
|
||||
private void handleExternalAudioRefresh() {
|
||||
if (Preferences.getDownloadDirectoryUri() != null) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) {
|
||||
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
|
||||
|
||||
if (isCurrent) {
|
||||
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
|
||||
if (isPlaying) {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
|
||||
} else {
|
||||
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
|
||||
}
|
||||
if (!showCoverArt) {
|
||||
holder.item.trackNumberTextView.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
|
||||
if (!showCoverArt) {
|
||||
holder.item.trackNumberTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -188,6 +262,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
return position;
|
||||
}
|
||||
|
||||
public void setPlaybackState(String mediaId, boolean playing) {
|
||||
String oldId = this.currentPlayingId;
|
||||
boolean oldPlaying = this.isPlaying;
|
||||
List<Integer> oldPositions = currentPlayingPositions;
|
||||
|
||||
this.currentPlayingId = mediaId;
|
||||
this.isPlaying = playing;
|
||||
|
||||
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
|
||||
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
if (oldPositions.equals(newPositionsCheck)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
|
||||
|
||||
for (int pos : oldPositions) {
|
||||
if (pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
for (int pos : currentPlayingPositions) {
|
||||
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
|
||||
notifyItemChanged(pos, "payload_playback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> findPositionsById(String id) {
|
||||
if (id == null) return Collections.emptyList();
|
||||
List<Integer> positions = new ArrayList<>();
|
||||
for (int i = 0; i < songs.size(); i++) {
|
||||
if (id.equals(songs.get(i).getId())) {
|
||||
positions.add(i);
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filtering;
|
||||
|
|
@ -215,11 +329,29 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
}
|
||||
|
||||
public void onClick() {
|
||||
int pos = getBindingAdapterPosition();
|
||||
Child tappedSong = songs.get(pos);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())));
|
||||
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()));
|
||||
|
||||
click.onMediaClick(bundle);
|
||||
if (tappedSong.getId().equals(currentPlayingId)) {
|
||||
Log.i("SongHorizontalAdapter", "Tapping on currently playing song, toggling playback");
|
||||
try{
|
||||
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||
Log.i("SongHorizontalAdapter", "MediaBrowser retrieved, isPlaying: " + isPlaying);
|
||||
if (isPlaying) {
|
||||
mediaBrowser.pause();
|
||||
} else {
|
||||
mediaBrowser.play();
|
||||
}
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
Log.e("SongHorizontalAdapter", "Error getting MediaBrowser", e);
|
||||
}
|
||||
} else {
|
||||
click.onMediaClick(bundle);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean onLongClick() {
|
||||
|
|
@ -247,4 +379,8 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
|||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setMediaBrowserListenableFuture(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
|
||||
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.cappielloantonio.tempo.ui.dialog;
|
|||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
|
|
@ -12,6 +15,9 @@ import androidx.media3.common.util.UnstableApi;
|
|||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
|
|
@ -42,7 +48,21 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
|||
if (dialog != null) {
|
||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
}
|
||||
|
||||
String uriString = Preferences.getDownloadDirectoryUri();
|
||||
if (uriString != null) {
|
||||
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
|
||||
if (directory != null && directory.canWrite()) {
|
||||
for (DocumentFile file : directory.listFiles()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
ExternalAudioReader.refreshCache();
|
||||
ExternalDownloadMetadataStore.clear();
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
public class DownloadDirectoryPickerDialog extends DialogFragment {
|
||||
|
||||
private ActivityResultLauncher<Intent> folderPickerLauncher;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public android.app.Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Register launcher *before* button triggers
|
||||
folderPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == android.app.Activity.RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
requireContext().getContentResolver().takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
);
|
||||
|
||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||
ExternalAudioReader.refreshCache();
|
||||
|
||||
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Set Download Directory")
|
||||
.setMessage("Choose a folder where downloaded songs will be stored.")
|
||||
.setPositiveButton("Choose Folder", (dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
folderPickerLauncher.launch(intent);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ public class DownloadStorageDialog extends DialogFragment {
|
|||
.setTitle(R.string.download_storage_dialog_title)
|
||||
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
||||
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
||||
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
|
|||
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||
neutralButton.setOnClickListener(v -> {
|
||||
int currentPreference = Preferences.getDownloadStoragePreference();
|
||||
int newPreference = 2;
|
||||
|
||||
if (currentPreference != newPreference) {
|
||||
Preferences.setDownloadStoragePreference(newPreference);
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
dialogClickCallback.onNeutralClick();
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.OptIn;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.DialogStarredArtistSyncBinding;
|
||||
import com.cappielloantonio.tempo.model.Download;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.StarredArtistsSyncViewModel;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@OptIn(markerClass = UnstableApi.class)
|
||||
public class StarredArtistSyncDialog extends DialogFragment {
|
||||
private StarredArtistsSyncViewModel starredArtistsSyncViewModel;
|
||||
|
||||
private Runnable onCancel;
|
||||
|
||||
public StarredArtistSyncDialog(Runnable onCancel) {
|
||||
this.onCancel = onCancel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
DialogStarredArtistSyncBinding bind = DialogStarredArtistSyncBinding.inflate(getLayoutInflater());
|
||||
|
||||
starredArtistsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredArtistsSyncViewModel.class);
|
||||
|
||||
return new MaterialAlertDialogBuilder(getActivity())
|
||||
.setView(bind.getRoot())
|
||||
.setTitle(R.string.starred_artist_sync_dialog_title)
|
||||
.setPositiveButton(R.string.starred_sync_dialog_positive_button, null)
|
||||
.setNeutralButton(R.string.starred_sync_dialog_neutral_button, null)
|
||||
.setNegativeButton(R.string.starred_sync_dialog_negative_button, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setButtonAction(requireContext());
|
||||
}
|
||||
|
||||
private void setButtonAction(Context context) {
|
||||
androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog();
|
||||
|
||||
if (dialog != null) {
|
||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
starredArtistsSyncViewModel.getStarredArtistSongs(requireActivity()).observe(this, allSongs -> {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(allSongs),
|
||||
allSongs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||
neutralButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredArtistsSyncEnabled(true);
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
|
||||
negativeButton.setOnClickListener(v -> {
|
||||
Preferences.setStarredArtistsSyncEnabled(false);
|
||||
if (onCancel != null) onCancel.run();
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ public class StarredSyncDialog extends DialogFragment {
|
|||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
||||
if (songs != null) {
|
||||
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -52,6 +55,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
private FragmentAlbumPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private AlbumPageViewModel albumPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
|
|
@ -74,6 +78,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
bind = FragmentAlbumPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
|
|
@ -91,6 +96,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -119,7 +132,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
|
||||
if (item.getItemId() == R.id.action_download_album) {
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
@ -269,10 +289,15 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.songRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, false, false, album);
|
||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -295,4 +320,31 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
public void onMediaLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,19 +29,16 @@ import com.cappielloantonio.tempo.service.MediaManager;
|
|||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@UnstableApi
|
||||
|
|
@ -49,6 +46,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
private FragmentArtistPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private ArtistPageViewModel artistPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
private AlbumCatalogueAdapter albumCatalogueAdapter;
|
||||
|
|
@ -63,6 +61,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
bind = FragmentArtistPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
|
|
@ -80,6 +79,13 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -159,7 +165,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
|
||||
bind.artistPageRadioButton.setOnClickListener(v -> {
|
||||
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (!songs.isEmpty()) {
|
||||
if (songs != null && !songs.isEmpty()) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
||||
activity.setBottomSheetInPeek(true);
|
||||
} else {
|
||||
|
|
@ -172,8 +178,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
private void initTopSongsView() {
|
||||
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, true, null);
|
||||
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (songs == null) {
|
||||
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
|
||||
|
|
@ -183,6 +191,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
if (bind != null)
|
||||
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -273,4 +282,31 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
|
|||
public void onArtistLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
|||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
|
@ -109,10 +111,14 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
|||
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
||||
if (isVisible() && getActivity() != null) {
|
||||
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,17 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
|
@ -40,6 +46,7 @@ import java.util.Objects;
|
|||
@UnstableApi
|
||||
public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
private static final String TAG = "DownloadFragment";
|
||||
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
|
||||
|
||||
private FragmentDownloadBinding bind;
|
||||
private MainActivity activity;
|
||||
|
|
@ -129,8 +136,27 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||
}
|
||||
});
|
||||
|
||||
downloadViewModel.getRefreshResult().observe(getViewLifecycleOwner(), count -> {
|
||||
if (count == null || bind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count == -1) {
|
||||
Toast.makeText(requireContext(), R.string.download_refresh_no_directory, Toast.LENGTH_SHORT).show();
|
||||
} else if (count == 0) {
|
||||
Toast.makeText(requireContext(), R.string.download_refresh_no_changes, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
getResources().getQuantityString(R.plurals.download_refresh_removed, count, count),
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
});
|
||||
|
||||
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
|
||||
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
|
||||
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
|
||||
}
|
||||
|
||||
private void finishDownloadView(List<Child> songs) {
|
||||
|
|
@ -216,6 +242,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
||||
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
||||
return true;
|
||||
} else if (menuItem.getItemId() == R.id.menu_download_set_directory) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -267,4 +297,21 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||
public void onDownloadGroupLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_PICK_DIRECTORY && resultCode == Activity.RESULT_OK) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
requireContext().getContentResolver().takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
);
|
||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||
ExternalAudioReader.refreshCache();
|
||||
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
package com.cappielloantonio.tempo.ui.fragment
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager
|
||||
import com.cappielloantonio.tempo.service.MediaService
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
|
||||
class EqualizerFragment : Fragment() {
|
||||
|
||||
private var equalizerManager: EqualizerManager? = null
|
||||
private lateinit var eqBandsContainer: LinearLayout
|
||||
private lateinit var eqSwitch: Switch
|
||||
private lateinit var resetButton: Button
|
||||
private lateinit var safeSpace: Space
|
||||
private val bandSeekBars = mutableListOf<SeekBar>()
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
val binder = service as MediaService.LocalBinder
|
||||
equalizerManager = binder.getEqualizerManager()
|
||||
initUI()
|
||||
restoreEqualizerPreferences()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||
equalizerManager = null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Intent(requireContext(), MediaService::class.java).also { intent ->
|
||||
intent.action = MediaService.ACTION_BIND_EQUALIZER
|
||||
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
requireActivity().unbindService(connection)
|
||||
equalizerManager = null
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val root = inflater.inflate(R.layout.fragment_equalizer, container, false)
|
||||
eqSwitch = root.findViewById(R.id.equalizer_switch)
|
||||
eqSwitch.isChecked = Preferences.isEqualizerEnabled()
|
||||
eqSwitch.jumpDrawablesToCurrentState()
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
eqBandsContainer = view.findViewById(R.id.eq_bands_container)
|
||||
resetButton = view.findViewById(R.id.equalizer_reset_button)
|
||||
safeSpace = view.findViewById(R.id.equalizer_bottom_space)
|
||||
}
|
||||
|
||||
private fun initUI() {
|
||||
val manager = equalizerManager
|
||||
val notSupportedView = view?.findViewById<LinearLayout>(R.id.equalizer_not_supported_container)
|
||||
val switchRow = view?.findViewById<View>(R.id.equalizer_switch_row)
|
||||
|
||||
if (manager == null || manager.getNumberOfBands().toInt() == 0) {
|
||||
switchRow?.visibility = View.GONE
|
||||
resetButton.visibility = View.GONE
|
||||
eqBandsContainer.visibility = View.GONE
|
||||
safeSpace.visibility = View.GONE
|
||||
notSupportedView?.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
|
||||
notSupportedView?.visibility = View.GONE
|
||||
switchRow?.visibility = View.VISIBLE
|
||||
resetButton.visibility = View.VISIBLE
|
||||
eqBandsContainer.visibility = View.VISIBLE
|
||||
safeSpace.visibility = View.VISIBLE
|
||||
|
||||
eqSwitch.setOnCheckedChangeListener(null)
|
||||
updateUiEnabledState(eqSwitch.isChecked)
|
||||
eqSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
manager.setEnabled(isChecked)
|
||||
Preferences.setEqualizerEnabled(isChecked)
|
||||
updateUiEnabledState(isChecked)
|
||||
}
|
||||
|
||||
createBandSliders()
|
||||
|
||||
resetButton.setOnClickListener {
|
||||
resetEqualizer()
|
||||
saveBandLevelsToPreferences()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUiEnabledState(isEnabled: Boolean) {
|
||||
resetButton.isEnabled = isEnabled
|
||||
bandSeekBars.forEach { it.isEnabled = isEnabled }
|
||||
}
|
||||
|
||||
private fun formatDb(value: Int): String = if (value > 0) "+$value dB" else "$value dB"
|
||||
|
||||
private fun createBandSliders() {
|
||||
val manager = equalizerManager ?: return
|
||||
eqBandsContainer.removeAllViews()
|
||||
bandSeekBars.clear()
|
||||
val bands = manager.getNumberOfBands()
|
||||
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
|
||||
val minLevelDb = bandLevelRange[0] / 100
|
||||
val maxLevelDb = bandLevelRange[1] / 100
|
||||
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
val band = i.toShort()
|
||||
val freq = manager.getCenterFreq(band) ?: 0
|
||||
|
||||
val row = LinearLayout(requireContext()).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
val topBottomMarginDp = 16
|
||||
topMargin = topBottomMarginDp.dpToPx(context)
|
||||
bottomMargin = topBottomMarginDp.dpToPx(context)
|
||||
}
|
||||
setPadding(0, 8, 0, 8)
|
||||
}
|
||||
|
||||
val freqLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply {
|
||||
text = if (freq >= 1000) {
|
||||
if (freq % 1000 == 0) {
|
||||
"${freq / 1000} kHz"
|
||||
} else {
|
||||
String.format("%.1f kHz", freq / 1000f)
|
||||
}
|
||||
} else {
|
||||
"$freq Hz"
|
||||
}
|
||||
gravity = Gravity.START
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
|
||||
}
|
||||
row.addView(freqLabel)
|
||||
|
||||
val initialLevelDb = (savedLevels.getOrNull(i) ?: (manager.getBandLevel(band) ?: 0)) / 100
|
||||
val dbLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply {
|
||||
text = formatDb(initialLevelDb)
|
||||
setPadding(12, 0, 0, 0)
|
||||
gravity = Gravity.END
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
|
||||
}
|
||||
|
||||
val seekBar = SeekBar(requireContext()).apply {
|
||||
max = maxLevelDb - minLevelDb
|
||||
progress = initialLevelDb - minLevelDb
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 6f)
|
||||
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
val thisLevelDb = progress + minLevelDb
|
||||
if (fromUser) {
|
||||
manager.setBandLevel(band, (thisLevelDb * 100).toShort())
|
||||
saveBandLevelsToPreferences()
|
||||
}
|
||||
dbLabel.text = formatDb(thisLevelDb)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
}
|
||||
bandSeekBars.add(seekBar)
|
||||
row.addView(seekBar)
|
||||
row.addView(dbLabel)
|
||||
eqBandsContainer.addView(row)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetEqualizer() {
|
||||
val manager = equalizerManager ?: return
|
||||
val bands = manager.getNumberOfBands()
|
||||
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
|
||||
val minLevelDb = bandLevelRange[0] / 100
|
||||
val midLevelDb = 0
|
||||
|
||||
for (i in 0 until bands) {
|
||||
manager.setBandLevel(i.toShort(), (0).toShort())
|
||||
bandSeekBars.getOrNull(i)?.progress = midLevelDb - minLevelDb
|
||||
}
|
||||
Preferences.setEqualizerBandLevels(ShortArray(bands.toInt()))
|
||||
}
|
||||
|
||||
private fun saveBandLevelsToPreferences() {
|
||||
val manager = equalizerManager ?: return
|
||||
val bands = manager.getNumberOfBands()
|
||||
val levels = ShortArray(bands.toInt()) { i -> manager.getBandLevel(i.toShort()) ?: 0 }
|
||||
Preferences.setEqualizerBandLevels(levels)
|
||||
}
|
||||
|
||||
private fun restoreEqualizerPreferences() {
|
||||
val manager = equalizerManager ?: return
|
||||
eqSwitch.isChecked = Preferences.isEqualizerEnabled()
|
||||
updateUiEnabledState(eqSwitch.isChecked)
|
||||
|
||||
val bands = manager.getNumberOfBands()
|
||||
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
|
||||
val minLevelDb = bandLevelRange[0] / 100
|
||||
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
val savedDb = savedLevels[i] / 100
|
||||
manager.setBandLevel(i.toShort(), (savedDb * 100).toShort())
|
||||
bandSeekBars.getOrNull(i)?.progress = savedDb - minLevelDb
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun Int.dpToPx(context: Context): Int =
|
||||
(this * context.resources.displayMetrics.density).toInt()
|
||||
|
|
@ -9,6 +9,7 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -40,6 +41,7 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Share;
|
||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
|
||||
|
|
@ -60,9 +62,12 @@ import com.cappielloantonio.tempo.util.MusicUtil;
|
|||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.UIUtil;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -74,6 +79,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
private FragmentHomeTabMusicBinding bind;
|
||||
private MainActivity activity;
|
||||
private HomeViewModel homeViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private DiscoverSongAdapter discoverSongAdapter;
|
||||
private SimilarTrackAdapter similarMusicAdapter;
|
||||
|
|
@ -101,6 +107,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
|
||||
|
|
@ -113,6 +120,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
|
||||
initSyncStarredView();
|
||||
initSyncStarredAlbumsView();
|
||||
initSyncStarredArtistsView();
|
||||
initDiscoverSongSlideView();
|
||||
initSimilarSongView();
|
||||
initArtistRadio();
|
||||
|
|
@ -138,12 +146,18 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observeStarredSongsPlayback();
|
||||
observeTopSongsPlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
refreshSharesView();
|
||||
if (topSongAdapter != null) setTopSongsMediaBrowserListenableFuture();
|
||||
if (starredSongAdapter != null) setStarredSongsMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -265,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
}
|
||||
|
||||
private void initSyncStarredView() {
|
||||
if (Preferences.isStarredSyncEnabled()) {
|
||||
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
|
|
@ -318,32 +332,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
|
||||
private void initSyncStarredAlbumsView() {
|
||||
if (Preferences.isStarredAlbumsSyncEnabled()) {
|
||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observeForever(new Observer<List<AlbumID3>>() {
|
||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<AlbumID3>>() {
|
||||
@Override
|
||||
public void onChanged(List<AlbumID3> albums) {
|
||||
if (albums != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
List<String> albumsToSync = new ArrayList<>();
|
||||
int albumCount = 0;
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
boolean needsSync = false;
|
||||
albumCount++;
|
||||
albumsToSync.add(album.getName());
|
||||
}
|
||||
|
||||
if (albumCount > 0) {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_albums_count,
|
||||
albumCount,
|
||||
albumCount
|
||||
);
|
||||
bind.homeSyncStarredAlbumsToSync.setText(message);
|
||||
}
|
||||
if (albums != null && !albums.isEmpty()) {
|
||||
checkIfAlbumsNeedSync(albums);
|
||||
}
|
||||
|
||||
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).removeObserver(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -353,26 +347,157 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
});
|
||||
|
||||
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
|
||||
homeViewModel.getAllStarredAlbumSongs().observeForever(new Observer<List<Child>>() {
|
||||
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
homeViewModel.getAllStarredAlbumSongs().removeObserver(this);
|
||||
if (songsToDownload > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void checkIfAlbumsNeedSync(List<AlbumID3> albums) {
|
||||
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
List<String> albumsNeedingSync = new ArrayList<>();
|
||||
|
||||
for (AlbumID3 album : albums) {
|
||||
boolean albumNeedsSync = false;
|
||||
// Check if any songs from this album need downloading
|
||||
for (Child song : allSongs) {
|
||||
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
albumNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (albumNeedsSync) {
|
||||
albumsNeedingSync.add(album.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_albums_count,
|
||||
albumsNeedingSync.size(),
|
||||
albumsNeedingSync.size()
|
||||
);
|
||||
bind.homeSyncStarredAlbumsToSync.setText(message);
|
||||
} else {
|
||||
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initSyncStarredArtistsView() {
|
||||
if (Preferences.isStarredArtistsSyncEnabled()) {
|
||||
homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<ArtistID3>>() {
|
||||
@Override
|
||||
public void onChanged(List<ArtistID3> artists) {
|
||||
if (artists != null && !artists.isEmpty()) {
|
||||
checkIfArtistsNeedSync(artists);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bind.homeSyncStarredArtistsCancel.setOnClickListener(v -> {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
bind.homeSyncStarredArtistsDownload.setOnClickListener(v -> {
|
||||
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null && !allSongs.isEmpty()) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
|
||||
for (Child song : allSongs) {
|
||||
if (!manager.isDownloaded(song.getId())) {
|
||||
manager.download(MappingUtil.mapDownload(song), new Download(song));
|
||||
songsToDownload++;
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
Toast.makeText(requireContext(),
|
||||
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void checkIfArtistsNeedSync(List<ArtistID3> artists) {
|
||||
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> allSongs) {
|
||||
if (allSongs != null) {
|
||||
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
|
||||
int songsToDownload = 0;
|
||||
List<String> artistsNeedingSync = new ArrayList<>();
|
||||
|
||||
for (ArtistID3 artist : artists) {
|
||||
boolean artistNeedsSync = false;
|
||||
// Check if any songs from this artist need downloading
|
||||
for (Child song : allSongs) {
|
||||
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
|
||||
!manager.isDownloaded(song.getId())) {
|
||||
songsToDownload++;
|
||||
artistNeedsSync = true;
|
||||
}
|
||||
}
|
||||
if (artistNeedsSync) {
|
||||
artistsNeedingSync.add(artist.getName());
|
||||
}
|
||||
}
|
||||
|
||||
if (songsToDownload > 0) {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.VISIBLE);
|
||||
String message = getResources().getQuantityString(
|
||||
R.plurals.home_sync_starred_artists_count,
|
||||
artistsNeedingSync.size(),
|
||||
artistsNeedingSync.size()
|
||||
);
|
||||
bind.homeSyncStarredArtistsToSync.setText(message);
|
||||
} else {
|
||||
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initDiscoverSongSlideView() {
|
||||
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
|
||||
|
||||
|
|
@ -475,8 +600,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
|
||||
bind.topSongsRecyclerView.setHasFixedSize(true);
|
||||
|
||||
topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
topSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
|
||||
setTopSongsMediaBrowserListenableFuture();
|
||||
reapplyTopSongsPlayback();
|
||||
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
|
||||
if (chronologies == null || chronologies.isEmpty()) {
|
||||
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
|
||||
|
|
@ -492,6 +619,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
.collect(Collectors.toList());
|
||||
|
||||
topSongAdapter.setItems(topSongs);
|
||||
reapplyTopSongsPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -513,8 +641,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
|
||||
bind.starredTracksRecyclerView.setHasFixedSize(true);
|
||||
|
||||
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
starredSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
|
||||
setStarredSongsMediaBrowserListenableFuture();
|
||||
reapplyStarredSongsPlayback();
|
||||
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
|
||||
if (songs == null) {
|
||||
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE);
|
||||
|
|
@ -525,6 +655,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
|
||||
|
||||
starredSongAdapter.setItems(songs);
|
||||
reapplyStarredSongsPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -954,6 +1085,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||
activity.setBottomSheetInPeek(true);
|
||||
}
|
||||
topSongAdapter.notifyDataSetChanged();
|
||||
starredSongAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1043,4 +1176,58 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
public void onShareLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observeStarredSongsPlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (starredSongAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
starredSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (starredSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
starredSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void observeTopSongsPlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (topSongAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
topSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (topSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
topSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyStarredSongsPlayback() {
|
||||
if (starredSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
starredSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void reapplyTopSongsPlayback() {
|
||||
if (topSongAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
topSongAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTopSongsMediaBrowserListenableFuture() {
|
||||
topSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
|
||||
private void setStarredSongsMediaBrowserListenableFuture() {
|
||||
starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
|
@ -24,11 +28,14 @@ import androidx.media3.common.util.RepeatModeUtil;
|
|||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.session.MediaBrowser;
|
||||
import androidx.media3.session.SessionToken;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavOptions;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||
|
|
@ -68,11 +75,15 @@ public class PlayerControllerFragment extends Fragment {
|
|||
private ImageButton playerOpenQueueButton;
|
||||
private ImageButton playerTrackInfo;
|
||||
private LinearLayout ratingContainer;
|
||||
private ImageButton equalizerButton;
|
||||
|
||||
private MainActivity activity;
|
||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private MediaService.LocalBinder mediaServiceBinder;
|
||||
private boolean isServiceBound = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
activity = (MainActivity) getActivity();
|
||||
|
|
@ -89,6 +100,7 @@ public class PlayerControllerFragment extends Fragment {
|
|||
initMediaListenable();
|
||||
initMediaLabelButton();
|
||||
initArtistLabelButton();
|
||||
initEqualizerButton();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
|
@ -126,6 +138,7 @@ public class PlayerControllerFragment extends Fragment {
|
|||
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
|
||||
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
|
||||
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
|
||||
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
|
||||
checkAndSetRatingContainerVisibility();
|
||||
}
|
||||
|
||||
|
|
@ -426,6 +439,18 @@ public class PlayerControllerFragment extends Fragment {
|
|||
});
|
||||
}
|
||||
|
||||
private void initEqualizerButton() {
|
||||
equalizerButton.setOnClickListener(v -> {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
NavOptions navOptions = new NavOptions.Builder()
|
||||
.setLaunchSingleTop(true)
|
||||
.setPopUpTo(R.id.equalizerFragment, true)
|
||||
.build();
|
||||
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
||||
if (activity != null) activity.collapseBottomSheetDelayed();
|
||||
});
|
||||
}
|
||||
|
||||
public void goToControllerPage() {
|
||||
playerMediaCoverViewPager.setCurrentItem(0, false);
|
||||
}
|
||||
|
|
@ -461,4 +486,66 @@ public class PlayerControllerFragment extends Fragment {
|
|||
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
|
||||
// TODO Resettare lo skip del silenzio
|
||||
}
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mediaServiceBinder = (MediaService.LocalBinder) service;
|
||||
isServiceBound = true;
|
||||
checkEqualizerBands();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mediaServiceBinder = null;
|
||||
isServiceBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
private void bindMediaService() {
|
||||
Intent intent = new Intent(requireActivity(), MediaService.class);
|
||||
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
|
||||
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
isServiceBound = true;
|
||||
}
|
||||
|
||||
private void checkEqualizerBands() {
|
||||
if (mediaServiceBinder != null) {
|
||||
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
|
||||
short numBands = eqManager.getNumberOfBands();
|
||||
|
||||
if (equalizerButton != null) {
|
||||
if (numBands == 0) {
|
||||
equalizerButton.setVisibility(View.GONE);
|
||||
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams();
|
||||
params.startToEnd = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
|
||||
playerOpenQueueButton.setLayoutParams(params);
|
||||
} else {
|
||||
equalizerButton.setVisibility(View.VISIBLE);
|
||||
|
||||
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams();
|
||||
params.startToStart = ConstraintLayout.LayoutParams.UNSET;
|
||||
params.startToEnd = R.id.player_open_equalizer_button;
|
||||
playerOpenQueueButton.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
bindMediaService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (isServiceBound) {
|
||||
requireActivity().unbindService(serviceConnection);
|
||||
isServiceBound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import java.util.ArrayList;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
|
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
|
@ -115,10 +117,14 @@ public class PlayerCoverFragment extends Fragment {
|
|||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
||||
if (song != null && bind != null) {
|
||||
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
} else {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
}
|
||||
});
|
||||
|
||||
bind.innerButtonTopRight.setOnClickListener(view -> {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
|
|||
import android.content.ComponentName;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Layout;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||
import com.cappielloantonio.tempo.subsonic.models.Line;
|
||||
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
||||
import java.util.List;
|
||||
|
|
@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
private MediaBrowser mediaBrowser;
|
||||
private Handler syncLyricsHandler;
|
||||
private Runnable syncLyricsRunnable;
|
||||
private String currentLyrics;
|
||||
private LyricsList currentLyricsList;
|
||||
private String currentDescription;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
|
@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
initPanelContent();
|
||||
observeDownloadState();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
bind = null;
|
||||
currentLyrics = null;
|
||||
currentLyricsList = null;
|
||||
currentDescription = null;
|
||||
}
|
||||
|
||||
private void initOverlay() {
|
||||
bind.syncLyricsTapButton.setOnClickListener(view -> {
|
||||
playerBottomSheetViewModel.changeSyncLyricsState();
|
||||
});
|
||||
|
||||
bind.downloadLyricsButton.setOnClickListener(view -> {
|
||||
boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics();
|
||||
if (getContext() != null) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure,
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeBrowser() {
|
||||
|
|
@ -136,50 +155,91 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
}
|
||||
|
||||
private void initPanelContent() {
|
||||
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
setPanelContent(null, lyricsList);
|
||||
});
|
||||
} else {
|
||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||
setPanelContent(lyrics, null);
|
||||
});
|
||||
}
|
||||
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
|
||||
currentLyrics = lyrics;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
currentLyricsList = lyricsList;
|
||||
updatePanelContent();
|
||||
});
|
||||
|
||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||
currentDescription = description;
|
||||
updatePanelContent();
|
||||
});
|
||||
}
|
||||
|
||||
private void setPanelContent(String lyrics, LyricsList lyricsList) {
|
||||
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
|
||||
private void observeDownloadState() {
|
||||
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
|
||||
if (bind != null) {
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||
|
||||
if (lyrics != null && !lyrics.trim().equals("")) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
|
||||
setSyncLirics(lyricsList);
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||
} else if (description != null && !description.trim().equals("")) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
|
||||
if (cached != null && cached) {
|
||||
downloadButton.setIconResource(R.drawable.ic_done);
|
||||
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
|
||||
} else {
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
downloadButton.setIconResource(R.drawable.ic_download);
|
||||
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updatePanelContent() {
|
||||
if (bind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
|
||||
|
||||
if (hasStructuredLyrics(currentLyricsList)) {
|
||||
setSyncLirics(currentLyricsList);
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setEnabled(true);
|
||||
} else if (hasText(currentLyrics)) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
|
||||
bind.downloadLyricsButton.setEnabled(true);
|
||||
} else if (hasText(currentDescription)) {
|
||||
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription));
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.GONE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setEnabled(false);
|
||||
} else {
|
||||
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
|
||||
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
|
||||
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
|
||||
bind.syncLyricsTapButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setVisibility(View.GONE);
|
||||
bind.downloadLyricsButton.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private boolean hasStructuredLyrics(LyricsList lyricsList) {
|
||||
return lyricsList != null
|
||||
&& lyricsList.getStructuredLyrics() != null
|
||||
&& !lyricsList.getStructuredLyrics().isEmpty()
|
||||
&& lyricsList.getStructuredLyrics().get(0) != null
|
||||
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
|
||||
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private void setSyncLirics(LyricsList lyricsList) {
|
||||
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||
|
|
@ -198,28 +258,28 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
|
||||
private void defineProgressHandler() {
|
||||
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
|
||||
if (lyricsList != null) {
|
||||
|
||||
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
syncLyricsHandler = new Handler();
|
||||
syncLyricsRunnable = () -> {
|
||||
if (syncLyricsHandler != null) {
|
||||
if (bind != null) {
|
||||
displaySyncedLyrics();
|
||||
}
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
}
|
||||
};
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
} else {
|
||||
if (!hasStructuredLyrics(lyricsList)) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!lyricsList.getStructuredLyrics().get(0).getSynced()) {
|
||||
releaseHandler();
|
||||
return;
|
||||
}
|
||||
|
||||
syncLyricsHandler = new Handler();
|
||||
syncLyricsRunnable = () -> {
|
||||
if (syncLyricsHandler != null) {
|
||||
if (bind != null) {
|
||||
displaySyncedLyrics();
|
||||
}
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
}
|
||||
};
|
||||
|
||||
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
|
|||
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
|
||||
int timestamp = (int) (mediaBrowser.getCurrentPosition());
|
||||
|
||||
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
|
||||
if (hasStructuredLyrics(lyricsList)) {
|
||||
StringBuilder lyricsBuilder = new StringBuilder();
|
||||
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.cappielloantonio.tempo.service.MediaService;
|
|||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
|
@ -38,6 +39,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||
private InnerFragmentPlayerQueueBinding bind;
|
||||
|
||||
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
private PlayerSongQueueAdapter playerSongQueueAdapter;
|
||||
|
|
@ -48,6 +50,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||
View view = bind.getRoot();
|
||||
|
||||
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
initQueueRecyclerView();
|
||||
|
||||
|
|
@ -59,6 +62,9 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||
super.onStart();
|
||||
initializeBrowser();
|
||||
bindMediaController();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -110,9 +116,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||
|
||||
playerSongQueueAdapter = new PlayerSongQueueAdapter(this);
|
||||
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
|
||||
reapplyPlayback();
|
||||
|
||||
playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> {
|
||||
if (queue != null) {
|
||||
playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList()));
|
||||
reapplyPlayback();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -216,4 +225,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
|
|||
public void onMediaClick(Bundle bundle) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (playerSongQueueAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,9 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
|
@ -49,6 +52,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
private FragmentPlaylistPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private PlaylistPageViewModel playlistPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
|
||||
|
|
@ -94,6 +98,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
bind = FragmentPlaylistPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
|
|
@ -109,6 +114,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
super.onStart();
|
||||
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -128,7 +142,8 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
if (item.getItemId() == R.id.action_download_playlist) {
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (isVisible() && getActivity() != null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(child -> {
|
||||
Download toDownload = new Download(child);
|
||||
|
|
@ -136,7 +151,10 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
||||
return toDownload;
|
||||
}).collect(Collectors.toList())
|
||||
);
|
||||
);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
|
|
@ -246,10 +264,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.songRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
|
|
@ -270,4 +293,31 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
public void onMediaLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,11 @@ import android.content.ComponentName;
|
|||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -34,6 +31,7 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
|
|||
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
|
@ -46,6 +44,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
private FragmentSearchBinding bind;
|
||||
private MainActivity activity;
|
||||
private SearchViewModel searchViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private ArtistAdapter artistAdapter;
|
||||
private AlbumAdapter albumAdapter;
|
||||
|
|
@ -61,6 +60,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
bind = FragmentSearchBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
initSearchResultView();
|
||||
initSearchView();
|
||||
|
|
@ -73,6 +73,15 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -112,7 +121,10 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
|
||||
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +254,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
}
|
||||
|
||||
private boolean isQueryValid(String query) {
|
||||
return !query.equals("") && query.trim().length() > 2;
|
||||
return !query.equals("") && query.trim().length() > 1;
|
||||
}
|
||||
|
||||
private void inputFocus() {
|
||||
|
|
@ -260,6 +272,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
@Override
|
||||
public void onMediaClick(Bundle bundle) {
|
||||
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
|
||||
songHorizontalAdapter.notifyDataSetChanged();
|
||||
activity.setBottomSheetInPeek(true);
|
||||
}
|
||||
|
||||
|
|
@ -287,4 +300,31 @@ public class SearchFragment extends Fragment implements ClickCallback {
|
|||
public void onArtistLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.media.audiofx.AudioEffect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
|
|
@ -18,6 +24,9 @@ import androidx.appcompat.app.AppCompatDelegate;
|
|||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.NavOptions;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
|
@ -28,15 +37,19 @@ import com.cappielloantonio.tempo.R;
|
|||
import com.cappielloantonio.tempo.helper.ThemeHelper;
|
||||
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
|
||||
import com.cappielloantonio.tempo.interfaces.ScanCallback;
|
||||
import com.cappielloantonio.tempo.service.EqualizerManager;
|
||||
import com.cappielloantonio.tempo.service.MediaService;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
|
||||
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.UIUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
|
||||
|
||||
import java.util.Locale;
|
||||
|
|
@ -49,15 +62,41 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
private MainActivity activity;
|
||||
private SettingViewModel settingViewModel;
|
||||
|
||||
private ActivityResultLauncher<Intent> someActivityResultLauncher;
|
||||
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||
|
||||
private MediaService.LocalBinder mediaServiceBinder;
|
||||
private boolean isServiceBound = false;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
someActivityResultLauncher = registerForActivityResult(
|
||||
equalizerResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {}
|
||||
);
|
||||
|
||||
directoryPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
requireContext().getContentResolver().takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
);
|
||||
|
||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||
ExternalAudioReader.refreshCache();
|
||||
Toast.makeText(requireContext(), "Download folder set.", Toast.LENGTH_SHORT).show();
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +125,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
checkEqualizer();
|
||||
checkSystemEqualizer();
|
||||
checkCacheStorage();
|
||||
checkStorage();
|
||||
checkDownloadDirectory();
|
||||
|
||||
setStreamingCacheSize();
|
||||
setAppLanguage();
|
||||
|
|
@ -98,10 +138,17 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
actionScan();
|
||||
actionSyncStarredAlbums();
|
||||
actionSyncStarredTracks();
|
||||
actionSyncStarredArtists();
|
||||
actionChangeStreamingCacheStorage();
|
||||
actionChangeDownloadStorage();
|
||||
actionSetDownloadDirectory();
|
||||
actionDeleteDownloadStorage();
|
||||
actionKeepScreenOn();
|
||||
actionAutoDownloadLyrics();
|
||||
actionMiniPlayerHeart();
|
||||
|
||||
bindMediaService();
|
||||
actionAppEqualizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -124,8 +171,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
}
|
||||
}
|
||||
|
||||
private void checkEqualizer() {
|
||||
Preference equalizer = findPreference("equalizer");
|
||||
private void checkSystemEqualizer() {
|
||||
Preference equalizer = findPreference("system_equalizer");
|
||||
|
||||
if (equalizer == null) return;
|
||||
|
||||
|
|
@ -133,7 +180,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
|
||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||
equalizer.setOnPreferenceClickListener(preference -> {
|
||||
someActivityResultLauncher.launch(intent);
|
||||
equalizerResultLauncher.launch(intent);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
|
|
@ -150,7 +197,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||
storage.setVisible(false);
|
||||
} else {
|
||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
storage.setVisible(false);
|
||||
|
|
@ -166,13 +213,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||
storage.setVisible(false);
|
||||
} else {
|
||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
int pref = Preferences.getDownloadStoragePreference();
|
||||
if (pref == 0) {
|
||||
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||
} else if (pref == 1) {
|
||||
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||
} else {
|
||||
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
storage.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDownloadDirectory() {
|
||||
Preference storage = findPreference("download_storage");
|
||||
Preference directory = findPreference("set_download_directory");
|
||||
|
||||
if (directory == null) return;
|
||||
|
||||
String current = Preferences.getDownloadDirectoryUri();
|
||||
if (current != null) {
|
||||
if (storage != null) storage.setVisible(false);
|
||||
directory.setVisible(true);
|
||||
directory.setIcon(R.drawable.ic_close);
|
||||
directory.setTitle("Clear download folder");
|
||||
directory.setSummary(current);
|
||||
} else {
|
||||
if (storage != null) storage.setVisible(true);
|
||||
if (Preferences.getDownloadStoragePreference() == 2) {
|
||||
directory.setVisible(true);
|
||||
directory.setIcon(R.drawable.ic_folder);
|
||||
directory.setTitle("Set download folder");
|
||||
directory.setSummary("Choose a folder for downloaded music files");
|
||||
} else {
|
||||
directory.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStreamingCacheSize() {
|
||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||
|
||||
|
|
@ -281,7 +361,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private void actionSyncStarredArtists() {
|
||||
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (newValue instanceof Boolean) {
|
||||
if ((Boolean) newValue) {
|
||||
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
|
||||
((SwitchPreference)preference).setChecked(false);
|
||||
});
|
||||
dialog.show(activity.getSupportFragmentManager(), null);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void actionChangeStreamingCacheStorage() {
|
||||
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
|
||||
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
|
||||
|
|
@ -306,11 +400,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
@Override
|
||||
public void onPositiveClick() {
|
||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNegativeClick() {
|
||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNeutralClick() {
|
||||
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
});
|
||||
dialog.show(activity.getSupportFragmentManager(), null);
|
||||
|
|
@ -318,6 +420,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
});
|
||||
}
|
||||
|
||||
private void actionSetDownloadDirectory() {
|
||||
Preference pref = findPreference("set_download_directory");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
String current = Preferences.getDownloadDirectoryUri();
|
||||
|
||||
if (current != null) {
|
||||
Preferences.setDownloadDirectoryUri(null);
|
||||
Preferences.setDownloadStoragePreference(0);
|
||||
ExternalAudioReader.refreshCache();
|
||||
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
|
||||
checkStorage();
|
||||
checkDownloadDirectory();
|
||||
} else {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
directoryPickerLauncher.launch(intent);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void actionDeleteDownloadStorage() {
|
||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
||||
|
|
@ -326,6 +453,36 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
});
|
||||
}
|
||||
|
||||
private void actionMiniPlayerHeart() {
|
||||
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
|
||||
if (preference == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
|
||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
if (newValue instanceof Boolean) {
|
||||
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void actionAutoDownloadLyrics() {
|
||||
SwitchPreference preference = findPreference("auto_download_lyrics");
|
||||
if (preference == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
|
||||
preference.setOnPreferenceChangeListener((pref, newValue) -> {
|
||||
if (newValue instanceof Boolean) {
|
||||
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void getScanStatus() {
|
||||
settingViewModel.getScanStatus(new ScanCallback() {
|
||||
@Override
|
||||
|
|
@ -353,4 +510,63 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
mediaServiceBinder = (MediaService.LocalBinder) service;
|
||||
isServiceBound = true;
|
||||
checkEqualizerBands();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
mediaServiceBinder = null;
|
||||
isServiceBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
private void bindMediaService() {
|
||||
Intent intent = new Intent(requireActivity(), MediaService.class);
|
||||
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
|
||||
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
isServiceBound = true;
|
||||
}
|
||||
|
||||
private void checkEqualizerBands() {
|
||||
if (mediaServiceBinder != null) {
|
||||
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
|
||||
short numBands = eqManager.getNumberOfBands();
|
||||
Preference appEqualizer = findPreference("app_equalizer");
|
||||
if (appEqualizer != null) {
|
||||
appEqualizer.setVisible(numBands > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void actionAppEqualizer() {
|
||||
Preference appEqualizer = findPreference("app_equalizer");
|
||||
if (appEqualizer != null) {
|
||||
appEqualizer.setOnPreferenceClickListener(preference -> {
|
||||
NavController navController = NavHostFragment.findNavController(this);
|
||||
NavOptions navOptions = new NavOptions.Builder()
|
||||
.setLaunchSingleTop(true)
|
||||
.setPopUpTo(R.id.equalizerFragment, true)
|
||||
.build();
|
||||
activity.setBottomNavigationBarVisibility(true);
|
||||
activity.setBottomSheetVisibility(true);
|
||||
navController.navigate(R.id.equalizerFragment, null, navOptions);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (isServiceBound) {
|
||||
requireActivity().unbindService(serviceConnection);
|
||||
isServiceBound = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
|
|||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||
private FragmentSongListPageBinding bind;
|
||||
private MainActivity activity;
|
||||
private SongListPageViewModel songListPageViewModel;
|
||||
private PlaybackViewModel playbackViewModel;
|
||||
|
||||
private SongHorizontalAdapter songHorizontalAdapter;
|
||||
|
||||
|
|
@ -69,6 +71,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||
bind = FragmentSongListPageBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class);
|
||||
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
|
||||
|
||||
init();
|
||||
initAppBar();
|
||||
|
|
@ -82,6 +85,15 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||
public void onStart() {
|
||||
super.onStart();
|
||||
initializeMediaBrowser();
|
||||
|
||||
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
|
||||
observePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setMediaBrowserListenableFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -189,11 +201,14 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
bind.songListRecyclerView.setHasFixedSize(true);
|
||||
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
|
||||
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
|
||||
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
|
||||
setMediaBrowserListenableFuture();
|
||||
reapplyPlayback();
|
||||
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
|
||||
isLoading = false;
|
||||
songHorizontalAdapter.setItems(songs);
|
||||
reapplyPlayback();
|
||||
setSongListPageSubtitle(songs);
|
||||
});
|
||||
|
||||
|
|
@ -325,4 +340,31 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
|
|||
public void onMediaLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
private void observePlayback() {
|
||||
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void reapplyPlayback() {
|
||||
if (songHorizontalAdapter != null) {
|
||||
String id = playbackViewModel.getCurrentSongId().getValue();
|
||||
Boolean playing = playbackViewModel.getIsPlaying().getValue();
|
||||
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMediaBrowserListenableFuture() {
|
||||
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
|
@ -37,6 +38,8 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
|
|||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
|
@ -54,6 +57,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
|
||||
private AlbumID3 album;
|
||||
|
||||
private TextView removeAllTextView;
|
||||
private List<Child> currentAlbumTracks = Collections.emptyList();
|
||||
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@Nullable
|
||||
|
|
@ -72,6 +79,12 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateRemoveAllVisibility);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
|
@ -163,7 +176,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
|
||||
downloadAll.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||
} else {
|
||||
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
});
|
||||
|
|
@ -182,19 +199,23 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
});
|
||||
});
|
||||
|
||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
|
||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
currentAlbumTracks = songs != null ? songs : Collections.emptyList();
|
||||
currentAlbumMediaItems = MappingUtil.mapDownloads(currentAlbumTracks);
|
||||
|
||||
removeAll.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||
removeAllTextView.setOnClickListener(v -> {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<Download> downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads);
|
||||
} else {
|
||||
currentAlbumTracks.forEach(ExternalAudioReader::delete);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
updateRemoveAllVisibility();
|
||||
});
|
||||
|
||||
initDownloadUI(removeAll);
|
||||
|
||||
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
|
||||
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
|
||||
if (artist != null) {
|
||||
|
|
@ -234,14 +255,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
dismiss();
|
||||
}
|
||||
|
||||
private void initDownloadUI(TextView removeAll) {
|
||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
private void updateRemoveAllVisibility() {
|
||||
if (removeAllTextView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||
removeAll.setVisibility(View.VISIBLE);
|
||||
if (currentAlbumTracks == null || currentAlbumTracks.isEmpty()) {
|
||||
removeAllTextView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<MediaItem> mediaItems = currentAlbumMediaItems;
|
||||
if (mediaItems == null || mediaItems.isEmpty()) {
|
||||
removeAllTextView.setVisibility(View.GONE);
|
||||
} else if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||
removeAllTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
removeAllTextView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
boolean hasLocal = currentAlbumTracks.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null);
|
||||
removeAllTextView.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeMediaBrowser() {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
|||
super.onStop();
|
||||
}
|
||||
|
||||
// TODO Utilizzare il viewmodel come tramite ed evitare le chiamate dirette
|
||||
// TODO Use the viewmodel as a conduit and avoid direct calls
|
||||
private void init(View view) {
|
||||
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
|
||||
CustomGlideRequest.Builder
|
||||
|
|
@ -81,7 +81,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
|
|||
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
|
||||
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
|
||||
favoriteToggle.setOnClickListener(v -> {
|
||||
artistBottomSheetViewModel.setFavorite();
|
||||
artistBottomSheetViewModel.setFavorite(requireContext());
|
||||
});
|
||||
|
||||
TextView playRadio = view.findViewById(R.id.play_radio_text_view);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
|
@ -117,10 +119,13 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
|
|||
|
||||
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
|
||||
removeAll.setOnClickListener(v -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
|
||||
} else {
|
||||
songs.forEach(ExternalAudioReader::delete);
|
||||
}
|
||||
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
|
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
|
|||
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
|
@ -39,6 +41,10 @@ import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
|
|
@ -48,6 +54,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
private SongBottomSheetViewModel songBottomSheetViewModel;
|
||||
private Child song;
|
||||
|
||||
private TextView downloadButton;
|
||||
private TextView removeButton;
|
||||
|
||||
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
|
||||
|
||||
@Nullable
|
||||
|
|
@ -66,6 +75,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateDownloadButtons);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
|
@ -157,25 +172,33 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
TextView download = view.findViewById(R.id.download_text_view);
|
||||
download.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
downloadButton = view.findViewById(R.id.download_text_view);
|
||||
downloadButton.setOnClickListener(v -> {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
} else {
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
TextView remove = view.findViewById(R.id.remove_text_view);
|
||||
remove.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
removeButton = view.findViewById(R.id.remove_text_view);
|
||||
removeButton.setOnClickListener(v -> {
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).remove(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
} else {
|
||||
ExternalAudioReader.delete(song);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
initDownloadUI(download, remove);
|
||||
updateDownloadButtons();
|
||||
|
||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||
addToPlaylist.setOnClickListener(v -> {
|
||||
|
|
@ -243,12 +266,19 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
dismiss();
|
||||
}
|
||||
|
||||
private void initDownloadUI(TextView download, TextView remove) {
|
||||
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
|
||||
remove.setVisibility(View.VISIBLE);
|
||||
private void updateDownloadButtons() {
|
||||
if (downloadButton == null || removeButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
boolean downloaded = DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId());
|
||||
downloadButton.setVisibility(downloaded ? View.GONE : View.VISIBLE);
|
||||
removeButton.setVisibility(downloaded ? View.VISIBLE : View.GONE);
|
||||
} else {
|
||||
download.setVisibility(View.VISIBLE);
|
||||
remove.setVisibility(View.GONE);
|
||||
boolean hasLocal = ExternalAudioReader.getUri(song) != null;
|
||||
downloadButton.setVisibility(hasLocal ? View.GONE : View.VISIBLE);
|
||||
removeButton.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue