Skip to content

Commit

Permalink
feat: add sorting and search functionality for song list
Browse files Browse the repository at this point in the history
  • Loading branch information
CappielloAntonio committed Nov 22, 2024
1 parent 780f1c3 commit 9e6926f
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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) -> {
Expand All @@ -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);
Expand All @@ -162,6 +184,7 @@ private void initButtons() {
});
}

@SuppressLint("ClickableViewAccessibility")
private void initSongListView() {
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songListRecyclerView.setHasFixedSize(true);
Expand All @@ -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()) {
Expand All @@ -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<Child> 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() {
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 "•" +
" " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class SongListPageViewModel extends AndroidViewModel {
public ArrayList<String> filterNames = new ArrayList<>();

public int year = 0;
public int maxNumberByYear = 500;
public int maxNumberByGenre = 100;

public SongListPageViewModel(@NonNull Application application) {
super(application);
Expand All @@ -58,7 +60,7 @@ public LiveData<List<Child>> 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);
Expand All @@ -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<Child> currentMedia = songList.getValue();
Expand Down
37 changes: 34 additions & 3 deletions app/src/main/res/layout/fragment_song_list_page.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,48 @@
<TextView
android:id="@+id/page_title_label"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingEnd="4dp"
android:paddingBottom="24dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/song_list_shuffle_image_view"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/page_subtitle_label"
style="@style/TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="4dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="24dp"
app:layout_constraintTop_toTopOf="@id/page_title_label"
app:layout_constraintBottom_toBottomOf="@id/page_title_label"
app:layout_constraintStart_toEndOf="@id/page_title_label" />

<Button
android:id="@+id/song_list_sort_image_view"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="12dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:visibility="gone"
app:cornerRadius="30dp"
app:icon="@drawable/ic_sort_list"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/song_list_shuffle_image_view" />

<Button
android:id="@+id/song_list_shuffle_image_view"
style="@style/Widget.Material3.Button.TonalButton.Icon"
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/menu/sort_song_popup_menu.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_song_sort_name"
android:title="@string/menu_sort_name" />
<item
android:id="@+id/menu_song_sort_most_recently_starred"
android:title="@string/menu_sort_most_recently_starred" />
<item
android:id="@+id/menu_song_sort_least_recently_starred"
android:title="@string/menu_sort_least_recently_starred" />
</menu>
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
<string name="menu_sort_recently_added">Recently added</string>
<string name="menu_sort_recently_played">Recently played</string>
<string name="menu_sort_most_played">Most played</string>
<string name="menu_sort_most_recently_starred">Most recently starred</string>
<string name="menu_sort_least_recently_starred">Least recently starred</string>
<string name="menu_pin_button">Add to home screen</string>
<string name="menu_unpin_button">Remove from home screen</string>
<string name="menu_sort_year">Year</string>
Expand Down Expand Up @@ -373,6 +375,8 @@
<string name="song_list_page_starred">Starred tracks</string>
<string name="song_list_page_top">%1$s\'s top tracks</string>
<string name="song_list_page_year">Year %1$d</string>
<string name="song_list_page_count">(%1$d)</string>
<string name="song_list_page_count_unknown">(+%1$d)</string>
<string name="song_subtitle_formatter">%1$s • %2$s %3$s</string>
<string name="starred_sync_dialog_negative_button">Cancel</string>
<string name="starred_sync_dialog_neutral_button">Continue</string>
Expand Down

0 comments on commit 9e6926f

Please sign in to comment.