From 94b02a0aac9817aee1059aabff568abe987a0123 Mon Sep 17 00:00:00 2001 From: beeetfarmer <176325048+beeetfarmer@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:28:13 +0530 Subject: [PATCH 1/8] feat: enhance artist detail page with categorized album carousels, circular similar artists, and improved top songs display --- .../subsonic/models/ArtistWithAlbumsID3.kt | 2 + .../ui/adapter/AlbumCarouselAdapter.java | 86 +++++++++ .../ui/adapter/ArtistCarouselAdapter.java | 80 ++++++++ .../ui/fragment/AlbumListPageFragment.java | 4 + .../tempo/ui/fragment/ArtistPageFragment.java | 108 ++++++++--- .../cappielloantonio/tempo/util/Constants.kt | 2 + .../viewmodel/AlbumListPageViewModel.java | 4 + .../tempo/viewmodel/ArtistPageViewModel.java | 63 +++++++ .../main/res/layout/fragment_artist_page.xml | 173 ++++++++++++++++-- .../main/res/layout/item_album_carousel.xml | 45 +++++ .../main/res/layout/item_artist_carousel.xml | 33 ++++ app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 4 + 13 files changed, 568 insertions(+), 41 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java create mode 100644 app/src/main/res/layout/item_album_carousel.xml create mode 100644 app/src/main/res/layout/item_artist_carousel.xml diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt index 2e21e1115..8818aeeb9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/models/ArtistWithAlbumsID3.kt @@ -10,4 +10,6 @@ import kotlinx.parcelize.Parcelize class ArtistWithAlbumsID3( @SerializedName("album") var albums: List? = null, + @SerializedName("appearsOn") + var appearsOn: List? = null, ) : ArtistID3(), Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java new file mode 100644 index 000000000..5e06d853b --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/AlbumCarouselAdapter.java @@ -0,0 +1,86 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemAlbumCarouselBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.AlbumID3; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +public class AlbumCarouselAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + private List albums; + private boolean showArtist; + + public AlbumCarouselAdapter(ClickCallback click, boolean showArtist) { + this.click = click; + this.albums = Collections.emptyList(); + this.showArtist = showArtist; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemAlbumCarouselBinding view = ItemAlbumCarouselBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + AlbumID3 album = albums.get(position); + + holder.item.albumNameLabel.setText(album.getName()); + holder.item.artistNameLabel.setText(album.getArtist()); + holder.item.artistNameLabel.setVisibility(showArtist ? View.VISIBLE : View.GONE); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), album.getCoverArtId(), CustomGlideRequest.ResourceType.Album) + .build() + .into(holder.item.albumCoverImageView); + } + + @Override + public int getItemCount() { + return albums.size(); + } + + public void setItems(List albums) { + this.albums = albums; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemAlbumCarouselBinding item; + + ViewHolder(ItemAlbumCarouselBinding item) { + super(item.getRoot()); + this.item = item; + + itemView.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + click.onAlbumClick(bundle); + }); + + itemView.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ALBUM_OBJECT, albums.get(getBindingAdapterPosition())); + click.onAlbumLongClick(bundle); + return true; + }); + + item.albumNameLabel.setSelected(true); + item.artistNameLabel.setSelected(true); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java new file mode 100644 index 000000000..802725ba7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/ArtistCarouselAdapter.java @@ -0,0 +1,80 @@ +package com.cappielloantonio.tempo.ui.adapter; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.cappielloantonio.tempo.databinding.ItemArtistCarouselBinding; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.interfaces.ClickCallback; +import com.cappielloantonio.tempo.subsonic.models.ArtistID3; +import com.cappielloantonio.tempo.util.Constants; + +import java.util.Collections; +import java.util.List; + +public class ArtistCarouselAdapter extends RecyclerView.Adapter { + private final ClickCallback click; + private List artists; + + public ArtistCarouselAdapter(ClickCallback click) { + this.click = click; + this.artists = Collections.emptyList(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ItemArtistCarouselBinding view = ItemArtistCarouselBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ArtistID3 artist = artists.get(position); + + holder.item.artistNameLabel.setText(artist.getName()); + + CustomGlideRequest.Builder + .from(holder.itemView.getContext(), artist.getCoverArtId(), CustomGlideRequest.ResourceType.Artist) + .build() + .into(holder.item.artistCoverImageView); + } + + @Override + public int getItemCount() { + return artists.size(); + } + + public void setItems(List artists) { + this.artists = artists; + notifyDataSetChanged(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ItemArtistCarouselBinding item; + + ViewHolder(ItemArtistCarouselBinding item) { + super(item.getRoot()); + this.item = item; + + itemView.setOnClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + click.onArtistClick(bundle); + }); + + itemView.setOnLongClickListener(v -> { + Bundle bundle = new Bundle(); + bundle.putParcelable(Constants.ARTIST_OBJECT, artists.get(getBindingAdapterPosition())); + click.onArtistLongClick(bundle); + return true; + }); + + item.artistNameLabel.setSelected(true); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java index 38dc9b10b..574825bc9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/AlbumListPageFragment.java @@ -93,6 +93,10 @@ private void init() { albumListPageViewModel.artist = requireArguments().getParcelable(Constants.ARTIST_OBJECT); albumListPageViewModel.title = Constants.ALBUM_FROM_ARTIST; bind.pageTitleLabel.setText(albumListPageViewModel.artist.getName()); + } else if (requireArguments().getParcelableArrayList(Constants.ALBUMS_OBJECT) != null) { + albumListPageViewModel.albums = requireArguments().getParcelableArrayList(Constants.ALBUMS_OBJECT); + albumListPageViewModel.title = requireArguments().getString(Constants.ALBUM_LIST_TITLE, ""); + bind.pageTitleLabel.setText(albumListPageViewModel.title); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index e995d209f..63ea91e88 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -37,7 +37,8 @@ import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; -import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter; +import com.cappielloantonio.tempo.ui.adapter.AlbumCarouselAdapter; +import com.cappielloantonio.tempo.ui.adapter.ArtistCarouselAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; @@ -59,8 +60,11 @@ public class ArtistPageFragment extends Fragment implements ClickCallback { private PlaybackViewModel playbackViewModel; private SongHorizontalAdapter songHorizontalAdapter; - private AlbumCatalogueAdapter albumCatalogueAdapter; - private ArtistCatalogueAdapter artistCatalogueAdapter; + private AlbumCarouselAdapter mainAlbumAdapter; + private AlbumCarouselAdapter epAdapter; + private AlbumCarouselAdapter singleAdapter; + private AlbumCarouselAdapter appearsOnAdapter; + private ArtistCarouselAdapter similarArtistAdapter; private ListenableFuture mediaBrowserListenableFuture; @@ -84,7 +88,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, initArtistInfo(); initPlayButtons(); initTopSongsView(); - initAlbumsView(); + initCategorizedAlbumsView(); initSimilarArtistsView(); return view; @@ -118,6 +122,7 @@ public void onDestroyView() { private void init(View view) { artistPageViewModel.setArtist(requireArguments().getParcelable(Constants.ARTIST_OBJECT)); + artistPageViewModel.fetchCategorizedAlbums(getViewLifecycleOwner()); bind.mostStreamedSongTextViewClickable.setOnClickListener(v -> { Bundle bundle = new Bundle(); @@ -275,40 +280,95 @@ private void initTopSongsView() { if (songs == null) { if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE); } else { - if (bind != null) + if (bind != null) { bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); - songHorizontalAdapter.setItems(songs); + bind.mostStreamedSongTextViewClickable.setVisibility(songs.size() > 10 ? View.VISIBLE : View.GONE); + } + songHorizontalAdapter.setItems(songs.stream().limit(10).collect(java.util.stream.Collectors.toList())); reapplyPlayback(); } }); } - private void initAlbumsView() { - bind.albumsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount)); - bind.albumsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false)); - bind.albumsRecyclerView.setHasFixedSize(true); + private void initCategorizedAlbumsView() { + // Main Albums + bind.mainAlbumsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.mainAlbumsRecyclerView.setHasFixedSize(true); + mainAlbumAdapter = new AlbumCarouselAdapter(this, false); + bind.mainAlbumsRecyclerView.setAdapter(mainAlbumAdapter); + artistPageViewModel.getMainAlbums().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageMainAlbumsSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.mainAlbumsSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + mainAlbumAdapter.setItems(albums); + bind.mainAlbumsSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_album_section), albums)); + } + } + }); + + // EPs + bind.epsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.epsRecyclerView.setHasFixedSize(true); + epAdapter = new AlbumCarouselAdapter(this, false); + bind.epsRecyclerView.setAdapter(epAdapter); + artistPageViewModel.getEPs().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageEpsSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.epsSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + epAdapter.setItems(albums); + bind.epsSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_ep_section), albums)); + } + } + }); - albumCatalogueAdapter = new AlbumCatalogueAdapter(this, false); - bind.albumsRecyclerView.setAdapter(albumCatalogueAdapter); + // Singles + bind.singlesRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.singlesRecyclerView.setHasFixedSize(true); + singleAdapter = new AlbumCarouselAdapter(this, false); + bind.singlesRecyclerView.setAdapter(singleAdapter); + artistPageViewModel.getSingles().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageSinglesSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.singlesSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + singleAdapter.setItems(albums); + bind.singlesSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_single_section), albums)); + } + } + }); - artistPageViewModel.getAlbumList().observe(getViewLifecycleOwner(), albums -> { - if (albums == null) { - if (bind != null) bind.artistPageAlbumsSector.setVisibility(View.GONE); - } else { - if (bind != null) - bind.artistPageAlbumsSector.setVisibility(!albums.isEmpty() ? View.VISIBLE : View.GONE); - albumCatalogueAdapter.setItems(albums); + // Appears On + bind.appearsOnRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); + bind.appearsOnRecyclerView.setHasFixedSize(true); + appearsOnAdapter = new AlbumCarouselAdapter(this, true); // Show artist name for Appears On + bind.appearsOnRecyclerView.setAdapter(appearsOnAdapter); + artistPageViewModel.getAppearsOn().observe(getViewLifecycleOwner(), albums -> { + if (bind != null) { + bind.artistPageAppearsOnSector.setVisibility(albums != null && !albums.isEmpty() ? View.VISIBLE : View.GONE); + if (albums != null) { + bind.appearsOnSeeAllTextView.setVisibility(albums.size() > 5 ? View.VISIBLE : View.GONE); + appearsOnAdapter.setItems(albums); + bind.appearsOnSeeAllTextView.setOnClickListener(v -> navigateToAlbumList(getString(R.string.artist_page_title_appears_on_section), albums)); + } } }); } + private void navigateToAlbumList(String title, List albums) { + Bundle bundle = new Bundle(); + bundle.putString(Constants.ALBUM_LIST_TITLE, title); + bundle.putParcelableArrayList(Constants.ALBUMS_OBJECT, new ArrayList<>(albums)); + Navigation.findNavController(requireView()).navigate(R.id.albumListPageFragment, bundle); + } + private void initSimilarArtistsView() { - bind.similarArtistsRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), spanCount)); - bind.similarArtistsRecyclerView.addItemDecoration(new GridItemDecoration(spanCount, 20, false)); + bind.similarArtistsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.similarArtistsRecyclerView.setHasFixedSize(true); - artistCatalogueAdapter = new ArtistCatalogueAdapter(this); - bind.similarArtistsRecyclerView.setAdapter(artistCatalogueAdapter); + similarArtistAdapter = new ArtistCarouselAdapter(this); + bind.similarArtistsRecyclerView.setAdapter(similarArtistAdapter); artistPageViewModel.getArtistInfo(artistPageViewModel.getArtist().getId()).observe(getViewLifecycleOwner(), artist -> { if (artist == null) { @@ -323,7 +383,7 @@ private void initSimilarArtistsView() { artists.addAll(artist.getSimilarArtists()); } - artistCatalogueAdapter.setItems(artists); + similarArtistAdapter.setItems(artists); } }); diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index 7d2224edf..baa8dbf62 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -18,6 +18,8 @@ object Constants { const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT" const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT" const val MUSIC_DIRECTORY_ID = "MUSIC_DIRECTORY_ID" + const val ALBUMS_OBJECT = "ALBUMS_OBJECT" + const val ALBUM_LIST_TITLE = "ALBUM_LIST_TITLE" const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED" const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED" diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java index 8e88cec85..4d2c7c329 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/AlbumListPageViewModel.java @@ -23,6 +23,7 @@ public class AlbumListPageViewModel extends AndroidViewModel { public String title; public ArtistID3 artist; + public List albums; private MutableLiveData> albumList; @@ -34,6 +35,9 @@ public AlbumListPageViewModel(@NonNull Application application) { } public LiveData> getAlbumList(LifecycleOwner owner) { + if (albums != null) { + return new MutableLiveData<>(albums); + } albumList = new MutableLiveData<>(new ArrayList<>()); switch (title) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java index f2b7d9e6a..affb6cc75 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -8,6 +8,7 @@ import androidx.annotation.OptIn; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.model.Download; @@ -24,6 +25,7 @@ import com.cappielloantonio.tempo.util.NetworkUtil; import com.cappielloantonio.tempo.util.Preferences; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -35,6 +37,11 @@ public class ArtistPageViewModel extends AndroidViewModel { private ArtistID3 artist; + private final MutableLiveData> singles = new MutableLiveData<>(); + private final MutableLiveData> eps = new MutableLiveData<>(); + private final MutableLiveData> mainAlbums = new MutableLiveData<>(); + private final MutableLiveData> appearsOn = new MutableLiveData<>(); + public ArtistPageViewModel(@NonNull Application application) { super(application); @@ -43,6 +50,62 @@ public ArtistPageViewModel(@NonNull Application application) { favoriteRepository = new FavoriteRepository(); } + public void fetchCategorizedAlbums(androidx.lifecycle.LifecycleOwner owner) { + artistRepository.getArtist(artist.getId()).observe(owner, artistWithAlbums -> { + if (artistWithAlbums != null && artistWithAlbums instanceof com.cappielloantonio.tempo.subsonic.models.ArtistWithAlbumsID3) { + com.cappielloantonio.tempo.subsonic.models.ArtistWithAlbumsID3 fullArtist = (com.cappielloantonio.tempo.subsonic.models.ArtistWithAlbumsID3) artistWithAlbums; + + List allAlbums = fullArtist.getAlbums(); + if (allAlbums != null) { + allAlbums.sort(Comparator.comparing(AlbumID3::getYear).reversed()); + + mainAlbums.setValue(allAlbums.stream() + .filter(a -> isType(a, "album")) + .collect(Collectors.toList())); + + singles.setValue(allAlbums.stream() + .filter(a -> isType(a, "single")) + .collect(Collectors.toList())); + + eps.setValue(allAlbums.stream() + .filter(a -> isType(a, "ep")) + .collect(Collectors.toList())); + } + + List appearsOnList = fullArtist.getAppearsOn(); + if (appearsOnList != null) { + appearsOnList.sort(Comparator.comparing(AlbumID3::getYear).reversed()); + appearsOn.setValue(appearsOnList); + } else { + appearsOn.setValue(new java.util.ArrayList<>()); + } + } + }); + } + + private boolean isType(AlbumID3 album, String targetType) { + if (album.getReleaseTypes() != null && !album.getReleaseTypes().isEmpty()) { + return album.getReleaseTypes().contains(targetType); + } + // Fallback to song count if releaseTypes is not available + int songCount = album.getSongCount() != null ? album.getSongCount() : 0; + switch (targetType) { + case "single": + return songCount >= 1 && songCount <= 2; + case "ep": + return songCount >= 3 && songCount <= 7; + case "album": + return songCount >= 8; + default: + return false; + } + } + + public LiveData> getSingles() { return singles; } + public LiveData> getEPs() { return eps; } + public LiveData> getMainAlbums() { return mainAlbums; } + public LiveData> getAppearsOn() { return appearsOn; } + public LiveData> getAlbumList() { return albumRepository.getArtistAlbums(artist.getId()); } diff --git a/app/src/main/res/layout/fragment_artist_page.xml b/app/src/main/res/layout/fragment_artist_page.xml index 6712dd2e9..b2ebc4fe2 100644 --- a/app/src/main/res/layout/fragment_artist_page.xml +++ b/app/src/main/res/layout/fragment_artist_page.xml @@ -54,7 +54,7 @@ android:clipToPadding="false" android:orientation="vertical" android:paddingTop="18dp" - android:paddingBottom="@dimen/global_padding_bottom"> + android:paddingBottom="120dp"> - + + + + + + + + android:paddingEnd="16dp" /> + + + + + + + + + + + android:paddingEnd="16dp" /> + + + + + + + + + + + + + + + + + + + + + + + + + android:paddingEnd="16dp" /> diff --git a/app/src/main/res/layout/item_album_carousel.xml b/app/src/main/res/layout/item_album_carousel.xml new file mode 100644 index 000000000..3f202989f --- /dev/null +++ b/app/src/main/res/layout/item_album_carousel.xml @@ -0,0 +1,45 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_artist_carousel.xml b/app/src/main/res/layout/item_artist_carousel.xml new file mode 100644 index 000000000..336ede173 --- /dev/null +++ b/app/src/main/res/layout/item_artist_carousel.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 88c11fb77..774a4ac20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,9 +45,12 @@ Switch layout More like this Albums + EPs + Singles + Appears On More Biography - Most Streamed Songs + Top Songs See all Ignore Don\'t ask again diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index aa08c1514..b598c2541 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -68,4 +68,8 @@ ?attr/colorErrorContainer ?attr/colorOnErrorContainer + + \ No newline at end of file From 82f9679da7793069d1dfb295f8dd95fd65072dfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Sat, 21 Feb 2026 01:14:08 -0300 Subject: [PATCH 2/8] feat: enhance navigation --- .../tempo/ui/activity/MainActivity.java | 106 +++++++++++++++--- .../tempo/ui/fragment/DownloadFragment.java | 2 +- .../tempo/ui/fragment/HomeFragment.java | 2 +- .../tempo/ui/fragment/LibraryFragment.java | 2 +- .../tempo/ui/fragment/SettingsFragment.java | 4 + .../tempo/util/Preferences.kt | 12 ++ app/src/main/res/drawable/ic_albums.xml | 23 ++++ app/src/main/res/drawable/ic_artists.xml | 32 ++++++ app/src/main/res/drawable/ic_genres.xml | 11 ++ app/src/main/res/drawable/ic_playlist.xml | 33 ++++++ .../main/res/layout-land/activity_main.xml | 30 ++--- app/src/main/res/layout/activity_main.xml | 40 +++++-- app/src/main/res/layout/nav_drawer_header.xml | 31 +++++ app/src/main/res/menu/nav_drawer.xml | 60 ++++++++++ app/src/main/res/navigation/nav_graph.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/global_preferences.xml | 10 ++ 17 files changed, 365 insertions(+), 41 deletions(-) create mode 100644 app/src/main/res/drawable/ic_albums.xml create mode 100644 app/src/main/res/drawable/ic_artists.xml create mode 100644 app/src/main/res/drawable/ic_genres.xml create mode 100644 app/src/main/res/drawable/ic_playlist.xml create mode 100644 app/src/main/res/layout/nav_drawer_header.xml create mode 100644 app/src/main/res/menu/nav_drawer.xml diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index 3509175f4..d63d799c6 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -3,7 +3,6 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; -import android.graphics.Rect; import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.NetworkInfo; @@ -11,12 +10,16 @@ import android.os.Bundle; import android.os.Handler; import android.text.TextUtils; -import android.util.Log; +import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.core.splashscreen.SplashScreen; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.MediaItem; @@ -48,6 +51,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.color.DynamicColors; +import com.google.android.material.navigation.NavigationView; import com.google.common.util.concurrent.MoreExecutors; import java.util.Objects; @@ -63,9 +67,12 @@ public class MainActivity extends BaseActivity { private FragmentManager fragmentManager; private NavHostFragment navHostFragment; private BottomNavigationView bottomNavigationView; + private FrameLayout bottomNavigationViewFrame; public NavController navController; + private DrawerLayout drawerLayout; + private NavigationView navigationView; private BottomSheetBehavior bottomSheetBehavior; - private boolean isLandscape = false; + public boolean isLandscape = false; private AssetLinkNavigator assetLinkNavigator; private AssetLinkUtil.AssetLink pendingAssetLink; @@ -111,6 +118,7 @@ protected void onStart() { protected void onResume() { super.onResume(); pingServer(); + toggleNavigationDrawerLockOnOrientationChange(); } @Override @@ -148,14 +156,8 @@ public void init() { goToLogin(); } - // Set bottom navigation height - if (isLandscape) { - ViewGroup.LayoutParams layoutParams = bottomNavigationView.getLayoutParams(); - Rect windowRect = new Rect(); - bottomNavigationView.getWindowVisibleDisplayFrame(windowRect); - layoutParams.width = windowRect.height(); - bottomNavigationView.setLayoutParams(layoutParams); - } + toggleNavigationDrawerLockOnOrientationChange(); + } // BOTTOM SHEET/NAVIGATION @@ -259,8 +261,12 @@ private void animateBottomNavigation(float slideOffset, int navigationHeight) { private void initNavigation() { bottomNavigationView = findViewById(R.id.bottom_navigation); + bottomNavigationViewFrame = findViewById(R.id.bottom_navigation_frame); navHostFragment = (NavHostFragment) fragmentManager.findFragmentById(R.id.nav_host_fragment); navController = Objects.requireNonNull(navHostFragment).getNavController(); + // This is the lateral slide-in drawer + drawerLayout = findViewById(R.id.drawer_layout); + navigationView = findViewById(R.id.nav_view); /* * In questo modo intercetto il cambio schermata tramite navbar e se il bottom sheet รจ aperto, @@ -277,16 +283,90 @@ private void initNavigation() { }); NavigationUI.setupWithNavController(bottomNavigationView, navController); + NavigationUI.setupWithNavController(navigationView, navController); } public void setBottomNavigationBarVisibility(boolean visibility) { if (visibility) { bottomNavigationView.setVisibility(View.VISIBLE); + bottomNavigationViewFrame.setVisibility(View.VISIBLE); } else { bottomNavigationView.setVisibility(View.GONE); + bottomNavigationViewFrame.setVisibility(View.GONE); + } + } + + public void toggleBottomNavigationBarVisibilityOnOrientationChange() { + // Ignore orientation change, bottom navbar always hidden + if (Preferences.getHideBottomNavbarOnPortrait()) { + setBottomNavigationBarVisibility(false); + setPortraitPlayerBottomSheetPeekHeight(56); + setSystemBarsVisibility(!isLandscape); + return; + } + + if (!isLandscape) { + // Show app navbar + show system bars + setPortraitPlayerBottomSheetPeekHeight(136); + setBottomNavigationBarVisibility(true); + setSystemBarsVisibility(true); + } else { + // Hide app navbar + hide system bars + setPortraitPlayerBottomSheetPeekHeight(56); + setBottomNavigationBarVisibility(false); + setSystemBarsVisibility(false); + } + } + + public void setNavigationDrawerLock(boolean locked) { + int mode = locked + ? DrawerLayout.LOCK_MODE_LOCKED_CLOSED + : DrawerLayout.LOCK_MODE_UNLOCKED; + drawerLayout.setDrawerLockMode(mode); + } + + private void toggleNavigationDrawerLockOnOrientationChange() { + // Ignore orientation check, drawer always unlocked + if (Preferences.getEnableDrawerOnPortrait()) { + setNavigationDrawerLock(false); + return; + } + if (!isLandscape) { + setNavigationDrawerLock(true); + } else { + setNavigationDrawerLock(false); } } + public void setSystemBarsVisibility(boolean visibility) { + WindowInsetsControllerCompat insetsController; + View decorView = getWindow().getDecorView(); + insetsController = new WindowInsetsControllerCompat(getWindow(), decorView); + + if (visibility) { + WindowCompat.setDecorFitsSystemWindows(getWindow(), true); + insetsController.show(WindowInsetsCompat.Type.navigationBars()); + insetsController.show(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT); + } else { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + insetsController.hide(WindowInsetsCompat.Type.navigationBars()); + insetsController.hide(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + } + + private void setPortraitPlayerBottomSheetPeekHeight(int peekHeight) { + FrameLayout bottomSheet = findViewById(R.id.player_bottom_sheet); + BottomSheetBehavior behavior = + BottomSheetBehavior.from(bottomSheet); + + int newPeekPx = (int) (peekHeight * getResources().getDisplayMetrics().density); + behavior.setPeekHeight(newPeekPx); + } + private void initService() { MediaManager.check(getMediaBrowserListenableFuture()); @@ -570,4 +650,4 @@ private void playDownloadedMedia(Intent intent) { MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java index 12ca24349..72d1e0f30 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/DownloadFragment.java @@ -83,7 +83,7 @@ public void onStart() { super.onStart(); initializeMediaBrowser(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); activity.setBottomSheetVisibility(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java index a0d0380c3..7697c01f7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/HomeFragment.java @@ -53,7 +53,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); activity.setBottomSheetVisibility(true); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java index b50ee60f9..daa5d4121 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/LibraryFragment.java @@ -87,7 +87,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat public void onStart() { super.onStart(); initializeMediaBrowser(); - activity.setBottomNavigationBarVisibility(true); + activity.toggleBottomNavigationBarVisibilityOnOrientationChange(); } @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index aab615936..2a6ac691b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -130,6 +130,8 @@ public void onStart() { super.onStart(); activity.setBottomNavigationBarVisibility(false); activity.setBottomSheetVisibility(false); + activity.setNavigationDrawerLock(true); + activity.setSystemBarsVisibility(!activity.isLandscape); } @Override @@ -167,6 +169,8 @@ public void onResume() { public void onStop() { super.onStop(); activity.setBottomSheetVisibility(true); + activity.setNavigationDrawerLock(false); + activity.setSystemBarsVisibility(!activity.isLandscape); } @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index a95c84ae1..47c30cc00 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -30,6 +30,8 @@ object Preferences { private const val IMAGE_CACHE_SIZE = "image_cache_size" private const val STREAMING_CACHE_SIZE = "streaming_cache_size" private const val LANDSCAPE_ITEMS_PER_ROW = "landscape_items_per_row" + private const val ENABLE_DRAWER_ON_PORTRAIT = "enable_drawer_on_portrait" + private const val HIDE_BOTTOM_NAVBAR_ON_PORTRAIT = "hide_bottom_navbar_on_portrait" private const val IMAGE_SIZE = "image_size" private const val MAX_BITRATE_WIFI = "max_bitrate_wifi" private const val MAX_BITRATE_MOBILE = "max_bitrate_mobile" @@ -310,6 +312,16 @@ object Preferences { return App.getInstance().preferences.getString(LANDSCAPE_ITEMS_PER_ROW, "4")!!.toInt() } + @JvmStatic + fun getEnableDrawerOnPortrait(): Boolean { + return App.getInstance().preferences.getBoolean(ENABLE_DRAWER_ON_PORTRAIT, false) + } + + @JvmStatic + fun getHideBottomNavbarOnPortrait(): Boolean { + return App.getInstance().preferences.getBoolean(HIDE_BOTTOM_NAVBAR_ON_PORTRAIT, false) + } + @JvmStatic fun getImageSize(): Int { return App.getInstance().preferences.getString(IMAGE_SIZE, "-1")!!.toInt() diff --git a/app/src/main/res/drawable/ic_albums.xml b/app/src/main/res/drawable/ic_albums.xml new file mode 100644 index 000000000..4c1c697df --- /dev/null +++ b/app/src/main/res/drawable/ic_albums.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_artists.xml b/app/src/main/res/drawable/ic_artists.xml new file mode 100644 index 000000000..03d8308f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_artists.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_genres.xml b/app/src/main/res/drawable/ic_genres.xml new file mode 100644 index 000000000..3e7b66795 --- /dev/null +++ b/app/src/main/res/drawable/ic_genres.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_playlist.xml b/app/src/main/res/drawable/ic_playlist.xml new file mode 100644 index 000000000..08225cc90 --- /dev/null +++ b/app/src/main/res/drawable/ic_playlist.xml @@ -0,0 +1,33 @@ + + + + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 581aa4ce7..1b6d2e9c7 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -1,16 +1,15 @@ - + android:background="?attr/colorSurface"> + android:layout_height="match_parent"> - - - + + - - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 01edf7f5f..15a8d99cd 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,16 @@ - + android:background="?attr/colorSurface"> + + android:layout_height="match_parent"> + + + + + + - - + diff --git a/app/src/main/res/layout/nav_drawer_header.xml b/app/src/main/res/layout/nav_drawer_header.xml new file mode 100644 index 000000000..b6ff546c5 --- /dev/null +++ b/app/src/main/res/layout/nav_drawer_header.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml new file mode 100644 index 000000000..a01dfcedf --- /dev/null +++ b/app/src/main/res/menu/nav_drawer.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index c6aba5abb..d234d5440 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -220,6 +220,10 @@ + If enabled, show the shuffle button, remove the heart in the mini player Show radio If enabled, show the radio section. Restart the app for it to take full effect. + Enable drawer on portrait + Unlocks the lateral landscape menu drawer on portrait. The changes will take effect on restart. + Hide bottom navbar on portrait + Increases vertical space by removing the bottom navbar. The changes will take effect on restart. Auto download lyrics Automatically save lyrics when they are available so they can be shown while offline. Set replay gain mode diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index 4ee034e53..cdb875a9f 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -54,6 +54,16 @@ android:defaultValue="false" android:key="always_on_display" /> + + + + Date: Sat, 21 Feb 2026 14:31:22 -0300 Subject: [PATCH 3/8] fix: leaving settings always unlocks drawer --- .../com/cappielloantonio/tempo/ui/activity/MainActivity.java | 2 +- .../cappielloantonio/tempo/ui/fragment/SettingsFragment.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java index d63d799c6..f872b403f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/activity/MainActivity.java @@ -325,7 +325,7 @@ public void setNavigationDrawerLock(boolean locked) { drawerLayout.setDrawerLockMode(mode); } - private void toggleNavigationDrawerLockOnOrientationChange() { + public void toggleNavigationDrawerLockOnOrientationChange() { // Ignore orientation check, drawer always unlocked if (Preferences.getEnableDrawerOnPortrait()) { setNavigationDrawerLock(false); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 2a6ac691b..2981fa7c9 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -169,7 +169,7 @@ public void onResume() { public void onStop() { super.onStop(); activity.setBottomSheetVisibility(true); - activity.setNavigationDrawerLock(false); + activity.toggleNavigationDrawerLockOnOrientationChange(); activity.setSystemBarsVisibility(!activity.isLandscape); } From 52cfd36b09c461de72bed9b07a8c8852856c3421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 00:03:41 -0300 Subject: [PATCH 4/8] feat: set app settings inside a frame layout In order to add a toolbar with a back button in settings I needed to extend from a fragment so I converted SettingsFragment into a fragment and created SettingsContainerFragment, the latter is injected as a child of SettingsFragment inside a FrameLayout. Since SettingsContainerFragment extends from PreferenceFragmentCompat, this allows to swap it for other and, in the bigger picture, allow an arbitrary organization. --- .../fragment/SettingsContainerFragment.java | 609 ++++++++++++++++++ .../tempo/ui/fragment/SettingsFragment.java | 557 ++-------------- app/src/main/res/layout/fragment_settings.xml | 24 +- 3 files changed, 665 insertions(+), 525 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java new file mode 100644 index 000000000..172f21217 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -0,0 +1,609 @@ +package com.cappielloantonio.tempo.ui.fragment; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.audiofx.AudioEffect; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.text.InputFilter; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.os.LocaleListCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.media3.common.util.UnstableApi; +import androidx.navigation.NavController; +import androidx.navigation.NavOptions; +import androidx.navigation.fragment.NavHostFragment; +import androidx.preference.EditTextPreference; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; + +import com.cappielloantonio.tempo.BuildConfig; +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.helper.ThemeHelper; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.interfaces.ScanCallback; +import com.cappielloantonio.tempo.service.EqualizerManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; +import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.ExternalAudioReader; +import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.util.UIUtil; +import com.cappielloantonio.tempo.viewmodel.SettingViewModel; + +import java.util.Locale; +import java.util.Map; + +@OptIn(markerClass = UnstableApi.class) +public class SettingsContainerFragment extends PreferenceFragmentCompat { + + private static final String TAG = "SettingsFragment"; + private MainActivity activity; + + private SettingViewModel settingViewModel; + + private ActivityResultLauncher directoryPickerLauncher; + + private MediaService.LocalBinder mediaServiceBinder; + private boolean isServiceBound = false; + private ActivityResultLauncher equalizerResultLauncher; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + equalizerResultLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> {} + ); + + if (!BuildConfig.FLAVOR.equals("tempus")) { + PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); + if (githubUpdateCategory != null) { + getPreferenceScreen().removePreference(githubUpdateCategory); + } + } + + directoryPickerLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + Intent data = result.getData(); + if (data != null) { + Uri uri = data.getData(); + if (uri != null) { + requireContext().getContentResolver().takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ); + + Preferences.setDownloadDirectoryUri(uri.toString()); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); + checkDownloadDirectory(); + } + } + } + }); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + activity = (MainActivity) getActivity(); + + View view = super.onCreateView(inflater, container, savedInstanceState); + settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); + + if (view != null) { + getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); + } + + return view; + } + + @Override + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(false); + activity.setBottomSheetVisibility(false); + } + + @Override + public void onResume() { + super.onResume(); + + checkSystemEqualizer(); + checkCacheStorage(); + checkStorage(); + checkDownloadDirectory(); + + setStreamingCacheSize(); + setAppLanguage(); + setVersion(); + setNetorkPingTimeoutBase(); + + actionLogout(); + actionScan(); + actionSyncStarredAlbums(); + actionSyncStarredTracks(); + actionSyncStarredArtists(); + actionChangeStreamingCacheStorage(); + actionChangeDownloadStorage(); + actionSetDownloadDirectory(); + actionDeleteDownloadStorage(); + actionKeepScreenOn(); + actionAutoDownloadLyrics(); + actionMiniPlayerHeart(); + + bindMediaService(); + actionAppEqualizer(); + } + + @Override + public void onStop() { + super.onStop(); + activity.setBottomSheetVisibility(true); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.global_preferences, rootKey); + ListPreference themePreference = findPreference(Preferences.THEME); + if (themePreference != null) { + themePreference.setOnPreferenceChangeListener( + (preference, newValue) -> { + String themeOption = (String) newValue; + ThemeHelper.applyTheme(themeOption); + return true; + }); + } + } + + private void checkSystemEqualizer() { + Preference equalizer = findPreference("system_equalizer"); + + if (equalizer == null) return; + + Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); + + if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { + equalizer.setOnPreferenceClickListener(preference -> { + equalizerResultLauncher.launch(intent); + return true; + }); + } else { + equalizer.setVisible(false); + } + } + + private void checkCacheStorage() { + Preference storage = findPreference("streaming_cache_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkStorage() { + Preference storage = findPreference("download_storage"); + + if (storage == null) return; + + try { + if (requireContext().getExternalFilesDirs(null)[1] == null) { + storage.setVisible(false); + } else { + int pref = Preferences.getDownloadStoragePreference(); + if (pref == 0) { + storage.setSummary(R.string.download_storage_internal_dialog_negative_button); + } else if (pref == 1) { + storage.setSummary(R.string.download_storage_external_dialog_positive_button); + } else { + storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); + } + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + + private void checkDownloadDirectory() { + Preference storage = findPreference("download_storage"); + Preference directory = findPreference("set_download_directory"); + + if (directory == null) return; + + String current = Preferences.getDownloadDirectoryUri(); + if (current != null) { + if (storage != null) storage.setVisible(false); + directory.setVisible(true); + directory.setIcon(R.drawable.ic_close); + directory.setTitle(R.string.settings_clear_download_folder); + directory.setSummary(current); + } else { + if (storage != null) storage.setVisible(true); + if (Preferences.getDownloadStoragePreference() == 2) { + directory.setVisible(true); + directory.setIcon(R.drawable.ic_folder); + directory.setTitle(R.string.settings_set_download_folder); + directory.setSummary(R.string.settings_choose_download_folder); + } else { + directory.setVisible(false); + } + } + } + + private void setNetorkPingTimeoutBase() { + EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base"); + + if (networkPingTimeoutBase != null) { + networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance()); + networkPingTimeoutBase.setOnBindEditTextListener(editText -> { + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> { + for (int i = start; i < end; i++) { + if (!Character.isDigit(source.charAt(i))) { + return ""; + } + } + return null; + }}); + }); + + networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> { + String input = (String) newValue; + return input != null && !input.isEmpty(); + }); + } + } + + private void setStreamingCacheSize() { + ListPreference streamingCachePreference = findPreference("streaming_cache_size"); + + if (streamingCachePreference != null) { + streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { + @Nullable + @Override + public CharSequence provideSummary(@NonNull ListPreference preference) { + CharSequence entry = preference.getEntry(); + + if (entry == null) return null; + + long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); + + return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); + } + }); + } + } + + private void setAppLanguage() { + ListPreference localePref = (ListPreference) findPreference("language"); + + Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); + + CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); + CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); + + localePref.setEntries(entries); + localePref.setEntryValues(entryValues); + + String value = localePref.getValue(); + if ("default".equals(value)) { + localePref.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); + } + + localePref.setOnPreferenceChangeListener((preference, newValue) -> { + if ("default".equals(newValue)) { + AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); + preference.setSummary(requireContext().getString(R.string.settings_system_language)); + } else { + LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); + AppCompatDelegate.setApplicationLocales(appLocale); + preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); + } + return true; + }); + } + + private void setVersion() { + findPreference("version").setSummary(BuildConfig.VERSION_NAME); + } + + private void actionLogout() { + findPreference("logout").setOnPreferenceClickListener(preference -> { + activity.quit(); + return true; + }); + } + + private void actionScan() { + findPreference("scan_library").setOnPreferenceClickListener(preference -> { + settingViewModel.launchScan(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + + return true; + }); + } + + private void actionSyncStarredTracks() { + findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredSyncDialog dialog = new StarredSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredAlbums() { + findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionSyncStarredArtists() { + findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { + ((SwitchPreference)preference).setChecked(false); + }); + dialog.show(activity.getSupportFragmentManager(), null); + } + } + return true; + }); + } + + private void actionChangeStreamingCacheStorage() { + findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { + StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); + } + + @Override + public void onNegativeClick() { + findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionChangeDownloadStorage() { + findPreference("download_storage").setOnPreferenceClickListener(preference -> { + DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); + checkDownloadDirectory(); + } + + @Override + public void onNegativeClick() { + findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); + checkDownloadDirectory(); + } + + @Override + public void onNeutralClick() { + findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); + checkDownloadDirectory(); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionSetDownloadDirectory() { + Preference pref = findPreference("set_download_directory"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + String current = Preferences.getDownloadDirectoryUri(); + + if (current != null) { + Preferences.setDownloadDirectoryUri(null); + Preferences.setDownloadStoragePreference(0); + ExternalAudioReader.refreshCache(); + Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); + checkStorage(); + checkDownloadDirectory(); + } else { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + directoryPickerLauncher.launch(intent); + } + return true; + }); + } + } + + private void actionDeleteDownloadStorage() { + findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { + DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); + } + + private void actionMiniPlayerHeart() { + SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.showShuffleInsteadOfHeart()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setShuffleInsteadOfHeart((Boolean) newValue); + } + return true; + }); + } + + private void actionAutoDownloadLyrics() { + SwitchPreference preference = findPreference("auto_download_lyrics"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); + } + return true; + }); + } + + private void getScanStatus() { + settingViewModel.getScanStatus(new ScanCallback() { + @Override + public void onError(Exception exception) { + findPreference("scan_library").setSummary(exception.getMessage()); + } + + @Override + public void onSuccess(boolean isScanning, long count) { + findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); + if (isScanning) getScanStatus(); + } + }); + } + + private void actionKeepScreenOn() { + findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof Boolean) { + if ((Boolean) newValue) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } else { + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + } + return true; + }); + } + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mediaServiceBinder = (MediaService.LocalBinder) service; + isServiceBound = true; + checkEqualizerBands(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mediaServiceBinder = null; + isServiceBound = false; + } + }; + + private void bindMediaService() { + Intent intent = new Intent(requireActivity(), MediaService.class); + intent.setAction(MediaService.ACTION_BIND_EQUALIZER); + requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + isServiceBound = true; + } + + private void checkEqualizerBands() { + if (mediaServiceBinder != null) { + EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); + short numBands = eqManager.getNumberOfBands(); + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setVisible(numBands > 0); + } + } + } + + private void actionAppEqualizer() { + Preference appEqualizer = findPreference("app_equalizer"); + if (appEqualizer != null) { + appEqualizer.setOnPreferenceClickListener(preference -> { + NavController navController = NavHostFragment.findNavController(this); + NavOptions navOptions = new NavOptions.Builder() + .setLaunchSingleTop(true) + .setPopUpTo(R.id.equalizerFragment, true) + .build(); + activity.setBottomNavigationBarVisibility(true); + activity.setBottomSheetVisibility(true); + navController.navigate(R.id.equalizerFragment, null, navOptions); + return true; + }); + } + } + + @Override + public void onPause() { + super.onPause(); + if (isServiceBound) { + requireActivity().unbindService(serviceConnection); + isServiceBound = false; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 2981fa7c9..7d3464b7a 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -1,5 +1,7 @@ package com.cappielloantonio.tempo.ui.fragment; +import static com.google.android.material.internal.ViewUtils.hideKeyboard; + import android.app.Activity; import android.content.Context; import android.content.ComponentName; @@ -22,8 +24,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.OptIn; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; import androidx.core.os.LocaleListCompat; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.media3.common.util.UnstableApi; import androidx.navigation.NavController; @@ -38,6 +45,8 @@ import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.FragmentAlbumCatalogueBinding; +import com.cappielloantonio.tempo.databinding.FragmentSettingsBinding; import com.cappielloantonio.tempo.helper.ThemeHelper; import com.cappielloantonio.tempo.interfaces.DialogClickCallback; import com.cappielloantonio.tempo.interfaces.ScanCallback; @@ -59,554 +68,60 @@ import java.util.Locale; import java.util.Map; -@OptIn(markerClass = UnstableApi.class) -public class SettingsFragment extends PreferenceFragmentCompat { - private static final String TAG = "SettingsFragment"; +public class SettingsFragment extends Fragment { private MainActivity activity; - private SettingViewModel settingViewModel; - - private ActivityResultLauncher equalizerResultLauncher; - private ActivityResultLauncher directoryPickerLauncher; - - private MediaService.LocalBinder mediaServiceBinder; - private boolean isServiceBound = false; + private FragmentSettingsBinding bind; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - equalizerResultLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> {} - ); - - if (!BuildConfig.FLAVOR.equals("tempus")) { - PreferenceCategory githubUpdateCategory = findPreference("settings_github_update_category_key"); - if (githubUpdateCategory != null) { - getPreferenceScreen().removePreference(githubUpdateCategory); - } - } - - directoryPickerLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent data = result.getData(); - if (data != null) { - Uri uri = data.getData(); - if (uri != null) { - requireContext().getContentResolver().takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ); + activity = (MainActivity) getActivity(); - Preferences.setDownloadDirectoryUri(uri.toString()); - ExternalAudioReader.refreshCache(); - Toast.makeText(requireContext(), R.string.settings_download_folder_set, Toast.LENGTH_SHORT).show(); - checkDownloadDirectory(); - } - } - } - }); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - activity = (MainActivity) getActivity(); - - View view = super.onCreateView(inflater, container, savedInstanceState); - settingViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class); + bind = FragmentSettingsBinding.inflate(inflater,container,false); + View view = bind.getRoot(); - if (view != null) { - getListView().setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.global_padding_bottom)); - } + initAppBar(); return view; - } - @Override - public void onStart() { - super.onStart(); - activity.setBottomNavigationBarVisibility(false); - activity.setBottomSheetVisibility(false); - activity.setNavigationDrawerLock(true); - activity.setSystemBarsVisibility(!activity.isLandscape); } @Override - public void onResume() { - super.onResume(); + public void onViewCreated(@NonNull View view, + @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); - checkSystemEqualizer(); - checkCacheStorage(); - checkStorage(); - checkDownloadDirectory(); + // Add the PreferenceFragment only the first time + if (savedInstanceState == null) { + SettingsContainerFragment prefFragment = new SettingsContainerFragment(); - setStreamingCacheSize(); - setAppLanguage(); - setVersion(); - setNetorkPingTimeoutBase(); - - actionLogout(); - actionScan(); - actionSyncStarredAlbums(); - actionSyncStarredTracks(); - actionSyncStarredArtists(); - actionChangeStreamingCacheStorage(); - actionChangeDownloadStorage(); - actionSetDownloadDirectory(); - actionDeleteDownloadStorage(); - actionKeepScreenOn(); - actionAutoDownloadLyrics(); - actionMiniPlayerHeart(); - - bindMediaService(); - actionAppEqualizer(); + // Use the child fragment manager so the PreferenceFragment is scoped to this fragment + getChildFragmentManager() + .beginTransaction() + .replace(R.id.settings_container, prefFragment) + .setReorderingAllowed(true) // optional but recommended + .commit(); + } } @Override - public void onStop() { - super.onStop(); - activity.setBottomSheetVisibility(true); - activity.toggleNavigationDrawerLockOnOrientationChange(); + public void onStart() { + super.onStart(); + activity.setBottomNavigationBarVisibility(false); + activity.setBottomSheetVisibility(false); + activity.setNavigationDrawerLock(true); activity.setSystemBarsVisibility(!activity.isLandscape); } - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.global_preferences, rootKey); - ListPreference themePreference = findPreference(Preferences.THEME); - if (themePreference != null) { - themePreference.setOnPreferenceChangeListener( - (preference, newValue) -> { - String themeOption = (String) newValue; - ThemeHelper.applyTheme(themeOption); - return true; - }); - } - } - - private void checkSystemEqualizer() { - Preference equalizer = findPreference("system_equalizer"); - - if (equalizer == null) return; - - Intent intent = new Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL); - - if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) { - equalizer.setOnPreferenceClickListener(preference -> { - equalizerResultLauncher.launch(intent); - return true; - }); - } else { - equalizer.setVisible(false); - } - } - - private void checkCacheStorage() { - Preference storage = findPreference("streaming_cache_storage"); - - if (storage == null) return; - - try { - if (requireContext().getExternalFilesDirs(null)[1] == null) { - storage.setVisible(false); - } else { - storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button); - } - } catch (Exception exception) { - storage.setVisible(false); - } - } - - private void checkStorage() { - Preference storage = findPreference("download_storage"); - - if (storage == null) return; - - try { - if (requireContext().getExternalFilesDirs(null)[1] == null) { - storage.setVisible(false); - } else { - int pref = Preferences.getDownloadStoragePreference(); - if (pref == 0) { - storage.setSummary(R.string.download_storage_internal_dialog_negative_button); - } else if (pref == 1) { - storage.setSummary(R.string.download_storage_external_dialog_positive_button); - } else { - storage.setSummary(R.string.download_storage_directory_dialog_neutral_button); - } - } - } catch (Exception exception) { - storage.setVisible(false); - } - } - - private void checkDownloadDirectory() { - Preference storage = findPreference("download_storage"); - Preference directory = findPreference("set_download_directory"); - - if (directory == null) return; - - String current = Preferences.getDownloadDirectoryUri(); - if (current != null) { - if (storage != null) storage.setVisible(false); - directory.setVisible(true); - directory.setIcon(R.drawable.ic_close); - directory.setTitle(R.string.settings_clear_download_folder); - directory.setSummary(current); - } else { - if (storage != null) storage.setVisible(true); - if (Preferences.getDownloadStoragePreference() == 2) { - directory.setVisible(true); - directory.setIcon(R.drawable.ic_folder); - directory.setTitle(R.string.settings_set_download_folder); - directory.setSummary(R.string.settings_choose_download_folder); - } else { - directory.setVisible(false); - } - } - } - - private void setNetorkPingTimeoutBase() { - EditTextPreference networkPingTimeoutBase = findPreference("network_ping_timeout_base"); - - if (networkPingTimeoutBase != null) { - networkPingTimeoutBase.setSummaryProvider(EditTextPreference.SimpleSummaryProvider.getInstance()); - networkPingTimeoutBase.setOnBindEditTextListener(editText -> { - editText.setInputType(InputType.TYPE_CLASS_NUMBER); - editText.setFilters(new InputFilter[]{ (source, start, end, dest, dstart, dend) -> { - for (int i = start; i < end; i++) { - if (!Character.isDigit(source.charAt(i))) { - return ""; - } - } - return null; - }}); + private void initAppBar() { + bind.settingsToolbar.setNavigationOnClickListener(v -> { + activity.navController.navigateUp(); }); - - networkPingTimeoutBase.setOnPreferenceChangeListener((preference, newValue) -> { - String input = (String) newValue; - return input != null && !input.isEmpty(); - }); - } - } - - private void setStreamingCacheSize() { - ListPreference streamingCachePreference = findPreference("streaming_cache_size"); - - if (streamingCachePreference != null) { - streamingCachePreference.setSummaryProvider(new Preference.SummaryProvider() { - @Nullable - @Override - public CharSequence provideSummary(@NonNull ListPreference preference) { - CharSequence entry = preference.getEntry(); - - if (entry == null) return null; - - long currentSizeMb = DownloadUtil.getStreamingCacheSize(requireActivity()) / (1024 * 1024); - - return getString(R.string.settings_summary_streaming_cache_size, entry, String.valueOf(currentSizeMb)); - } - }); - } - } - - private void setAppLanguage() { - ListPreference localePref = (ListPreference) findPreference("language"); - - Map locales = UIUtil.getLangPreferenceDropdownEntries(requireContext()); - - CharSequence[] entries = locales.keySet().toArray(new CharSequence[locales.size()]); - CharSequence[] entryValues = locales.values().toArray(new CharSequence[locales.size()]); - - localePref.setEntries(entries); - localePref.setEntryValues(entryValues); - - String value = localePref.getValue(); - if ("default".equals(value)) { - localePref.setSummary(requireContext().getString(R.string.settings_system_language)); - } else { - localePref.setSummary(Locale.forLanguageTag(value).getDisplayName()); - } - - localePref.setOnPreferenceChangeListener((preference, newValue) -> { - if ("default".equals(newValue)) { - AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList()); - preference.setSummary(requireContext().getString(R.string.settings_system_language)); - } else { - LocaleListCompat appLocale = LocaleListCompat.forLanguageTags((String) newValue); - AppCompatDelegate.setApplicationLocales(appLocale); - preference.setSummary(Locale.forLanguageTag((String) newValue).getDisplayName()); - } - return true; - }); - } - - private void setVersion() { - findPreference("version").setSummary(BuildConfig.VERSION_NAME); - } - - private void actionLogout() { - findPreference("logout").setOnPreferenceClickListener(preference -> { - activity.quit(); - return true; - }); - } - - private void actionScan() { - findPreference("scan_library").setOnPreferenceClickListener(preference -> { - settingViewModel.launchScan(new ScanCallback() { - @Override - public void onError(Exception exception) { - findPreference("scan_library").setSummary(exception.getMessage()); - } - - @Override - public void onSuccess(boolean isScanning, long count) { - findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); - if (isScanning) getScanStatus(); - } - }); - - return true; - }); - } - - private void actionSyncStarredTracks() { - findPreference("sync_starred_tracks_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredSyncDialog dialog = new StarredSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionSyncStarredAlbums() { - findPreference("sync_starred_albums_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredAlbumSyncDialog dialog = new StarredAlbumSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionSyncStarredArtists() { - findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> { - ((SwitchPreference)preference).setChecked(false); - }); - dialog.show(activity.getSupportFragmentManager(), null); - } - } - return true; - }); - } - - private void actionChangeStreamingCacheStorage() { - findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> { - StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() { - @Override - public void onPositiveClick() { - findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_external_dialog_positive_button); - } - - @Override - public void onNegativeClick() { - findPreference("streaming_cache_storage").setSummary(R.string.streaming_cache_storage_internal_dialog_negative_button); - } - }); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionChangeDownloadStorage() { - findPreference("download_storage").setOnPreferenceClickListener(preference -> { - DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { - @Override - public void onPositiveClick() { - findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); - checkDownloadDirectory(); - } - - @Override - public void onNegativeClick() { - findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); - checkDownloadDirectory(); - } - - @Override - public void onNeutralClick() { - findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button); - checkDownloadDirectory(); - } - }); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionSetDownloadDirectory() { - Preference pref = findPreference("set_download_directory"); - if (pref != null) { - pref.setOnPreferenceClickListener(preference -> { - String current = Preferences.getDownloadDirectoryUri(); - - if (current != null) { - Preferences.setDownloadDirectoryUri(null); - Preferences.setDownloadStoragePreference(0); - ExternalAudioReader.refreshCache(); - Toast.makeText(requireContext(), R.string.settings_download_folder_cleared, Toast.LENGTH_SHORT).show(); - checkStorage(); - checkDownloadDirectory(); - } else { - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - directoryPickerLauncher.launch(intent); - } - return true; - }); - } - } - - private void actionDeleteDownloadStorage() { - findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> { - DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog(); - dialog.show(activity.getSupportFragmentManager(), null); - return true; - }); - } - - private void actionMiniPlayerHeart() { - SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); - if (preference == null) { - return; - } - - preference.setChecked(Preferences.showShuffleInsteadOfHeart()); - preference.setOnPreferenceChangeListener((pref, newValue) -> { - if (newValue instanceof Boolean) { - Preferences.setShuffleInsteadOfHeart((Boolean) newValue); - } - return true; - }); - } - - private void actionAutoDownloadLyrics() { - SwitchPreference preference = findPreference("auto_download_lyrics"); - if (preference == null) { - return; - } - - preference.setChecked(Preferences.isAutoDownloadLyricsEnabled()); - preference.setOnPreferenceChangeListener((pref, newValue) -> { - if (newValue instanceof Boolean) { - Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue); - } - return true; - }); - } - - private void getScanStatus() { - settingViewModel.getScanStatus(new ScanCallback() { - @Override - public void onError(Exception exception) { - findPreference("scan_library").setSummary(exception.getMessage()); - } - - @Override - public void onSuccess(boolean isScanning, long count) { - findPreference("scan_library").setSummary(getString(R.string.settings_scan_result, count)); - if (isScanning) getScanStatus(); - } - }); - } - - private void actionKeepScreenOn() { - findPreference("always_on_display").setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof Boolean) { - if ((Boolean) newValue) { - activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } else { - activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } - } - return true; - }); - } - - private final ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - mediaServiceBinder = (MediaService.LocalBinder) service; - isServiceBound = true; - checkEqualizerBands(); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - mediaServiceBinder = null; - isServiceBound = false; - } - }; - - private void bindMediaService() { - Intent intent = new Intent(requireActivity(), MediaService.class); - intent.setAction(MediaService.ACTION_BIND_EQUALIZER); - requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); - isServiceBound = true; - } - - private void checkEqualizerBands() { - if (mediaServiceBinder != null) { - EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager(); - short numBands = eqManager.getNumberOfBands(); - Preference appEqualizer = findPreference("app_equalizer"); - if (appEqualizer != null) { - appEqualizer.setVisible(numBands > 0); - } - } - } - - private void actionAppEqualizer() { - Preference appEqualizer = findPreference("app_equalizer"); - if (appEqualizer != null) { - appEqualizer.setOnPreferenceClickListener(preference -> { - NavController navController = NavHostFragment.findNavController(this); - NavOptions navOptions = new NavOptions.Builder() - .setLaunchSingleTop(true) - .setPopUpTo(R.id.equalizerFragment, true) - .build(); - activity.setBottomNavigationBarVisibility(true); - activity.setBottomSheetVisibility(true); - navController.navigate(R.id.equalizerFragment, null, navOptions); - return true; - }); - } - } - - @Override - public void onPause() { - super.onPause(); - if (isServiceBound) { - requireActivity().unbindService(serviceConnection); - isServiceBound = false; - } } } diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 3e8d99309..c5a98dc4a 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -1,6 +1,22 @@ - \ No newline at end of file + android:layout_height="match_parent"> + + + + + From 34d354d8039ac70798b880bf99c808ef00a1e330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 00:18:50 -0300 Subject: [PATCH 5/8] fix: onStop declaration on wrong class --- .../tempo/ui/fragment/SettingsContainerFragment.java | 6 ------ .../tempo/ui/fragment/SettingsFragment.java | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java index 172f21217..e770976a3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsContainerFragment.java @@ -164,12 +164,6 @@ public void onResume() { actionAppEqualizer(); } - @Override - public void onStop() { - super.onStop(); - activity.setBottomSheetVisibility(true); - } - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.global_preferences, rootKey); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 7d3464b7a..c1399d750 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -119,6 +119,12 @@ public void onStart() { activity.setSystemBarsVisibility(!activity.isLandscape); } + @Override + public void onStop() { + super.onStop(); + activity.setBottomSheetVisibility(true); + } + private void initAppBar() { bind.settingsToolbar.setNavigationOnClickListener(v -> { activity.navController.navigateUp(); From eeb125542d41760059e3a7c7653abf4d54a538f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 00:19:20 -0300 Subject: [PATCH 6/8] fix: equalizer not respecting navigation ui directives --- .../tempo/ui/fragment/EqualizerFragment.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt index ac6608c73..56f6419bd 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/EqualizerFragment.kt @@ -21,10 +21,12 @@ import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.service.EqualizerManager import com.cappielloantonio.tempo.service.BaseMediaService import com.cappielloantonio.tempo.service.MediaService +import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Preferences class EqualizerFragment : Fragment() { + private lateinit var activity: MainActivity private var equalizerManager: EqualizerManager? = null private lateinit var eqBandsContainer: LinearLayout private lateinit var eqSwitch: Switch @@ -33,6 +35,13 @@ class EqualizerFragment : Fragment() { private val bandSeekBars = mutableListOf() private var receiverRegistered = false + + @OptIn(UnstableApi::class) + override fun onAttach(context: Context) { + super.onAttach(context) + activity = requireActivity() as MainActivity + } + private val equalizerUpdatedReceiver = object : BroadcastReceiver() { @OptIn(UnstableApi::class) override fun onReceive(context: Context?, intent: Intent?) { @@ -73,8 +82,13 @@ class EqualizerFragment : Fragment() { ) receiverRegistered = true } + activity.setBottomNavigationBarVisibility(false) + activity.setBottomSheetVisibility(false) + activity.setNavigationDrawerLock(true) + activity.setSystemBarsVisibility(!activity.isLandscape) } + @OptIn(UnstableApi::class) override fun onStop() { super.onStop() requireActivity().unbindService(connection) @@ -87,6 +101,8 @@ class EqualizerFragment : Fragment() { } receiverRegistered = false } + + activity.setBottomSheetVisibility(true); } override fun onCreateView( From 96970f0281f5ade5d3da0d62791a58f45667f271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Mon, 23 Feb 2026 23:49:48 -0300 Subject: [PATCH 7/8] feat: reduce top songs from 10 to 3 10 items take almost the entire screen, which can become tiresome to scroll now it is limited to 3 before it truncates the list and shows the See All text --- .../tempo/ui/fragment/ArtistPageFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java index 63ea91e88..ccf4f6679 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/ArtistPageFragment.java @@ -282,9 +282,9 @@ private void initTopSongsView() { } else { if (bind != null) { bind.artistPageTopSongsSector.setVisibility(!songs.isEmpty() ? View.VISIBLE : View.GONE); - bind.mostStreamedSongTextViewClickable.setVisibility(songs.size() > 10 ? View.VISIBLE : View.GONE); + bind.mostStreamedSongTextViewClickable.setVisibility(songs.size() > 3 ? View.VISIBLE : View.GONE); } - songHorizontalAdapter.setItems(songs.stream().limit(10).collect(java.util.stream.Collectors.toList())); + songHorizontalAdapter.setItems(songs.stream().limit(3).collect(java.util.stream.Collectors.toList())); reapplyPlayback(); } }); From 97e027c777c00131e41eb383e31a0b061e0e91de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Villegas?= Date: Tue, 24 Feb 2026 00:16:22 -0300 Subject: [PATCH 8/8] fix: AppearsOn is not filtred correctly Albums/EPS/Singles were including releases were the artist was featured now each of them checks explicitly that the release matches the artist the opposite check is made for AppearsOn, this way they no longer mix --- .../tempo/viewmodel/ArtistPageViewModel.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java index affb6cc75..36af0af4f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/ArtistPageViewModel.java @@ -28,6 +28,7 @@ import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class ArtistPageViewModel extends AndroidViewModel { @@ -60,24 +61,27 @@ public void fetchCategorizedAlbums(androidx.lifecycle.LifecycleOwner owner) { allAlbums.sort(Comparator.comparing(AlbumID3::getYear).reversed()); mainAlbums.setValue(allAlbums.stream() - .filter(a -> isType(a, "album")) + .filter(a -> + isType(a, "album") && Objects.equals(a.getArtistId(), artist.getId())) .collect(Collectors.toList())); singles.setValue(allAlbums.stream() - .filter(a -> isType(a, "single")) + .filter(a -> + isType(a, "single") && Objects.equals(a.getArtistId(), artist.getId())) .collect(Collectors.toList())); eps.setValue(allAlbums.stream() - .filter(a -> isType(a, "ep")) + .filter(a -> + isType(a, "ep") && Objects.equals(a.getArtistId(), artist.getId())) .collect(Collectors.toList())); } + if (allAlbums != null) { + allAlbums.sort(Comparator.comparing(AlbumID3::getYear).reversed()); - List appearsOnList = fullArtist.getAppearsOn(); - if (appearsOnList != null) { - appearsOnList.sort(Comparator.comparing(AlbumID3::getYear).reversed()); - appearsOn.setValue(appearsOnList); - } else { - appearsOn.setValue(new java.util.ArrayList<>()); + appearsOn.setValue(allAlbums.stream() + .filter(a -> !Objects.equals(a.getArtistId(), artist.getId())) + .collect(Collectors.toList()) + ); } } });