From 9e6926fc9704c6145bd46e57d509597fb81d8c75 Mon Sep 17 00:00:00 2001 From: CappielloAntonio Date: Fri, 22 Nov 2024 21:57:27 +0100 Subject: [PATCH] feat: add sorting and search functionality for song list --- .../ui/adapter/SongHorizontalAdapter.java | 17 +++ .../ui/fragment/SongListPageFragment.java | 124 +++++++++++++++++- .../cappielloantonio/tempo/util/Constants.kt | 3 + .../tempo/util/MusicUtil.java | 2 +- .../viewmodel/SongListPageViewModel.java | 8 +- .../res/layout/fragment_song_list_page.xml | 37 +++++- .../main/res/menu/sort_song_popup_menu.xml | 12 ++ app/src/main/res/values/strings.xml | 4 + 8 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 app/src/main/res/menu/sort_song_popup_menu.xml diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java index 826a7edf..a1630bca 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/adapter/SongHorizontalAdapter.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -230,4 +231,20 @@ private boolean onLongClick() { return true; } } + + public void sort(String order) { + switch (order) { + case Constants.MEDIA_BY_TITLE: + songs.sort(Comparator.comparing(Child::getTitle)); + break; + case Constants.MEDIA_MOST_RECENTLY_STARRED: + songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.reverseOrder()))); + break; + case Constants.MEDIA_LEAST_RECENTLY_STARRED: + songs.sort(Comparator.comparing(Child::getStarred, Comparator.nullsLast(Comparator.naturalOrder()))); + break; + } + + notifyDataSetChanged(); + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java index 7262705e..8d20281e 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SongListPageFragment.java @@ -1,12 +1,22 @@ package com.cappielloantonio.tempo.ui.fragment; +import android.annotation.SuppressLint; import android.content.ComponentName; +import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.PopupMenu; +import android.widget.SearchView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; @@ -22,14 +32,15 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; -import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel; import com.google.common.util.concurrent.ListenableFuture; import java.util.Collections; +import java.util.List; @UnstableApi public class SongListPageFragment extends Fragment implements ClickCallback { @@ -45,6 +56,12 @@ public class SongListPageFragment extends Fragment implements ClickCallback { private boolean isLoading = true; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { activity = (MainActivity) getActivity(); @@ -138,7 +155,10 @@ private void initAppBar() { } if (bind != null) - bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp()); + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); if (bind != null) bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { @@ -153,6 +173,8 @@ private void initAppBar() { private void initButtons() { songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { if (bind != null) { + setSongListPageSorter(); + bind.songListShuffleImageView.setOnClickListener(v -> { Collections.shuffle(songs); MediaManager.startQueue(mediaBrowserListenableFuture, songs.subList(0, Math.min(25, songs.size())), 0); @@ -162,6 +184,7 @@ private void initButtons() { }); } + @SuppressLint("ClickableViewAccessibility") private void initSongListView() { bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); bind.songListRecyclerView.setHasFixedSize(true); @@ -171,6 +194,7 @@ private void initSongListView() { songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> { isLoading = false; songHorizontalAdapter.setItems(songs); + setSongListPageSubtitle(songs); }); bind.songListRecyclerView.addOnScrollListener(new PaginationScrollListener((LinearLayoutManager) bind.songListRecyclerView.getLayoutManager()) { @@ -185,6 +209,101 @@ public boolean isLoading() { return isLoading; } }); + + bind.songListRecyclerView.setOnTouchListener((v, event) -> { + hideKeyboard(v); + return false; + }); + + bind.songListSortImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.sort_song_popup_menu)); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.toolbar_menu, menu); + + MenuItem searchItem = menu.findItem(R.id.action_search); + + SearchView searchView = (SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + songHorizontalAdapter.getFilter().filter(newText); + return false; + } + }); + + searchView.setPadding(-32, 0, 0, 0); + } + + private void hideKeyboard(View view) { + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private void showPopupMenu(View view, int menuResource) { + PopupMenu popup = new PopupMenu(requireContext(), view); + popup.getMenuInflater().inflate(menuResource, popup.getMenu()); + + popup.setOnMenuItemClickListener(menuItem -> { + if (menuItem.getItemId() == R.id.menu_song_sort_name) { + songHorizontalAdapter.sort(Constants.MEDIA_BY_TITLE); + return true; + } else if (menuItem.getItemId() == R.id.menu_song_sort_most_recently_starred) { + songHorizontalAdapter.sort(Constants.MEDIA_MOST_RECENTLY_STARRED); + return true; + } else if (menuItem.getItemId() == R.id.menu_song_sort_least_recently_starred) { + songHorizontalAdapter.sort(Constants.MEDIA_LEAST_RECENTLY_STARRED); + return true; + } + + return false; + }); + + popup.show(); + } + + private void setSongListPageSubtitle(List children) { + switch (songListPageViewModel.title) { + case Constants.MEDIA_BY_GENRE: + bind.pageSubtitleLabel.setText(children.size() < songListPageViewModel.maxNumberByGenre ? + getString(R.string.song_list_page_count, children.size()) : + getString(R.string.song_list_page_count_unknown, songListPageViewModel.maxNumberByGenre) + ); + break; + case Constants.MEDIA_BY_YEAR: + bind.pageSubtitleLabel.setText(children.size() < songListPageViewModel.maxNumberByYear ? + getString(R.string.song_list_page_count, children.size()) : + getString(R.string.song_list_page_count_unknown, songListPageViewModel.maxNumberByYear) + ); + break; + case Constants.MEDIA_BY_ARTIST: + case Constants.MEDIA_BY_GENRES: + case Constants.MEDIA_STARRED: + bind.pageSubtitleLabel.setText(getString(R.string.song_list_page_count, children.size())); + break; + } + } + + private void setSongListPageSorter() { + switch (songListPageViewModel.title) { + case Constants.MEDIA_BY_GENRE: + case Constants.MEDIA_BY_YEAR: + bind.songListSortImageView.setVisibility(View.GONE); + break; + case Constants.MEDIA_BY_ARTIST: + case Constants.MEDIA_BY_GENRES: + case Constants.MEDIA_STARRED: + bind.songListSortImageView.setVisibility(View.VISIBLE); + break; + } } private void initializeMediaBrowser() { @@ -197,6 +316,7 @@ private void releaseMediaBrowser() { @Override public void onMediaClick(Bundle bundle) { + hideKeyboard(requireView()); MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION)); activity.setBottomSheetInPeek(true); } 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 2299eafe..7281b0fe 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -76,6 +76,9 @@ object Constants { const val MEDIA_MIX = "MEDIA_MIX" const val MEDIA_CHRONOLOGY = "MEDIA_CHRONOLOGY" const val MEDIA_BEST_OF = "MEDIA_BEST_OF" + const val MEDIA_BY_TITLE = "MEDIA_BY_TITLE" + const val MEDIA_MOST_RECENTLY_STARRED = "MEDIA_MOST_RECENTLY_STARRED" + const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED" const val DOWNLOAD_URI = "rest/download" diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java index dfbf956a..fddb4cce 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MusicUtil.java @@ -157,7 +157,7 @@ public static String getReadableDurationString(Integer duration, boolean millis) } public static String getReadableAudioQualityString(Child child) { - if (!Preferences.showAudioQuality()) return ""; + if (!Preferences.showAudioQuality() || child.getBitrate() == null) return ""; return "•" + " " + diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java index 2e54c33f..d2396f61 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SongListPageViewModel.java @@ -36,6 +36,8 @@ public class SongListPageViewModel extends AndroidViewModel { public ArrayList filterNames = new ArrayList<>(); public int year = 0; + public int maxNumberByYear = 500; + public int maxNumberByGenre = 100; public SongListPageViewModel(@NonNull Application application) { super(application); @@ -58,7 +60,7 @@ public LiveData> getSongList() { songList = songRepository.getSongsByGenres(filters); break; case Constants.MEDIA_BY_YEAR: - songList = songRepository.getRandomSample(500, year, year + 10); + songList = songRepository.getRandomSample(maxNumberByYear, year, year + 10); break; case Constants.MEDIA_STARRED: songList = songRepository.getStarredSongs(false, -1); @@ -73,9 +75,9 @@ public void getSongsByPage(LifecycleOwner owner) { case Constants.MEDIA_BY_GENRE: int songCount = songList.getValue() != null ? songList.getValue().size() : 0; - if (songCount > 0 && songCount % 100 != 0) return; + if (songCount > 0 && songCount % maxNumberByGenre != 0) return; - int page = songCount / 100; + int page = songCount / maxNumberByGenre; songRepository.getSongsByGenre(genre.getGenre(), page).observe(owner, children -> { if (children != null && !children.isEmpty()) { List currentMedia = songList.getValue(); diff --git a/app/src/main/res/layout/fragment_song_list_page.xml b/app/src/main/res/layout/fragment_song_list_page.xml index 8a57e949..be4bdbc9 100644 --- a/app/src/main/res/layout/fragment_song_list_page.xml +++ b/app/src/main/res/layout/fragment_song_list_page.xml @@ -33,17 +33,48 @@ + + +