From 38fb2c69f16ff7a3f3d6f5c6ec1a1ec591d96284 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Mon, 24 Nov 2025 11:36:56 -0800 Subject: [PATCH 1/6] wip: added fab, need to implement actions --- .../ui/fragment/PlayerQueueFragment.java | 184 ++++++++++++++---- .../layout/inner_fragment_player_queue.xml | 84 +++++++- 2 files changed, 228 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java index b8b1326f..baecb480 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java @@ -39,6 +39,20 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { 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 fabSaveQueue; + 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 PlaybackViewModel playbackViewModel; private ListenableFuture mediaBrowserListenableFuture; @@ -53,6 +67,24 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class); playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class); + fabMenuToggle = bind.fabMenuToggle; + fabClearQueue = bind.fabClearQueue; + fabShuffleQueue = bind.fabShuffleQueue; + + fabSaveToPlaylist = bind.fabSaveToPlaylist; + fabDownloadAll = bind.fabDownloadAll; + fabSaveQueue = bind.fabSaveQueue; + fabLoadQueue = bind.fabLoadQueue; + + fabMenuToggle.setOnClickListener(v -> toggleFabMenu()); + fabClearQueue.setOnClickListener(v -> handleClearQueueClick()); + fabShuffleQueue.setOnClickListener(v -> handleShuffleQueueClick()); + + fabSaveToPlaylist.setOnClickListener(v -> handleSaveToPlaylistClick()); + fabDownloadAll.setOnClickListener(v -> handleDownloadAllClick()); + fabSaveQueue.setOnClickListener(v -> handleSaveQueueClick()); + fabLoadQueue.setOnClickListener(v -> handleLoadQueueClick()); + initQueueRecyclerView(); return view; @@ -109,8 +141,8 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { mediaBrowserListenableFuture.addListener(() -> { try { MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); - initShuffleButton(mediaBrowser); - initCleanButton(mediaBrowser); + // initShuffleButton(mediaBrowser); + // initCleanButton(mediaBrowser); } catch (Exception exception) { exception.printStackTrace(); } @@ -188,45 +220,45 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { }).attachToRecyclerView(bind.playerQueueRecyclerView); } - private void initShuffleButton(MediaBrowser mediaBrowser) { - bind.playerShuffleQueueFab.setOnClickListener(view -> { - int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1; - int endPosition = playerSongQueueAdapter.getItems().size() - 1; + // private void initShuffleButton(MediaBrowser mediaBrowser) { + // bind.playerShuffleQueueFab.setOnClickListener(view -> { + // int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1; + // int endPosition = playerSongQueueAdapter.getItems().size() - 1; - if (startPosition < endPosition) { - ArrayList pool = new ArrayList<>(); + // if (startPosition < endPosition) { + // ArrayList pool = new ArrayList<>(); - for (int i = startPosition; i <= endPosition; i++) { - pool.add(i); - } + // 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); + // 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); + // 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); - } + // Collections.swap(playerSongQueueAdapter.getItems(), positionA, positionB); + // bind.playerQueueRecyclerView.getAdapter().notifyItemMoved(positionA, positionB); + // } - MediaManager.shuffle(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition); - } - }); - } + // 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(); + // 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); - }); - } + // MediaManager.removeRange(mediaBrowserListenableFuture, playerSongQueueAdapter.getItems(), startPosition, endPosition); + // bind.playerQueueRecyclerView.getAdapter().notifyItemRangeRemoved(startPosition, endPosition); + // }); + // } private void updateNowPlayingItem() { playerSongQueueAdapter.notifyDataSetChanged(); @@ -259,4 +291,90 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { 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) + closeFab(fabSaveToPlaylist, 5); + closeFab(fabDownloadAll, 4); + closeFab(fabSaveQueue, 3); + closeFab(fabLoadQueue, 2); + closeFab(fabClearQueue, 1); + closeFab(fabShuffleQueue, 0); + + fabMenuToggle.animate().rotation(0f).setDuration(ANIMATION_DURATION).start(); + } else { + // OPEN MENU (lowest index at bottom) + openFab(fabShuffleQueue, 0); + openFab(fabClearQueue, 1); + openFab(fabLoadQueue, 2); + openFab(fabSaveQueue, 3); + openFab(fabDownloadAll, 4); + openFab(fabSaveToPlaylist, 5); + + 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!"); + toggleFabMenu(); + // TODO: Insert existing shuffle logic here + } + + private void handleClearQueueClick() { + Log.d(TAG, "Clear Queue Clicked!"); + toggleFabMenu(); + // TODO: Insert existing clear queue logic here + } + + private void handleSaveToPlaylistClick() { + Log.d(TAG, "Save to Playlist Clicked! (Placeholder)"); + toggleFabMenu(); + } + + private void handleDownloadAllClick() { + Log.d(TAG, "Download All Clicked! (Placeholder)"); + toggleFabMenu(); + } + + private void handleSaveQueueClick() { + Log.d(TAG, "Save Queue Clicked! (Placeholder)"); + toggleFabMenu(); + } + + private void handleLoadQueueClick() { + Log.d(TAG, "Load Queue Clicked! (Placeholder)"); + toggleFabMenu(); + } + } \ No newline at end of file diff --git a/app/src/main/res/layout/inner_fragment_player_queue.xml b/app/src/main/res/layout/inner_fragment_player_queue.xml index 72a70a22..e343f4f3 100644 --- a/app/src/main/res/layout/inner_fragment_player_queue.xml +++ b/app/src/main/res/layout/inner_fragment_player_queue.xml @@ -1,6 +1,8 @@ - @@ -27,14 +29,82 @@ - - + app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"> + + + + + + + + + + + + + + + + + \ No newline at end of file From 732b6ad09d7eb73ff9d2bea17734e359ac71d759 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Tue, 25 Nov 2025 15:48:48 -0800 Subject: [PATCH 2/6] fix: moved existing functionality to fab buttons, removed queue text/button from top --- .../ui/fragment/PlayerQueueFragment.java | 110 +++++++++--------- .../layout/inner_fragment_player_queue.xml | 10 -- 2 files changed, 52 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java index baecb480..2fa80702 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java @@ -141,8 +141,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { mediaBrowserListenableFuture.addListener(() -> { try { MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); - // initShuffleButton(mediaBrowser); - // initCleanButton(mediaBrowser); } catch (Exception exception) { exception.printStackTrace(); } @@ -181,18 +179,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { fromPosition = viewHolder.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); recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition); @@ -220,46 +206,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { }).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 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() { playerSongQueueAdapter.notifyDataSetChanged(); } @@ -347,14 +293,62 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { private void handleShuffleQueueClick() { Log.d(TAG, "Shuffle Queue Clicked!"); - toggleFabMenu(); - // TODO: Insert existing shuffle logic here + + mediaBrowserListenableFuture.addListener(() -> { + try { + MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get(); + int startPosition = mediaBrowser.getCurrentMediaItemIndex() + 1; + int endPosition = playerSongQueueAdapter.getItems().size() - 1; + + if (startPosition < endPosition) { + ArrayList 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!"); - toggleFabMenu(); - // TODO: Insert existing clear queue logic here + + 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() { diff --git a/app/src/main/res/layout/inner_fragment_player_queue.xml b/app/src/main/res/layout/inner_fragment_player_queue.xml index e343f4f3..1f3eec9f 100644 --- a/app/src/main/res/layout/inner_fragment_player_queue.xml +++ b/app/src/main/res/layout/inner_fragment_player_queue.xml @@ -6,15 +6,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - @@ -23,7 +14,6 @@ android:id="@+id/player_queue_recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:layout_marginTop="40dp" android:paddingTop="8dp" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> From 27f5a47cc962270c73b745b484d1973b0de653c2 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Thu, 27 Nov 2025 08:04:40 -0800 Subject: [PATCH 3/6] feat: save q to playlist, removed save queue button, added style to fab. --- .../ui/fragment/PlayerQueueFragment.java | 30 ++++++++++++------- .../layout/inner_fragment_player_queue.xml | 11 +------ app/src/main/res/values-night/styles.xml | 17 +++++++++++ app/src/main/res/values/styles.xml | 17 +++++++++++ 4 files changed, 55 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java index 2fa80702..809b1a18 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerQueueFragment.java @@ -6,6 +6,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -23,6 +24,7 @@ import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter; +import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; @@ -31,6 +33,7 @@ import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.stream.Collectors; @UnstableApi @@ -46,7 +49,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabSaveToPlaylist; private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabDownloadAll; - private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabSaveQueue; private com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton fabLoadQueue; private boolean isMenuOpen = false; @@ -73,7 +75,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { fabSaveToPlaylist = bind.fabSaveToPlaylist; fabDownloadAll = bind.fabDownloadAll; - fabSaveQueue = bind.fabSaveQueue; fabLoadQueue = bind.fabLoadQueue; fabMenuToggle.setOnClickListener(v -> toggleFabMenu()); @@ -82,7 +83,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { fabSaveToPlaylist.setOnClickListener(v -> handleSaveToPlaylistClick()); fabDownloadAll.setOnClickListener(v -> handleDownloadAllClick()); - fabSaveQueue.setOnClickListener(v -> handleSaveQueueClick()); fabLoadQueue.setOnClickListener(v -> handleLoadQueueClick()); initQueueRecyclerView(); @@ -246,7 +246,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { // CLOSE MENU (Reverse order for visual effect) closeFab(fabSaveToPlaylist, 5); closeFab(fabDownloadAll, 4); - closeFab(fabSaveQueue, 3); closeFab(fabLoadQueue, 2); closeFab(fabClearQueue, 1); closeFab(fabShuffleQueue, 0); @@ -257,7 +256,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { openFab(fabShuffleQueue, 0); openFab(fabClearQueue, 1); openFab(fabLoadQueue, 2); - openFab(fabSaveQueue, 3); openFab(fabDownloadAll, 4); openFab(fabSaveToPlaylist, 5); @@ -352,7 +350,23 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { } private void handleSaveToPlaylistClick() { - Log.d(TAG, "Save to Playlist Clicked! (Placeholder)"); + Log.d(TAG, "Save to Playlist Clicked!"); + + List 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(); } @@ -361,10 +375,6 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback { toggleFabMenu(); } - private void handleSaveQueueClick() { - Log.d(TAG, "Save Queue Clicked! (Placeholder)"); - toggleFabMenu(); - } private void handleLoadQueueClick() { Log.d(TAG, "Load Queue Clicked! (Placeholder)"); diff --git a/app/src/main/res/layout/inner_fragment_player_queue.xml b/app/src/main/res/layout/inner_fragment_player_queue.xml index 1f3eec9f..16fb9b44 100644 --- a/app/src/main/res/layout/inner_fragment_player_queue.xml +++ b/app/src/main/res/layout/inner_fragment_player_queue.xml @@ -35,7 +35,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="8dp" android:visibility="gone" - android:text="Save to Playlist (TODO)" + android:text="Save Queue to Playlist" tools:ignore="HardcodedText" app:icon="@android:drawable/ic_menu_edit" /> @@ -49,15 +49,6 @@ tools:ignore="HardcodedText" app:icon="@android:drawable/stat_sys_download_done" /> - ?attr/colorSurface ?attr/colorSurface none + + @style/FloatingActionButtonStyle + + + + + + + +