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 com.cappielloantonio.tempo.util.Preferences; 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 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter activity.navController.navigateUp()); + bind.toolbar.setNavigationOnClickListener(v -> { + hideKeyboard(v); + activity.navController.navigateUp(); + }); if (bind != null) bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> { @@ -153,6 +173,8 @@ public class SongListPageFragment extends Fragment implements ClickCallback { 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 @@ public class SongListPageFragment extends Fragment implements ClickCallback { }); } + @SuppressLint("ClickableViewAccessibility") private void initSongListView() { bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); bind.songListRecyclerView.setHasFixedSize(true); @@ -171,6 +194,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback { 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 class SongListPageFragment extends Fragment implements ClickCallback { 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 @@ public class SongListPageFragment extends Fragment implements ClickCallback { @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 class MusicUtil { } 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 class SongListPageViewModel extends AndroidViewModel { 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 class SongListPageViewModel extends AndroidViewModel { 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 @@ + + +