Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cappielloantonio.tempo.factory;

import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;

import com.cappielloantonio.tempo.repository.CoverRepository;
import com.cappielloantonio.tempo.viewmodel.CoverFlowViewModel;

public class CoverFlowViewModelFactory implements ViewModelProvider.Factory {

private final CoverRepository repository;

public CoverFlowViewModelFactory(@NonNull CoverRepository repository) {
this.repository = repository;
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(CoverFlowViewModel.class)) {
//noinspection unchecked
return (T) new CoverFlowViewModel(repository);
}
throw new IllegalArgumentException("Unknown ViewModel class");
}
}

25 changes: 25 additions & 0 deletions app/src/main/java/com/cappielloantonio/tempo/model/Cover.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.cappielloantonio.tempo.model;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class Cover {
private final String url; // image to show in the flow
private final String coverArtId; // id used by the CustomGlideRequest (may be null)

public Cover(@NonNull String url, @Nullable String coverArtId) {
this.url = url;
this.coverArtId = coverArtId;
}

@NonNull
public String getUrl() {
return url;
}

/** Returns the id that the CustomGlideRequest needs – can be null. */
@Nullable
public String getCoverArtId() {
return coverArtId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cappielloantonio.tempo.repository;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.media3.common.MediaMetadata;
import androidx.media3.session.MediaBrowser;

import com.cappielloantonio.tempo.model.Cover;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public interface CoverRepository {
/** Returns a list of covers. Call should be made off the UI thread. */
@WorkerThread
List<Cover> getCovers() throws Exception;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.cappielloantonio.tempo.repository;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.media3.common.MediaMetadata;
import androidx.media3.session.MediaBrowser;

import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Cover;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; /** Example implementation that extracts data from a MediaBrowser */
public class MediaBrowserCoverRepository implements CoverRepository {

private final ListenableFuture<MediaBrowser> mediaBrowserFuture;

public MediaBrowserCoverRepository(@NonNull ListenableFuture<MediaBrowser> mediaBrowserFuture) {
this.mediaBrowserFuture = mediaBrowserFuture;
}

@Override
@WorkerThread
public List<Cover> getCovers() throws Exception {
MediaBrowser mediaBrowser = mediaBrowserFuture.get(); // blocks only inside a background thread
MediaMetadata metadata = mediaBrowser.getMediaMetadata();

// Inject this here, somehow, since it grabs the covertArtId
/*`
CustomGlideRequest.Builder
.from(requireContext(), metadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
.build()
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);

*/
// -----------------------------------------------------------------
// Replace the below with the real extraction logic from metadata.
// For demonstration we just return the three dog‑image URLs.
// -----------------------------------------------------------------
List<String> urls = Arrays.asList(
"https://images.dog.ceo/breeds/affenpinscher/n02110627_11858.jpg",
"https://images.dog.ceo/breeds/hound-english/n02089973_811.jpg",
"https://images.dog.ceo/breeds/shiba/shiba-14.jpg"
);

List<Cover> covers = new ArrayList<>();
for (String url : urls) {
covers.add(new Cover(url, null)); // coverArtId can be filled later if needed
}
return covers;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.cappielloantonio.tempo.ui.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import androidx.recyclerview.widget.RecyclerView;

import com.bumptech.glide.Glide;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.model.Cover;

import org.jspecify.annotations.NonNull;

import java.util.List;

public class CoverFlowAdapter extends RecyclerView.Adapter<CoverFlowAdapter.CoverViewHolder> {

/** Callback that is invoked for every bound item – useful for the extra Glide request. */
public interface OnCoverBoundListener {
void onCoverBound(@NonNull Cover cover, @NonNull ImageView coverImage);
}

private final Context context;
private final List<Cover> covers;
private final OnCoverBoundListener boundListener;

public CoverFlowAdapter(@NonNull Context context,
@NonNull List<Cover> covers,
@NonNull OnCoverBoundListener boundListener) {
this.context = context;
this.covers = covers;
this.boundListener = boundListener;
}

@NonNull
@Override
public CoverViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// Inflate a simple item layout that contains only an ImageView.
View view = LayoutInflater.from(context).inflate(R.layout.item_cover_flow, parent, false);
return new CoverViewHolder(view);
}

@Override
public void onBindViewHolder(@NonNull CoverViewHolder holder, int position) {
Cover cover = covers.get(position);
holder.bind(cover);
// Let the fragment (or caller) run the extra CustomGlideRequest if it needs the id.
boundListener.onCoverBound(cover, holder.coverImage);
}

@Override
public int getItemCount() {
return covers.size();
}

/** -----------------------------------------------------------------
* ViewHolder – each item holds a single ImageView.
* ----------------------------------------------------------------- */
class CoverViewHolder extends RecyclerView.ViewHolder {
final ImageView coverImage;

CoverViewHolder(@NonNull View itemView) {
super(itemView);
coverImage = itemView.findViewById(R.id.item_cover_image);
}

void bind(@NonNull Cover cover) {
// Normal image loading (the flow thumbnails)
Glide.with(context)
.load(cover.getUrl())
.centerCrop()
.into(coverImage);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.Fragment;
Expand All @@ -38,6 +39,8 @@
import androidx.navigation.NavController;
import androidx.navigation.NavOptions;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.ChangeBounds;
import androidx.transition.Slide;
import androidx.transition.TransitionManager;
Expand All @@ -46,9 +49,15 @@

import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
import com.cappielloantonio.tempo.factory.CoverFlowViewModelFactory;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Cover;
import com.cappielloantonio.tempo.repository.CoverRepository;
import com.cappielloantonio.tempo.repository.MediaBrowserCoverRepository;
import com.cappielloantonio.tempo.equalizer.EqualizerManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.CoverFlowAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaybackSpeedDialog;
import com.cappielloantonio.tempo.ui.dialog.SleepTimerDialog;
import com.cappielloantonio.tempo.util.SleepTimerManager;
Expand All @@ -62,6 +71,8 @@
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.CoverFlowViewModel;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
import com.google.android.material.chip.Chip;
Expand All @@ -73,6 +84,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

Expand Down Expand Up @@ -101,6 +113,8 @@ public class PlayerControllerFragment extends Fragment {
private LinearLayout sleepTimerContainer;
private ImageButton sleepTimerButton;
private android.widget.TextView sleepTimerLabel;

private RecyclerView playerCoverFlow;
private ChipGroup assetLinkChipGroup;
private Chip playerSongLinkChip;
private Chip playerAlbumLinkChip;
Expand Down Expand Up @@ -138,6 +152,78 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
return view;
}

private CoverFlowViewModel viewModel;

// public CoverFlowFragment() {
// super(R.layout.inner_fragment_player_controller_layout); // your layout that contains the RecyclerView
// }

@Override
public void onViewCreated(@NonNull View root, @Nullable Bundle savedInstanceState) {
super.onViewCreated(root, savedInstanceState);

// -----------------------------------------------------------------
// 1️⃣ Find the RecyclerView (the id changed to player_cover_flow)
// -----------------------------------------------------------------
playerCoverFlow = root.findViewById(R.id.player_cover_flow);

// -----------------------------------------------------------------
// 2️⃣ Set up the ViewModel (repository injection omitted for brevity)
// -----------------------------------------------------------------
// CoverRepository repository = new MediaBrowserCoverRepository(mediaBrowserListenableFuture);
// viewModel = new ViewModelProvider(this,
// new CoverFlowViewModelFactory(repository))
// .get(CoverFlowViewModel.class);
CoverRepository repository = new MediaBrowserCoverRepository(mediaBrowserListenableFuture);
CoverFlowViewModelFactory factory = new CoverFlowViewModelFactory(repository);

viewModel = new ViewModelProvider(this, factory)
.get(CoverFlowViewModel.class);

// -----------------------------------------------------------------
// 3️⃣ Observe the LiveData and build the UI when data arrives
// -----------------------------------------------------------------
viewModel.getCovers().observe(getViewLifecycleOwner(), this::setupCoverFlow);
}

/** Called once the list of covers is available */
private void setupCoverFlow(@NonNull List<Cover> covers) {
if (covers.isEmpty()) return; // optionally show an empty‑state view

// -----------------------------------------------------------------
// 4️⃣ Create the adapter – the lambda receives the Cover object
// and the ImageView that just got bound.
// -----------------------------------------------------------------
CoverFlowAdapter adapter = new CoverFlowAdapter(
requireContext(),
covers,
(cover, imageView) -> {
// Run the *extra* Glide request only when we have a valid id.
String coverArtId = cover.getCoverArtId();
if (coverArtId != null) {
CustomGlideRequest.Builder
.from(requireContext(),
coverArtId,
CustomGlideRequest.ResourceType.Song)
.build()
.into(imageView); // load into the same thumbnail ImageView
}
});

// -----------------------------------------------------------------
// 5️⃣ RecyclerView basics (same as your original code)
// -----------------------------------------------------------------
playerCoverFlow.setAdapter(adapter);
playerCoverFlow.setLayoutManager(
new LinearLayoutManager(requireContext(),
LinearLayoutManager.HORIZONTAL,
false));

playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
}

@Override
public void onStart() {
super.onStart();
Expand Down Expand Up @@ -171,6 +257,7 @@ private void init() {
playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button);
playerOpenLyricsButton = bind.getRoot().findViewById(R.id.player_open_lyrics_button);
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
playerCoverFlow = bind.getRoot().findViewById(R.id.player_cover_flow);
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
Expand Down Expand Up @@ -217,14 +304,20 @@ private void initQuickActionView() {
playerOpenLyricsButton.setOnClickListener(view -> {
int currentItem = playerMediaCoverViewPager.getCurrentItem();
if (currentItem == 0) {
playerMediaCoverViewPager.setCurrentItem(1, true);
playerMediaCoverViewPager.setCurrentItem(1, false);
playerCoverFlow.setVisibility(View.GONE);
playerMediaCoverViewPager.setVisibility(View.VISIBLE);
} else if (currentItem == 1) {
playerMediaCoverViewPager.setCurrentItem(0, true);;
playerMediaCoverViewPager.setCurrentItem(0, false);
playerCoverFlow.setVisibility(View.VISIBLE);
playerMediaCoverViewPager.setVisibility(View.GONE);
}

});
}



private void initializeBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
Expand Down
Loading