From 7380cc276ca06c72106b334ccfcd2ed076f72d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Sun, 10 May 2026 14:03:53 -0400 Subject: [PATCH 1/5] feat: helper classes for cover flow --- .../cappielloantonio/tempo/util/UIUtil.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java index 2184c10a3..597faf533 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java @@ -2,15 +2,22 @@ import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; +import android.view.View; import androidx.core.os.LocaleListCompat; import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSnapHelper; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.RecyclerView; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.R; +import org.jspecify.annotations.NonNull; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -112,4 +119,79 @@ public static String getReadableDate(Date date) { return formatter.format(date); } + public static RecyclerView.ItemDecoration horizontalSpacing(int spacingPx) { + return new HorizontalSpacingItemDecoration(spacingPx); + } + + public static RecyclerView.OnScrollListener scaleOnScroll() { + return new ScaleOnScrollListener(); + } + + private static class HorizontalSpacingItemDecoration extends RecyclerView.ItemDecoration { + private final int spacingPx; + + HorizontalSpacingItemDecoration(int spacingPx) { + this.spacingPx = spacingPx; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, + @NonNull View view, + @NonNull RecyclerView parent, + RecyclerView.@NonNull State state) { + int pos = parent.getChildAdapterPosition(view); + outRect.left = (pos == 0) ? spacingPx : 0; + outRect.right = spacingPx; + } + } + + private static class ScaleOnScrollListener extends RecyclerView.OnScrollListener { + private static final float MAX = 1.0f; + private static final float MIN = 0.70f; + private static final float ELEVATION_FACTOR = 8f; + + @Override + public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) { + final float centerX = rv.getWidth() / 2f; + + for (int i = 0; i < rv.getChildCount(); i++) { + View child = rv.getChildAt(i); + + float childCenter = (child.getLeft() + child.getRight()) / 2f; + float distance = Math.abs(centerX - childCenter); + float scale = MAX - (distance / centerX) * (MAX - MIN); + + child.setScaleX(scale); + child.setScaleY(scale); + child.setElevation(scale * ELEVATION_FACTOR); + } + } + } + + public static void centerAndSnapRecyclerView(@NonNull RecyclerView rv) { + final PagerSnapHelper snapHelper = new PagerSnapHelper(); + snapHelper.attachToRecyclerView(rv); + + rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, + int newState) { + if (newState != RecyclerView.SCROLL_STATE_IDLE) return; + + RecyclerView.LayoutManager lm = recyclerView.getLayoutManager(); + if (!(lm instanceof LinearLayoutManager)) return; + + View snapView = snapHelper.findSnapView(lm); + if (snapView == null) return; + + int[] distance = snapHelper.calculateDistanceToFinalSnap(lm, snapView); + if (distance == null) return; + + if (distance[0] != 0 || distance[1] != 0) { + recyclerView.smoothScrollBy(distance[0], distance[1]); + } + } + }); + } + } From 5311bddf883801249198f8c59b88e5bb3ada1356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Sun, 10 May 2026 14:08:12 -0400 Subject: [PATCH 2/5] feat: add new cover flow adapter and item --- .../tempo/ui/adapter/CoverFlowAdapter.java | 56 +++++++++++++++++++ app/src/main/res/layout/item_cover_flow.xml | 13 +++++ 2 files changed, 69 insertions(+) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java create mode 100644 app/src/main/res/layout/item_cover_flow.xml diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java new file mode 100644 index 000000000..2604692c4 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java @@ -0,0 +1,56 @@ +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 org.jspecify.annotations.NonNull; + +import java.util.List; + +public class CoverFlowAdapter extends RecyclerView.Adapter { + + private final List imageUrls; + private final Context ctx; + private final LayoutInflater inflater; + + public CoverFlowAdapter(Context ctx, List imageUrls) { + this.ctx = ctx; + this.imageUrls = imageUrls; + this.inflater = LayoutInflater.from(ctx); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = inflater.inflate(R.layout.item_cover_flow, parent, false); + return new ViewHolder(view); + } + + @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + String url = imageUrls.get(position); + Glide.with(ctx) + .load(url) + .placeholder(R.drawable.ui_empty_list) + .into(holder.cover); + } + + @Override public int getItemCount() { + return imageUrls.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + final ImageView cover; + ViewHolder(View itemView) { + super(itemView); + cover = itemView.findViewById(R.id.imgCover); + } + } +} diff --git a/app/src/main/res/layout/item_cover_flow.xml b/app/src/main/res/layout/item_cover_flow.xml new file mode 100644 index 000000000..80e3656d3 --- /dev/null +++ b/app/src/main/res/layout/item_cover_flow.xml @@ -0,0 +1,13 @@ + + + + + From b2f54abb1485e9cdc3b2946fbf33e2e889061eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Sun, 10 May 2026 14:09:12 -0400 Subject: [PATCH 3/5] feat: add draft cover flow to player --- .../ui/fragment/PlayerControllerFragment.java | 35 ++++++++++++++++++- ...nner_fragment_player_controller_layout.xml | 15 +++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index 1d4db854e..3970175ce 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -37,6 +37,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; @@ -48,10 +50,11 @@ import com.cappielloantonio.tempo.service.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; -import androidx.core.content.ContextCompat; + import androidx.core.widget.ImageViewCompat; import android.content.res.ColorStateList; import com.cappielloantonio.tempo.ui.dialog.RatingDialog; @@ -61,6 +64,7 @@ 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.PlayerBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.RatingViewModel; import com.google.android.material.chip.Chip; @@ -70,6 +74,7 @@ import com.google.common.util.concurrent.MoreExecutors; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -96,6 +101,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; @@ -120,6 +127,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, init(); initQuickActionView(); + initCoverFlow(); initCoverLyricsSlideView(); initMediaListenable(); initMediaLabelButton(); @@ -163,6 +171,7 @@ private void init() { playerQuickActionView = bind.getRoot().findViewById(R.id.player_quick_action_view); playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_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); @@ -188,6 +197,30 @@ private void initQuickActionView() { }); } + private void initCoverFlow() { + + List covers = 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", + "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" + ); + + playerCoverFlow.setAdapter(new CoverFlowAdapter(requireContext(), covers)); + playerCoverFlow.setLayoutManager( + new LinearLayoutManager(requireContext(), + LinearLayoutManager.HORIZONTAL, + false)); + + playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32)); + playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll()); + UIUtil.centerAndSnapRecyclerView(playerCoverFlow); + } + + + private void initializeBrowser() { mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); } diff --git a/app/src/main/res/layout/inner_fragment_player_controller_layout.xml b/app/src/main/res/layout/inner_fragment_player_controller_layout.xml index 46195d485..5ef204218 100644 --- a/app/src/main/res/layout/inner_fragment_player_controller_layout.xml +++ b/app/src/main/res/layout/inner_fragment_player_controller_layout.xml @@ -102,7 +102,20 @@ app:layout_constraintBottom_toTopOf="@id/guideline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" /> + app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" + android:visibility="gone"/> + + Date: Sun, 10 May 2026 14:19:18 -0400 Subject: [PATCH 4/5] feat: swap cover flow and view pager with lyrics button --- .../tempo/ui/fragment/PlayerControllerFragment.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index f0c124e10..e17ed6742 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -200,9 +200,13 @@ 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); } }); From 5ff9ff5af6256dd93671b1ca0efb4fc0443b2919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Thu, 21 May 2026 01:31:01 -0400 Subject: [PATCH 5/5] feat: heavily LLM-assisetd scaffolding of MVVM arch pattern This is exactly the same as before, but with an MVVM architecture patter with the following flow: Fragment -> RecyclerView -> Adapter -> ViewModel -> Repository -> Glide (data fetch). Here comes the fun part. setupCoverFlow (ran in the fragment when the UI is built), must be fed with the coverArt id recollected in the repository (all the way down on the MVVM chain). I stopped here because I have a confusion on the Repository -> Glide MVVM chain. The image fetch is done on the fragment, so the repository must read the queue itself to get the coverArt id? --- .../factory/CoverFlowViewModelFactory.java | 28 +++++ .../cappielloantonio/tempo/model/Cover.java | 25 +++++ .../tempo/repository/CoverRepository.java | 20 ++++ .../MediaBrowserCoverRepository.java | 53 +++++++++ .../tempo/ui/adapter/CoverFlowAdapter.java | 69 ++++++++---- .../ui/fragment/PlayerControllerFragment.java | 102 ++++++++++++++---- .../tempo/viewmodel/CoverFlowViewModel.java | 62 +++++++++++ app/src/main/res/layout/item_cover_flow.xml | 2 +- 8 files changed, 314 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/factory/CoverFlowViewModelFactory.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/model/Cover.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/repository/CoverRepository.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/repository/MediaBrowserCoverRepository.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/viewmodel/CoverFlowViewModel.java diff --git a/app/src/main/java/com/cappielloantonio/tempo/factory/CoverFlowViewModelFactory.java b/app/src/main/java/com/cappielloantonio/tempo/factory/CoverFlowViewModelFactory.java new file mode 100644 index 000000000..d0bf2a992 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/factory/CoverFlowViewModelFactory.java @@ -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 create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(CoverFlowViewModel.class)) { + //noinspection unchecked + return (T) new CoverFlowViewModel(repository); + } + throw new IllegalArgumentException("Unknown ViewModel class"); + } +} + diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/Cover.java b/app/src/main/java/com/cappielloantonio/tempo/model/Cover.java new file mode 100644 index 000000000..d7a50ccef --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/model/Cover.java @@ -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; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/CoverRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/CoverRepository.java new file mode 100644 index 000000000..7538f21b6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/CoverRepository.java @@ -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 getCovers() throws Exception; +} + diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/MediaBrowserCoverRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/MediaBrowserCoverRepository.java new file mode 100644 index 000000000..a5f95cb77 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/MediaBrowserCoverRepository.java @@ -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 mediaBrowserFuture; + + public MediaBrowserCoverRepository(@NonNull ListenableFuture mediaBrowserFuture) { + this.mediaBrowserFuture = mediaBrowserFuture; + } + + @Override + @WorkerThread + public List 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 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 covers = new ArrayList<>(); + for (String url : urls) { + covers.add(new Cover(url, null)); // coverArtId can be filled later if needed + } + return covers; + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java index 2604692c4..2f42a4df2 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java @@ -10,47 +10,70 @@ 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 { +public class CoverFlowAdapter extends RecyclerView.Adapter { - private final List imageUrls; - private final Context ctx; - private final LayoutInflater inflater; + /** 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 covers; + private final OnCoverBoundListener boundListener; - public CoverFlowAdapter(Context ctx, List imageUrls) { - this.ctx = ctx; - this.imageUrls = imageUrls; - this.inflater = LayoutInflater.from(ctx); + public CoverFlowAdapter(@NonNull Context context, + @NonNull List covers, + @NonNull OnCoverBoundListener boundListener) { + this.context = context; + this.covers = covers; + this.boundListener = boundListener; } @NonNull @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = inflater.inflate(R.layout.item_cover_flow, parent, false); - return new ViewHolder(view); + 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 ViewHolder holder, int position) { - String url = imageUrls.get(position); - Glide.with(ctx) - .load(url) - .placeholder(R.drawable.ui_empty_list) - .into(holder.cover); + @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 imageUrls.size(); + @Override + public int getItemCount() { + return covers.size(); } - static class ViewHolder extends RecyclerView.ViewHolder { - final ImageView cover; - ViewHolder(View itemView) { + /** ----------------------------------------------------------------- + * ViewHolder – each item holds a single ImageView. + * ----------------------------------------------------------------- */ + class CoverViewHolder extends RecyclerView.ViewHolder { + final ImageView coverImage; + + CoverViewHolder(@NonNull View itemView) { super(itemView); - cover = itemView.findViewById(R.id.imgCover); + 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); } } } + diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java index e17ed6742..03900b26a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java @@ -24,6 +24,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -47,6 +48,11 @@ 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.service.EqualizerManager; import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.ui.activity.MainActivity; @@ -65,6 +71,7 @@ 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; @@ -128,7 +135,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, init(); initQuickActionView(); - initCoverFlow(); initCoverLyricsSlideView(); initMediaListenable(); initMediaLabelButton(); @@ -141,6 +147,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 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(); @@ -212,28 +290,6 @@ private void initQuickActionView() { }); } - private void initCoverFlow() { - - List covers = 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", - "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" - ); - - playerCoverFlow.setAdapter(new CoverFlowAdapter(requireContext(), covers)); - playerCoverFlow.setLayoutManager( - new LinearLayoutManager(requireContext(), - LinearLayoutManager.HORIZONTAL, - false)); - - playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32)); - playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll()); - UIUtil.centerAndSnapRecyclerView(playerCoverFlow); - } - private void initializeBrowser() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/CoverFlowViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/CoverFlowViewModel.java new file mode 100644 index 000000000..1b8721712 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/CoverFlowViewModel.java @@ -0,0 +1,62 @@ +package com.cappielloantonio.tempo.viewmodel; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.cappielloantonio.tempo.model.Cover; +import com.cappielloantonio.tempo.repository.CoverRepository; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class CoverFlowViewModel extends ViewModel { + + private final CoverRepository repository; + private final MutableLiveData> coversLiveData = new MutableLiveData<>(); + + public CoverFlowViewModel(@NonNull CoverRepository repository) { + this.repository = repository; + loadCovers(); + } + + public LiveData> getCovers() { + return coversLiveData; + } + + private final Executor ioExecutor = Executors.newSingleThreadExecutor(); + + private void loadCovers() { + ioExecutor.execute(() -> { + try { + // Disabled, correct implementation not done just yet + // List list = repository.getCovers(); // Disabled as it ain't implemented + + /* Mock of data fecthing */ + List 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 coversList = new ArrayList<>(); + for (String url : urls) { + coversList.add(new Cover(url, null)); // coverArtId can be filled later if needed + } + + // Normal logic end + coversLiveData.postValue(coversList); + } catch (Exception e) { + coversLiveData.postValue(Collections.emptyList()); + } + + }); + } +} + diff --git a/app/src/main/res/layout/item_cover_flow.xml b/app/src/main/res/layout/item_cover_flow.xml index 80e3656d3..13bb927dc 100644 --- a/app/src/main/res/layout/item_cover_flow.xml +++ b/app/src/main/res/layout/item_cover_flow.xml @@ -4,7 +4,7 @@ android:layout_height="match_parent">