mirror of
https://github.com/antebudimir/tempus.git
synced 2026-04-15 16:27:26 +00:00
Port remove song of playlist from tempus ng (#457)
* feat: implement track removal from playlists with real-time UI updates - Added 'Remove from playlist' option to song bottom sheet (appears only when inside a playlist) - Implemented immediate UI refresh for track count and duration in playlist header - Fixed a bug where shuffling for covers scrambled the actual playlist song order - Improved PlaylistPageViewModel to clear stale data and handle isolated updates correctly - Added dedicated success/failure messages for track removal in English and Italian - Unified heart icon size to 14dp across all track list items * fix: missing code from port process The cherry-pick was missing the database getter and the function to remove a song from a playlist --------- Co-authored-by: beeetfarmer <176325048+beeetfarmer@users.noreply.github.com>
This commit is contained in:
parent
b403d69982
commit
4f8212d491
11 changed files with 256 additions and 20 deletions
|
|
@ -19,6 +19,9 @@ public interface PlaylistDao {
|
||||||
@Query("SELECT * FROM playlist")
|
@Query("SELECT * FROM playlist")
|
||||||
LiveData<List<Playlist>> getAll();
|
LiveData<List<Playlist>> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlist")
|
||||||
|
List<Playlist> getAllSync();
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
void insert(Playlist playlist);
|
void insert(Playlist playlist);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ package com.cappielloantonio.tempo.repository;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.OptIn;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.App;
|
import com.cappielloantonio.tempo.App;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
|
|
@ -23,8 +26,45 @@ import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class PlaylistRepository {
|
public class PlaylistRepository {
|
||||||
|
private static final MutableLiveData<Boolean> playlistUpdateTrigger = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public LiveData<Boolean> getPlaylistUpdateTrigger() {
|
||||||
|
return playlistUpdateTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyPlaylistChanged() {
|
||||||
|
playlistUpdateTrigger.postValue(true);
|
||||||
|
refreshAllPlaylists();
|
||||||
|
}
|
||||||
|
|
||||||
@androidx.media3.common.util.UnstableApi
|
@androidx.media3.common.util.UnstableApi
|
||||||
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
private final PlaylistDao playlistDao = AppDatabase.getInstance().playlistDao();
|
||||||
|
private static final MutableLiveData<List<Playlist>> allPlaylistsLiveData = new MutableLiveData<>();
|
||||||
|
|
||||||
|
public LiveData<List<Playlist>> getAllPlaylists(LifecycleOwner owner) {
|
||||||
|
refreshAllPlaylists();
|
||||||
|
return allPlaylistsLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshAllPlaylists() {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylists()
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null) {
|
||||||
|
List<Playlist> playlists = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||||
|
allPlaylistsLiveData.postValue(playlists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
public MutableLiveData<List<Playlist>> getPlaylists(boolean random, int size) {
|
||||||
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
MutableLiveData<List<Playlist>> listLivePlaylists = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
|
||||||
|
|
@ -104,9 +144,16 @@ public class PlaylistRepository {
|
||||||
return playlistLiveData;
|
return playlistLiveData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
public interface AddToPlaylistCallback {
|
||||||
|
void onSuccess();
|
||||||
|
void onFailure();
|
||||||
|
void onAllSkipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic, AddToPlaylistCallback callback) {
|
||||||
|
android.util.Log.d("PlaylistRepository", "addSongToPlaylist: id=" + playlistId + ", songs=" + songsId);
|
||||||
if (songsId.isEmpty()) {
|
if (songsId.isEmpty()) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();
|
if (callback != null) callback.onAllSkipped();
|
||||||
} else{
|
} else{
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
|
|
@ -114,17 +161,45 @@ public class PlaylistRepository {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_success), Toast.LENGTH_SHORT).show();
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
|
if (callback != null) callback.onSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_add_failure), Toast.LENGTH_SHORT).show();
|
if (callback != null) callback.onFailure();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeSongFromPlaylist(String playlistId, int index, AddToPlaylistCallback callback) {
|
||||||
|
ArrayList<Integer> indexes = new ArrayList<>();
|
||||||
|
indexes.add(index);
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.updatePlaylist(playlistId, null, true, null, indexes)
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
|
if (callback != null) {
|
||||||
|
if (response.isSuccessful()) callback.onSuccess();
|
||||||
|
else callback.onFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
if (callback != null) callback.onFailure();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId, Boolean playlistVisibilityIsPublic) {
|
||||||
|
addSongToPlaylist(playlistId, songsId, playlistVisibilityIsPublic, null);
|
||||||
|
}
|
||||||
|
|
||||||
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
|
|
@ -132,7 +207,7 @@ public class PlaylistRepository {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -145,20 +220,45 @@ public class PlaylistRepository {
|
||||||
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
public void updatePlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.deletePlaylist(playlistId)
|
.updatePlaylist(playlistId, name, true, null, null)
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
createPlaylist(null, name, songsId);
|
if (response.isSuccessful()) {
|
||||||
|
// After renaming, we need to handle the song list update.
|
||||||
|
// Subsonic doesn't have a "replace all songs" in updatePlaylist.
|
||||||
|
// So we might still need to recreate if the songs changed significantly,
|
||||||
|
// but if we just renamed, we should update the local pinned database.
|
||||||
|
updateLocalPinnedPlaylistName(playlistId, name);
|
||||||
|
notifyPlaylistChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If songsId is provided, we might want to re-sync them.
|
||||||
|
// For now, let's at least fix the name duplication issue.
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(markerClass = UnstableApi.class)
|
||||||
|
private void updateLocalPinnedPlaylistName(String id, String newName) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Playlist> pinned = playlistDao.getAllSync();
|
||||||
|
if (pinned != null) {
|
||||||
|
for (Playlist p : pinned) {
|
||||||
|
if (p.getId().equals(id)) {
|
||||||
|
p.setName(newName);
|
||||||
|
playlistDao.insert(p); // Replace strategy will update it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
public void deletePlaylist(String playlistId) {
|
public void deletePlaylist(String playlistId) {
|
||||||
App.getSubsonicClientInstance(false)
|
App.getSubsonicClientInstance(false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
|
|
@ -166,7 +266,7 @@ public class PlaylistRepository {
|
||||||
.enqueue(new Callback<ApiResponse>() {
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful()) notifyPlaylistChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -194,6 +294,49 @@ public class PlaylistRepository {
|
||||||
thread.start();
|
thread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
|
public void updatePinnedPlaylists() {
|
||||||
|
updatePinnedPlaylists(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@androidx.media3.common.util.UnstableApi
|
||||||
|
public void updatePinnedPlaylists(List<String> forceIds) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<Playlist> pinned = playlistDao.getAllSync();
|
||||||
|
if (pinned != null && !pinned.isEmpty()) {
|
||||||
|
App.getSubsonicClientInstance(false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.getPlaylists()
|
||||||
|
.enqueue(new Callback<ApiResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlaylists() != null) {
|
||||||
|
List<Playlist> remotes = response.body().getSubsonicResponse().getPlaylists().getPlaylists();
|
||||||
|
new Thread(() -> {
|
||||||
|
for (Playlist p : pinned) {
|
||||||
|
for (Playlist r : remotes) {
|
||||||
|
if (p.getId().equals(r.getId())) {
|
||||||
|
p.setName(r.getName());
|
||||||
|
p.setSongCount(r.getSongCount());
|
||||||
|
p.setDuration(r.getDuration());
|
||||||
|
p.setCoverArtId(r.getCoverArtId());
|
||||||
|
playlistDao.insert(p);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
private static class InsertThreadSafe implements Runnable {
|
private static class InsertThreadSafe implements Runnable {
|
||||||
private final PlaylistDao playlistDao;
|
private final PlaylistDao playlistDao;
|
||||||
private final Playlist playlist;
|
private final Playlist playlist;
|
||||||
|
|
|
||||||
|
|
@ -359,6 +359,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
|
||||||
private boolean onLongClick() {
|
private boolean onLongClick() {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
bundle.putParcelable(Constants.TRACK_OBJECT, songs.get(getBindingAdapterPosition()));
|
||||||
|
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
|
||||||
|
|
||||||
click.onMediaLongClick(bundle);
|
click.onMediaLongClick(bundle);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,9 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
});
|
});
|
||||||
|
|
||||||
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
bind.playlistPageShuffleButton.setOnClickListener(v -> {
|
||||||
Collections.shuffle(songs);
|
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> shuffledSongs = new java.util.ArrayList<>(songs);
|
||||||
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
|
java.util.Collections.shuffle(shuffledSongs);
|
||||||
|
MediaManager.startQueue(mediaBrowserListenableFuture, shuffledSongs, 0);
|
||||||
activity.setBottomSheetInPeek(true);
|
activity.setBottomSheetInPeek(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -227,32 +228,33 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
private void initBackCover() {
|
private void initBackCover() {
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
||||||
if (bind != null && songs != null && !songs.isEmpty()) {
|
if (bind != null && songs != null && !songs.isEmpty()) {
|
||||||
Collections.shuffle(songs);
|
java.util.List<com.cappielloantonio.tempo.subsonic.models.Child> randomSongs = new java.util.ArrayList<>(songs);
|
||||||
|
java.util.Collections.shuffle(randomSongs);
|
||||||
|
|
||||||
// Pic top-left
|
// Pic top-left
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), !songs.isEmpty() ? songs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), !randomSongs.isEmpty() ? randomSongs.get(0).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
.transform(new GranularRoundedCorners(CustomGlideRequest.CORNER_RADIUS, 0, 0, 0))
|
||||||
.into(bind.playlistCoverImageViewTopLeft);
|
.into(bind.playlistCoverImageViewTopLeft);
|
||||||
|
|
||||||
// Pic top-right
|
// Pic top-right
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 1 ? songs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 1 ? randomSongs.get(1).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
.transform(new GranularRoundedCorners(0, CustomGlideRequest.CORNER_RADIUS, 0, 0))
|
||||||
.into(bind.playlistCoverImageViewTopRight);
|
.into(bind.playlistCoverImageViewTopRight);
|
||||||
|
|
||||||
// Pic bottom-left
|
// Pic bottom-left
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 2 ? songs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 2 ? randomSongs.get(2).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
.transform(new GranularRoundedCorners(0, 0, 0, CustomGlideRequest.CORNER_RADIUS))
|
||||||
.into(bind.playlistCoverImageViewBottomLeft);
|
.into(bind.playlistCoverImageViewBottomLeft);
|
||||||
|
|
||||||
// Pic bottom-right
|
// Pic bottom-right
|
||||||
CustomGlideRequest.Builder
|
CustomGlideRequest.Builder
|
||||||
.from(requireContext(), songs.size() > 3 ? songs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
.from(requireContext(), randomSongs.size() > 3 ? randomSongs.get(3).getCoverArtId() : playlistPageViewModel.getPlaylist().getCoverArtId(), CustomGlideRequest.ResourceType.Song)
|
||||||
.build()
|
.build()
|
||||||
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
.transform(new GranularRoundedCorners(0, 0, CustomGlideRequest.CORNER_RADIUS, 0))
|
||||||
.into(bind.playlistCoverImageViewBottomRight);
|
.into(bind.playlistCoverImageViewBottomRight);
|
||||||
|
|
@ -271,6 +273,11 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
songHorizontalAdapter.setItems(songs);
|
songHorizontalAdapter.setItems(songs);
|
||||||
|
if (songs != null) {
|
||||||
|
bind.playlistSongCountLabel.setText(getString(R.string.playlist_song_count, songs.size()));
|
||||||
|
long totalDuration = songs.stream().mapToLong(s -> s.getDuration() != null ? s.getDuration() : 0).sum();
|
||||||
|
bind.playlistDurationLabel.setText(getString(R.string.playlist_duration, MusicUtil.getReadableDurationString(totalDuration, false)));
|
||||||
|
}
|
||||||
reapplyPlayback();
|
reapplyPlayback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -291,6 +298,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMediaLongClick(Bundle bundle) {
|
public void onMediaLongClick(Bundle bundle) {
|
||||||
|
bundle.putString(Constants.PLAYLIST_ID, playlistPageViewModel.getPlaylist().getId());
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,34 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
|
|
||||||
updateDownloadButtons();
|
updateDownloadButtons();
|
||||||
|
|
||||||
|
String playlistId = requireArguments().getString(Constants.PLAYLIST_ID);
|
||||||
|
int itemPosition = requireArguments().getInt(Constants.ITEM_POSITION, -1);
|
||||||
|
|
||||||
|
TextView removeFromPlaylist = view.findViewById(R.id.remove_from_playlist_text_view);
|
||||||
|
if (playlistId != null && itemPosition != -1) {
|
||||||
|
removeFromPlaylist.setVisibility(View.VISIBLE);
|
||||||
|
removeFromPlaylist.setOnClickListener(v -> {
|
||||||
|
songBottomSheetViewModel.removeFromPlaylist(playlistId, itemPosition, new com.cappielloantonio.tempo.repository.PlaylistRepository.AddToPlaylistCallback() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_success, Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure() {
|
||||||
|
Toast.makeText(requireContext(), R.string.playlist_chooser_dialog_toast_remove_failure, Toast.LENGTH_SHORT).show();
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAllSkipped() {
|
||||||
|
dismissBottomSheet();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
|
||||||
addToPlaylist.setOnClickListener(v -> {
|
addToPlaylist.setOnClickListener(v -> {
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ object Constants {
|
||||||
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
const val ARTIST_OBJECT = "ARTIST_OBJECT"
|
||||||
const val GENRE_OBJECT = "GENRE_OBJECT"
|
const val GENRE_OBJECT = "GENRE_OBJECT"
|
||||||
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
|
||||||
|
const val PLAYLIST_ID = "PLAYLIST_ID"
|
||||||
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
const val PODCAST_OBJECT = "PODCAST_OBJECT"
|
||||||
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
|
||||||
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,36 @@ public class PlaylistPageViewModel extends AndroidViewModel {
|
||||||
private Playlist playlist;
|
private Playlist playlist;
|
||||||
private boolean isOffline;
|
private boolean isOffline;
|
||||||
|
|
||||||
|
private final MutableLiveData<List<Child>> songLiveList = new MutableLiveData<>();
|
||||||
|
|
||||||
public PlaylistPageViewModel(@NonNull Application application) {
|
public PlaylistPageViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
||||||
playlistRepository = new PlaylistRepository();
|
playlistRepository = new PlaylistRepository();
|
||||||
|
playlistRepository.getPlaylistUpdateTrigger().observeForever(needsRefresh -> {
|
||||||
|
if (needsRefresh != null && needsRefresh && playlist != null) {
|
||||||
|
refreshSongs();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Child>> getPlaylistSongLiveList() {
|
public LiveData<List<Child>> getPlaylistSongLiveList() {
|
||||||
return playlistRepository.getPlaylistSongs(playlist.getId());
|
if (songLiveList.getValue() == null && playlist != null) {
|
||||||
|
refreshSongs();
|
||||||
|
}
|
||||||
|
return songLiveList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshSongs() {
|
||||||
|
if (playlist == null) return;
|
||||||
|
LiveData<List<Child>> remoteData = playlistRepository.getPlaylistSongs(playlist.getId());
|
||||||
|
remoteData.observeForever(new androidx.lifecycle.Observer<List<Child>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<Child> songs) {
|
||||||
|
songLiveList.postValue(songs);
|
||||||
|
remoteData.removeObserver(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Playlist getPlaylist() {
|
public Playlist getPlaylist() {
|
||||||
|
|
@ -35,7 +57,10 @@ public class PlaylistPageViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPlaylist(Playlist playlist) {
|
public void setPlaylist(Playlist playlist) {
|
||||||
this.playlist = playlist;
|
if (this.playlist == null || !this.playlist.getId().equals(playlist.getId())) {
|
||||||
|
this.playlist = playlist;
|
||||||
|
this.songLiveList.setValue(null); // Clear old data immediately
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Boolean> isPinned(LifecycleOwner owner) {
|
public LiveData<Boolean> isPinned(LifecycleOwner owner) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.cappielloantonio.tempo.model.Download;
|
||||||
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
import com.cappielloantonio.tempo.repository.AlbumRepository;
|
||||||
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
import com.cappielloantonio.tempo.repository.ArtistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
import com.cappielloantonio.tempo.repository.FavoriteRepository;
|
||||||
|
import com.cappielloantonio.tempo.repository.PlaylistRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SharingRepository;
|
import com.cappielloantonio.tempo.repository.SharingRepository;
|
||||||
import com.cappielloantonio.tempo.repository.SongRepository;
|
import com.cappielloantonio.tempo.repository.SongRepository;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
|
||||||
|
|
@ -39,6 +40,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
private final ArtistRepository artistRepository;
|
private final ArtistRepository artistRepository;
|
||||||
private final FavoriteRepository favoriteRepository;
|
private final FavoriteRepository favoriteRepository;
|
||||||
private final SharingRepository sharingRepository;
|
private final SharingRepository sharingRepository;
|
||||||
|
private final PlaylistRepository playlistRepository;
|
||||||
|
|
||||||
private Child song;
|
private Child song;
|
||||||
|
|
||||||
|
|
@ -52,6 +54,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
artistRepository = new ArtistRepository();
|
artistRepository = new ArtistRepository();
|
||||||
favoriteRepository = new FavoriteRepository();
|
favoriteRepository = new FavoriteRepository();
|
||||||
sharingRepository = new SharingRepository();
|
sharingRepository = new SharingRepository();
|
||||||
|
playlistRepository = new PlaylistRepository();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Child getSong() {
|
public Child getSong() {
|
||||||
|
|
@ -62,6 +65,10 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
this.song = song;
|
this.song = song;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void removeFromPlaylist(String playlistId, int index, PlaylistRepository.AddToPlaylistCallback callback) {
|
||||||
|
playlistRepository.removeSongFromPlaylist(playlistId, index, callback);
|
||||||
|
}
|
||||||
|
|
||||||
public void setFavorite(Context context) {
|
public void setFavorite(Context context) {
|
||||||
if (song.getStarred() != null) {
|
if (song.getStarred() != null) {
|
||||||
if (NetworkUtil.isOffline()) {
|
if (NetworkUtil.isOffline()) {
|
||||||
|
|
|
||||||
|
|
@ -164,6 +164,20 @@
|
||||||
android:paddingBottom="12dp"
|
android:paddingBottom="12dp"
|
||||||
android:text="@string/song_bottom_sheet_remove" />
|
android:text="@string/song_bottom_sheet_remove" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/remove_from_playlist_text_view"
|
||||||
|
style="@style/LabelMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:text="@string/song_bottom_sheet_remove_from_playlist"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/add_to_playlist_text_view"
|
android:id="@+id/add_to_playlist_text_view"
|
||||||
style="@style/LabelMedium"
|
style="@style/LabelMedium"
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,8 @@
|
||||||
<string name="playlist_chooser_dialog_title">Aggiungi a una playlist</string>
|
<string name="playlist_chooser_dialog_title">Aggiungi a una playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Aggiunta di un brano alla playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Impossibile aggiungere un brano alla playlist</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_remove_success">Canzone rimossa dalla playlist</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_remove_failure">Impossibile rimuovere la canzone dalla playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">Tutte le canzoni sono state saltate perché duplicate</string>
|
||||||
<string name="playlist_chooser_dialog_visibility_public">Pubblico</string>
|
<string name="playlist_chooser_dialog_visibility_public">Pubblico</string>
|
||||||
<string name="playlist_chooser_dialog_visibility_private">Privato</string>
|
<string name="playlist_chooser_dialog_visibility_private">Privato</string>
|
||||||
|
|
@ -448,7 +450,8 @@
|
||||||
<string name="song_bottom_sheet_instant_mix">Mix istantaneo</string>
|
<string name="song_bottom_sheet_instant_mix">Mix istantaneo</string>
|
||||||
<string name="song_bottom_sheet_play_next">Riproduci dopo</string>
|
<string name="song_bottom_sheet_play_next">Riproduci dopo</string>
|
||||||
<string name="song_bottom_sheet_rate">Valuta</string>
|
<string name="song_bottom_sheet_rate">Valuta</string>
|
||||||
<string name="song_bottom_sheet_remove">Rimuovi</string>
|
<string name="song_bottom_sheet_remove">Rimuovi dal dispositivo</string>
|
||||||
|
<string name="song_bottom_sheet_remove_from_playlist">Rimuovi dalla playlist</string>
|
||||||
<string name="song_bottom_sheet_share">Condividi</string>
|
<string name="song_bottom_sheet_share">Condividi</string>
|
||||||
<string name="song_list_page_downloaded">Scaricato</string>
|
<string name="song_list_page_downloaded">Scaricato</string>
|
||||||
<string name="song_list_page_most_played">Tracce più riprodotte</string>
|
<string name="song_list_page_most_played">Tracce più riprodotte</string>
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,8 @@
|
||||||
<string name="playlist_chooser_dialog_title">Add to a playlist</string>
|
<string name="playlist_chooser_dialog_title">Add to a playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_success">Added song(s) to playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
<string name="playlist_chooser_dialog_toast_add_failure">Failed to add song(s) to playlist</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_remove_success">Removed song from playlist</string>
|
||||||
|
<string name="playlist_chooser_dialog_toast_remove_failure">Failed to remove song from playlist</string>
|
||||||
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
<string name="playlist_chooser_dialog_toast_all_skipped">All songs were skipped as duplicates</string>
|
||||||
<string name="playlist_chooser_dialog_visibility_public">Public</string>
|
<string name="playlist_chooser_dialog_visibility_public">Public</string>
|
||||||
<string name="playlist_chooser_dialog_visibility_private">Private</string>
|
<string name="playlist_chooser_dialog_visibility_private">Private</string>
|
||||||
|
|
@ -472,7 +474,8 @@
|
||||||
<string name="song_bottom_sheet_instant_mix">Instant mix</string>
|
<string name="song_bottom_sheet_instant_mix">Instant mix</string>
|
||||||
<string name="song_bottom_sheet_play_next">Play next</string>
|
<string name="song_bottom_sheet_play_next">Play next</string>
|
||||||
<string name="song_bottom_sheet_rate">Rate</string>
|
<string name="song_bottom_sheet_rate">Rate</string>
|
||||||
<string name="song_bottom_sheet_remove">Remove</string>
|
<string name="song_bottom_sheet_remove">Remove from device</string>
|
||||||
|
<string name="song_bottom_sheet_remove_from_playlist">Remove from playlist</string>
|
||||||
<string name="song_bottom_sheet_share">Share</string>
|
<string name="song_bottom_sheet_share">Share</string>
|
||||||
<string name="song_list_page_downloaded">Downloaded</string>
|
<string name="song_list_page_downloaded">Downloaded</string>
|
||||||
<string name="song_list_page_most_played">Most played tracks</string>
|
<string name="song_list_page_most_played">Most played tracks</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue