Feat/playerqueue fab (#288)

This commit is contained in:
eddyizm 2025-11-30 10:25:11 -08:00 committed by GitHub
commit 45dda3af9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 414 additions and 99 deletions

View file

@ -1,8 +1,11 @@
package com.cappielloantonio.tempo.repository; package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase; import com.cappielloantonio.tempo.database.AppDatabase;
@ -52,6 +55,8 @@ public class QueueRepository {
public MutableLiveData<PlayQueue> getPlayQueue() { public MutableLiveData<PlayQueue> getPlayQueue() {
MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>(); MutableLiveData<PlayQueue> playQueue = new MutableLiveData<>();
Log.d(TAG, "Getting play queue from server...");
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getBookmarksClient() .getBookmarksClient()
.getPlayQueue() .getPlayQueue()
@ -59,12 +64,19 @@ public class QueueRepository {
@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() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) { if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getPlayQueue() != null) {
playQueue.setValue(response.body().getSubsonicResponse().getPlayQueue()); PlayQueue serverQueue = response.body().getSubsonicResponse().getPlayQueue();
Log.d(TAG, "Server returned play queue with " +
(serverQueue.getEntries() != null ? serverQueue.getEntries().size() : 0) + " items");
playQueue.setValue(serverQueue);
} else {
Log.d(TAG, "Server returned no play queue");
playQueue.setValue(null);
} }
} }
@Override @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to get play queue", t);
playQueue.setValue(null); playQueue.setValue(null);
} }
}); });
@ -73,18 +85,24 @@ public class QueueRepository {
} }
public void savePlayQueue(List<String> ids, String current, long position) { public void savePlayQueue(List<String> ids, String current, long position) {
Log.d(TAG, "Saving play queue to server - Items: " + ids.size() + ", Current: " + current);
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getBookmarksClient() .getBookmarksClient()
.savePlayQueue(ids, current, position) .savePlayQueue(ids, current, position)
.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()) {
Log.d(TAG, "Play queue saved successfully");
} else {
Log.d(TAG, "Play queue save failed with code: " + response.code());
}
} }
@Override @Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) { public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
Log.e(TAG, "Play queue save failed", t);
} }
}); });
} }
@ -123,10 +141,9 @@ public class QueueRepository {
private boolean isMediaInQueue(List<Queue> queue, Child media) { private boolean isMediaInQueue(List<Queue> queue, Child media) {
if (queue == null || media == null) return false; if (queue == null || media == null) return false;
return queue.stream().anyMatch(queueItem ->
return queue.stream().anyMatch(queueItem -> queueItem != null && media.getId() != null &&
queueItem != null && media.getId() != null && queueItem.getId().equals(media.getId())
queueItem.getId().equals(media.getId())
); );
} }
@ -146,8 +163,8 @@ public class QueueRepository {
List<Child> filteredToAdd = toAdd; List<Child> filteredToAdd = toAdd;
final List<Queue> finalMedia = media; final List<Queue> finalMedia = media;
filteredToAdd = toAdd.stream() filteredToAdd = toAdd.stream()
.filter(child -> !isMediaInQueue(finalMedia, child)) .filter(child -> !isMediaInQueue(finalMedia, child))
.collect(Collectors.toList()); .collect(Collectors.toList());
for (int i = 0; i < filteredToAdd.size(); i++) { for (int i = 0; i < filteredToAdd.size(); i++) {
Queue queueItem = new Queue(filteredToAdd.get(i)); Queue queueItem = new Queue(filteredToAdd.get(i));

View file

@ -18,8 +18,10 @@ import com.cappielloantonio.tempo.databinding.ItemPlayerQueueSongBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
@ -94,6 +96,20 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
} }
}); });
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(holder.itemView.getContext());
if (downloaderManager != null) {
boolean isDownloaded = downloaderManager.isDownloaded(song.getId());
if (isDownloaded) {
holder.item.downloadIndicatorIcon.setVisibility(View.VISIBLE);
} else {
holder.item.downloadIndicatorIcon.setVisibility(View.GONE);
}
} else {
holder.item.downloadIndicatorIcon.setVisibility(View.GONE);
}
if (Preferences.showItemRating()) { if (Preferences.showItemRating()) {
if (song.getStarred() == null && song.getUserRating() == null) { if (song.getStarred() == null && song.getUserRating() == null) {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE); holder.item.ratingIndicatorImageView.setVisibility(View.GONE);

View file

@ -2,16 +2,20 @@ package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName; import android.content.ComponentName;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.Observer;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser; import androidx.media3.session.MediaBrowser;
import androidx.media3.common.MediaItem;
import androidx.media3.session.SessionToken; import androidx.media3.session.SessionToken;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@ -19,11 +23,17 @@ import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding; import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerQueueBinding;
import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.service.DownloaderManager;
import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.PlayQueue;
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter; import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.util.Constants; 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.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@ -31,6 +41,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@UnstableApi @UnstableApi
@ -39,6 +50,18 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
private InnerFragmentPlayerQueueBinding bind; private InnerFragmentPlayerQueueBinding bind;
private com.google.android.material.floatingactionbutton.FloatingActionButton fabMenuToggle;
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabClearQueue;
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabShuffleQueue;
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabSaveToPlaylist;
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabDownloadAll;
private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabLoadQueue;
private boolean isMenuOpen = false;
private final int ANIMATION_DURATION = 250;
private final float FAB_VERTICAL_SPACING_DP = 70f;
private PlayerBottomSheetViewModel playerBottomSheetViewModel; private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private PlaybackViewModel playbackViewModel; private PlaybackViewModel playbackViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -53,6 +76,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
fabMenuToggle = bind.fabMenuToggle;
fabClearQueue = bind.fabClearQueue;
fabShuffleQueue = bind.fabShuffleQueue;
fabSaveToPlaylist = bind.fabSaveToPlaylist;
fabDownloadAll = bind.fabDownloadAll;
fabLoadQueue = bind.fabLoadQueue;
fabMenuToggle.setOnClickListener(v -> toggleFabMenu());
fabClearQueue.setOnClickListener(v -> handleClearQueueClick());
fabShuffleQueue.setOnClickListener(v -> handleShuffleQueueClick());
fabSaveToPlaylist.setOnClickListener(v -> handleSaveToPlaylistClick());
fabDownloadAll.setOnClickListener(v -> handleDownloadAllClick());
fabLoadQueue.setOnClickListener(v -> handleLoadQueueClick());
// Hide Load Queue FAB if sync is disabled
if (!Preferences.isSyncronizationEnabled()) {
fabLoadQueue.setVisibility(View.GONE);
}
initQueueRecyclerView(); initQueueRecyclerView();
return view; return view;
@ -62,8 +106,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
initializeBrowser(); initializeBrowser();
bindMediaController();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel); MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback(); observePlayback();
} }
@ -105,18 +147,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture); MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
} }
private void bindMediaController() {
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
initShuffleButton(mediaBrowser);
initCleanButton(mediaBrowser);
} catch (Exception exception) {
exception.printStackTrace();
}
}, MoreExecutors.directExecutor());
}
private void setMediaBrowserListenableFuture() { private void setMediaBrowserListenableFuture() {
playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); playerSongQueueAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
} }
@ -149,18 +179,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
fromPosition = viewHolder.getBindingAdapterPosition(); fromPosition = viewHolder.getBindingAdapterPosition();
toPosition = target.getBindingAdapterPosition(); toPosition = target.getBindingAdapterPosition();
/*
* Per spostare un elemento nella coda devo:
* - Spostare graficamente la traccia da una posizione all'altra con Collections.swap()
* - Spostare nel db la traccia, tramite QueueRepository
* - Notificare il Service dell'avvenuto spostamento con MusicPlayerRemote.moveSong()
*
* In onMove prendo la posizione di inizio e fine, ma solo al rilascio dell'elemento procedo allo spostamento
* In questo modo evito che ad ogni cambio di posizione vada a riscrivere nel db
* Al rilascio dell'elemento chiamo il metodo clearView()
*/
Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition); Collections.swap(playerSongQueueAdapter.getItems(), fromPosition, toPosition);
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition); recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
@ -188,46 +206,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
}).attachToRecyclerView(bind.playerQueueRecyclerView); }).attachToRecyclerView(bind.playerQueueRecyclerView);
} }
private void initShuffleButton(MediaBrowser mediaBrowser) {
bind.playerShuffleQueueFab.setOnClickListener(view -> {
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
int endPosition = playerSongQueueAdapter.getItems().size() - 1;
if (startPosition < endPosition) {
ArrayList<Integer> pool = new ArrayList<>();
for (int i = startPosition; i <= endPosition; i++) {
pool.add(i);
}
while (pool.size() >= 2) {
int fromPosition = (int) (Math.random() * (pool.size()));
int positionA = pool.get(fromPosition);
pool.remove(fromPosition);
int toPosition = (int) (Math.random() * (pool.size()));
int positionB = pool.get(toPosition);
pool.remove(toPosition);
Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB);
bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB);
}
MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
}
});
}
private void initCleanButton(MediaBrowser mediaBrowser) {
bind.playerCleanQueueButton.setOnClickListener(view -> {
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
int endPosition = playerSongQueueAdapter.getItems().size();
MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition);
});
}
private void updateNowPlayingItem() { private void updateNowPlayingItem() {
playerSongQueueAdapter.notifyDataSetChanged(); playerSongQueueAdapter.notifyDataSetChanged();
} }
@ -259,4 +237,216 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing); playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
} }
} }
/**
* Toggles the visibility and animates all six secondary FABs.
*/
private void toggleFabMenu() {
if (isMenuOpen) {
// CLOSE MENU (Reverse order for visual effect)
if (Preferences.isSyncronizationEnabled()) {
closeFab(fabLoadQueue, 4);
}
closeFab(fabSaveToPlaylist, 3);
closeFab(fabClearQueue, 2);
closeFab(fabDownloadAll, 1);
closeFab(fabShuffleQueue, 0);
fabMenuToggle.animate().rotation(0f).setDuration(ANIMATION_DURATION).start();
} else {
// OPEN MENU (lowest index at bottom)
openFab(fabShuffleQueue, 0);
openFab(fabDownloadAll, 1);
openFab(fabClearQueue, 2);
openFab(fabSaveToPlaylist, 3);
if (Preferences.isSyncronizationEnabled()) {
openFab(fabLoadQueue, 4);
}
fabMenuToggle.animate().rotation(45f).setDuration(ANIMATION_DURATION).start();
}
isMenuOpen = !isMenuOpen;
}
private void openFab(View fab, int index) {
final float displacement = getResources().getDisplayMetrics().density * (FAB_VERTICAL_SPACING_DP * (index + 1));
fab.setVisibility(View.VISIBLE);
fab.setAlpha(0f);
fab.setTranslationY(displacement); // Start at the hidden (closed) position
fab.animate()
.translationY(0f)
.alpha(1f)
.setDuration(ANIMATION_DURATION)
.start();
}
private void closeFab(View fab, int index) {
final float displacement = getResources().getDisplayMetrics().density * (FAB_VERTICAL_SPACING_DP * (index + 1));
fab.animate()
.translationY(displacement)
.alpha(0f)
.setDuration(ANIMATION_DURATION)
.withEndAction(() -> fab.setVisibility(View.GONE))
.start();
}
private void handleShuffleQueueClick() {
Log.d(TAG, "Shuffle Queue Clicked!");
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
int endPosition = playerSongQueueAdapter.getItems().size() - 1;
if (startPosition < endPosition) {
ArrayList<Integer> pool = new ArrayList<>();
for (int i = startPosition; i <= endPosition; i++) {
pool.add(i);
}
while (pool.size() >= 2) {
int fromPosition = (int) (Math.random() * (pool.size()));
int positionA = pool.get(fromPosition);
pool.remove(fromPosition);
int toPosition = (int) (Math.random() * (pool.size()));
int positionB = pool.get(toPosition);
pool.remove(toPosition);
Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB);
bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB);
}
MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
}
} catch (Exception e) {
Log.e(TAG, "Error shuffling queue", e);
}
toggleFabMenu();
}, MoreExecutors.directExecutor());
}
private void handleClearQueueClick() {
Log.d(TAG, "Clear Queue Clicked!");
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1;
int endPosition = playerSongQueueAdapter.getItems().size();
MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition);
bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition - startPosition);
} catch (Exception e) {
Log.e(TAG, "Error clearing queue", e);
}
toggleFabMenu();
}, MoreExecutors.directExecutor());
}
private void handleSaveToPlaylistClick() {
Log.d(TAG, "Save to Playlist Clicked!");
List<Child> queueSongs = playerSongQueueAdapter.getItems();
if (queueSongs == null || queueSongs.isEmpty()) {
Toast.makeText(requireContext(), "Queue is empty", Toast.LENGTH_SHORT).show();
toggleFabMenu();
return;
}
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(queueSongs));
PlaylistChooserDialog dialog = new PlaylistChooserDialog();
dialog.setArguments(bundle);
dialog.show(requireActivity().getSupportFragmentManager(), null);
toggleFabMenu();
}
private void handleDownloadAllClick() {
Log.d(TAG, "Download All Clicked!");
List<Child> queueSongs = playerSongQueueAdapter.getItems();
if (queueSongs == null || queueSongs.isEmpty()) {
Toast.makeText(requireContext(), "Queue is empty", Toast.LENGTH_SHORT).show();
toggleFabMenu();
return;
}
List<MediaItem> mediaItemsToDownload = MappingUtil.mapMediaItems(queueSongs);
List<com.cappielloantonio.tempo.model.Download> downloadModels = new ArrayList<>();
for (Child child : queueSongs) {
com.cappielloantonio.tempo.model.Download downloadModel =
new com.cappielloantonio.tempo.model.Download(child);
downloadModel.setArtist(child.getArtist());
downloadModel.setAlbum(child.getAlbum());
downloadModel.setCoverArtId(child.getCoverArtId());
downloadModels.add(downloadModel);
}
DownloaderManager downloaderManager = DownloadUtil.getDownloadTracker(requireContext());
if (downloaderManager != null) {
downloaderManager.download(mediaItemsToDownload, downloadModels);
Toast.makeText(requireContext(), "Starting download of " + queueSongs.size() + " songs in the background.", Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "DownloaderManager not initialized. Check DownloadUtil.");
Toast.makeText(requireContext(), "Download service unavailable.", Toast.LENGTH_SHORT).show();
}
toggleFabMenu();
}
private void handleLoadQueueClick() {
Log.d(TAG, "Load Queue Clicked!");
if (!Preferences.isSyncronizationEnabled()) {
toggleFabMenu();
return;
}
PlayerBottomSheetViewModel playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
playerBottomSheetViewModel.getPlayQueue().observe(getViewLifecycleOwner(), new Observer<PlayQueue>() {
@Override
public void onChanged(PlayQueue playQueue) {
playerBottomSheetViewModel.getPlayQueue().removeObserver(this);
if (playQueue != null && playQueue.getEntries() != null && !playQueue.getEntries().isEmpty()) {
int currentIndex = 0;
for (int i = 0; i < playQueue.getEntries().size(); i++) {
if (playQueue.getEntries().get(i).getId().equals(playQueue.getCurrent())) {
currentIndex = i;
break;
}
}
MediaManager.startQueue(mediaBrowserListenableFuture, playQueue.getEntries(), currentIndex);
Toast.makeText(requireContext(), "Queue loaded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(requireContext(), "No saved queue found", Toast.LENGTH_SHORT).show();
}
toggleFabMenu();
}
});
new Handler().postDelayed(() -> {
if (isMenuOpen) {
toggleFabMenu();
}
}, 1000);
}
} }

View file

@ -9,7 +9,6 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.repository.AlbumRepository; import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
@ -21,7 +20,6 @@ import java.util.List;
public class AlbumListPageViewModel extends AndroidViewModel { public class AlbumListPageViewModel extends AndroidViewModel {
private final AlbumRepository albumRepository; private final AlbumRepository albumRepository;
private final DownloadRepository downloadRepository;
public String title; public String title;
public ArtistID3 artist; public ArtistID3 artist;
@ -32,9 +30,7 @@ public class AlbumListPageViewModel extends AndroidViewModel {
public AlbumListPageViewModel(@NonNull Application application) { public AlbumListPageViewModel(@NonNull Application application) {
super(application); super(application);
albumRepository = new AlbumRepository(); albumRepository = new AlbumRepository();
downloadRepository = new DownloadRepository();
} }
public LiveData<List<AlbumID3>> getAlbumList(LifecycleOwner owner) { public LiveData<List<AlbumID3>> getAlbumList(LifecycleOwner owner) {

View file

@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.viewmodel;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
@ -12,6 +13,7 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import com.cappielloantonio.tempo.interfaces.StarCallback; import com.cappielloantonio.tempo.interfaces.StarCallback;
import com.cappielloantonio.tempo.model.Download; import com.cappielloantonio.tempo.model.Download;
@ -291,13 +293,13 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
List<String> ids = queue.stream().map(Child::getId).collect(Collectors.toList()); List<String> ids = queue.stream().map(Child::getId).collect(Collectors.toList());
if (media != null) { if (media != null) {
queueRepository.savePlayQueue(ids, media.getId(), 0); // TODO: We need to get the actual playback position here
Log.d(TAG, "Saving play queue - Current: " + media.getId() + ", Items: " + ids.size());
queueRepository.savePlayQueue(ids, media.getId(), 0); // Still hardcoded to 0 for now
return true; return true;
} }
return false; return false;
} }
private void observeCachedLyrics(LifecycleOwner owner, String songId) { private void observeCachedLyrics(LifecycleOwner owner, String songId) {
if (TextUtils.isEmpty(songId)) { if (TextUtils.isEmpty(songId)) {
return; return;

View file

@ -1,18 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView
android:id="@+id/player_clean_queue_button"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/player_queue_clean_all_button" />
<com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost <com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -21,20 +14,74 @@
android:id="@+id/player_queue_recycler_view" android:id="@+id/player_queue_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="40dp"
android:paddingTop="8dp" android:paddingTop="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost> </com.cappielloantonio.tempo.helper.recyclerview.NestedScrollableHost>
<com.google.android.material.floatingactionbutton.FloatingActionButton <LinearLayout
android:id="@+id/player_shuffle_queue_fab" android:id="@+id/fab_menu_container"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="end"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp" android:layout_margin="16dp"
android:contentDescription="@string/content_description_shuffle_button" app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior">
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:srcCompat="@drawable/ic_shuffle" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_save_to_playlist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:text="@string/player_queue_save_to_playlist"
app:icon="@android:drawable/ic_menu_edit" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_clear_queue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:text="@string/player_queue_clean_all_button"
app:icon="@android:drawable/ic_menu_delete" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_download_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:text="@string/menu_download_all_button"
app:icon="@android:drawable/stat_sys_download_done" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_load_queue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:text="@string/player_queue_load_queue"
app:icon="@android:drawable/ic_menu_revert" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fab_shuffle_queue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:visibility="gone"
android:text="@string/content_description_shuffle_button"
app:icon="@drawable/ic_shuffle" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_menu_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="Toggle FAB Action menu"
tools:ignore="HardcodedText"
app:srcCompat="@drawable/ic_add" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -139,6 +139,17 @@
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/download_indicator_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:visibility="gone"
android:src="@drawable/ic_download" app:layout_constraintBottom_toBottomOf="@+id/queue_song_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/queue_song_holder_image"
app:layout_constraintTop_toTopOf="@+id/queue_song_cover_image_view"
tools:visibility="visible" />
<ImageView <ImageView
android:id="@+id/queue_song_holder_image" android:id="@+id/queue_song_holder_image"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -32,6 +32,8 @@
<item name="android:statusBarColor">?attr/colorSurface</item> <item name="android:statusBarColor">?attr/colorSurface</item>
<item name="android:navigationBarColor">?attr/colorSurface</item> <item name="android:navigationBarColor">?attr/colorSurface</item>
<item name="android:scrollbars">none</item> <item name="android:scrollbars">none</item>
<item name="floatingActionButtonStyle">@style/FloatingActionButtonStyle</item>
</style> </style>
<style name="Divider"> <style name="Divider">
@ -40,6 +42,21 @@
<item name="android:background">@color/dividerColor</item> <item name="android:background">@color/dividerColor</item>
</style> </style>
<style name="FloatingActionButtonStyle" parent="Widget.MaterialComponents.FloatingActionButton">
<item name="backgroundTint">?attr/colorSecondary</item>
<item name="tint">?attr/colorOnPrimary</item>
<item name="shapeAppearanceOverlay">@style/FabShapeStyle</item>
</style>
<style name="FabShapeStyle" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerSize">50%</item>
<item name="cornerSizeBottomLeft">0dp</item>
<item name="cornerFamilyTopLeft">rounded</item>
<item name="cornerFamilyTopRight">rounded</item>
<item name="cornerFamilyBottomLeft">rounded</item>
<item name="cornerFamilyBottomRight">rounded</item>
</style>
<style name="NoConnectionTextView"> <style name="NoConnectionTextView">
<item name="background">?attr/colorErrorContainer</item> <item name="background">?attr/colorErrorContainer</item>
<item name="android:textColor">?attr/colorOnErrorContainer</item> <item name="android:textColor">?attr/colorOnErrorContainer</item>

View file

@ -212,6 +212,8 @@
<string name="player_playback_speed">%1$.2fx</string> <string name="player_playback_speed">%1$.2fx</string>
<string name="player_queue_clean_all_button">Clean play queue</string> <string name="player_queue_clean_all_button">Clean play queue</string>
<string name="player_queue_save_queue_success">Saved play queue</string> <string name="player_queue_save_queue_success">Saved play queue</string>
<string name="player_queue_save_to_playlist">Save Queue to Playlist</string>
<string name="player_queue_load_queue">Load Queue</string>
<string name="player_lyrics_download_content_description">Download lyrics for offline playback</string> <string name="player_lyrics_download_content_description">Download lyrics for offline playback</string>
<string name="player_lyrics_downloaded_content_description">Lyrics downloaded for offline playback</string> <string name="player_lyrics_downloaded_content_description">Lyrics downloaded for offline playback</string>
<string name="player_lyrics_download_success">Lyrics saved for offline playback.</string> <string name="player_lyrics_download_success">Lyrics saved for offline playback.</string>

View file

@ -39,6 +39,8 @@
<item name="android:statusBarColor">?attr/colorSurface</item> <item name="android:statusBarColor">?attr/colorSurface</item>
<item name="android:navigationBarColor">?attr/colorSurface</item> <item name="android:navigationBarColor">?attr/colorSurface</item>
<item name="android:scrollbars">none</item> <item name="android:scrollbars">none</item>
<item name="floatingActionButtonStyle">@style/FloatingActionButtonStyle</item>
</style> </style>
<style name="Divider"> <style name="Divider">
@ -47,6 +49,21 @@
<item name="android:background">@color/dividerColor</item> <item name="android:background">@color/dividerColor</item>
</style> </style>
<style name="FloatingActionButtonStyle" parent="Widget.MaterialComponents.FloatingActionButton">
<item name="backgroundTint">?attr/colorSecondary</item>
<item name="tint">?attr/colorOnPrimary</item>
<item name="shapeAppearanceOverlay">@style/FabShapeStyle</item>
</style>
<style name="FabShapeStyle" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerSize">50%</item>
<item name="cornerSizeBottomLeft">0dp</item>
<item name="cornerFamilyTopLeft">rounded</item>
<item name="cornerFamilyTopRight">rounded</item>
<item name="cornerFamilyBottomLeft">rounded</item>
<item name="cornerFamilyBottomRight">rounded</item>
</style>
<style name="NoConnectionTextView"> <style name="NoConnectionTextView">
<item name="background">?attr/colorErrorContainer</item> <item name="background">?attr/colorErrorContainer</item>
<item name="android:textColor">?attr/colorOnErrorContainer</item> <item name="android:textColor">?attr/colorOnErrorContainer</item>