diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java index 3d938862..7e750ce3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/AutomotiveRepository.java @@ -790,7 +790,7 @@ public class AutomotiveRepository { App.getSubsonicClientInstance(false) .getSearchingClient() - .search3(query, 20, 20, 20) + .search3(query, 20, 0, 20, 0, 20, 0) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java index 6762ed31..d1ee3159 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/SearchingRepository.java @@ -1,9 +1,14 @@ package com.cappielloantonio.tempo.repository; +import android.os.Handler; +import android.os.Looper; + import androidx.annotation.NonNull; import androidx.lifecycle.MutableLiveData; +import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.App; +import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.database.AppDatabase; import com.cappielloantonio.tempo.database.dao.RecentSearchDao; import com.cappielloantonio.tempo.model.RecentSearch; @@ -11,13 +16,18 @@ import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.AlbumID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.Child; +import com.cappielloantonio.tempo.subsonic.models.Playlist; +import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs; import com.cappielloantonio.tempo.subsonic.models.SearchResult2; import com.cappielloantonio.tempo.subsonic.models.SearchResult3; import com.cappielloantonio.tempo.util.Preferences; +import com.cappielloantonio.tempo.ui.fragment.SearchFragment; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; +import java.util.concurrent.Executors; import retrofit2.Call; import retrofit2.Callback; @@ -31,7 +41,7 @@ public class SearchingRepository { App.getSubsonicClientInstance(false) .getSearchingClient() - .search3(query, 20, 20, 20) + .search3(query, 20, 0, 20, 0, 20, 0) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { @@ -49,12 +59,63 @@ public class SearchingRepository { return result; } - public MutableLiveData search3(String query) { + @UnstableApi + public MutableLiveData search3(SearchFragment sf, String query) { MutableLiveData result = new MutableLiveData<>(); + Executors.newSingleThreadExecutor().execute(() -> { + List allSongs = new ArrayList<>(); + int offset = 0; + int limit = 1000; + boolean hasMore = true; + + while (hasMore) { + try { + Response response = App.getSubsonicClientInstance(false) + .getSearchingClient() + .search3(query, limit, offset, 0, 0, 0, 0) + .execute(); + + if (response.isSuccessful() && response.body() != null) { + SearchResult3 tmp = response.body().getSubsonicResponse().getSearchResult3(); + if (tmp != null && tmp.getSongs() != null && !tmp.getSongs().isEmpty()) { + List fetchedSongs = tmp.getSongs(); + allSongs.addAll(fetchedSongs); + + offset += fetchedSongs.size(); + hasMore = fetchedSongs.size() == limit; + } else { + hasMore = false; + } + } else { + hasMore = false; + } + } catch (IOException e) { + e.printStackTrace(); + hasMore = false; + } + } + PlaylistWithSongs pws = new PlaylistWithSongs("allsongs", allSongs); + pws.setName(sf.getView().getContext().getString(R.string.search_all_songs, String.valueOf(allSongs.size()))); + pws.setSongCount(allSongs.size()); + List lpws = new ArrayList<>(); + lpws.add(pws); + long duration = 0; + for (Child song: allSongs) { + if (song != null && song.getDuration() != null) { + duration += song.getDuration(); + } + } + pws.setDuration(duration); + + new Handler(Looper.getMainLooper()).post(() -> { + sf.updateUI(lpws); + }); + }); + App.getSubsonicClientInstance(false) .getSearchingClient() - .search3(query, 20, 20, 20) + .search3(query, 20, 0, 20, 0, 20, 0) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { @@ -77,7 +138,7 @@ public class SearchingRepository { App.getSubsonicClientInstance(false) .getSearchingClient() - .search3(query, 5, 5, 5) + .search3(query, 5, 0, 5, 0, 5, 0) .enqueue(new Callback() { @Override public void onResponse(@NonNull Call call, @NonNull Response response) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java index f4941047..70e49877 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingClient.java @@ -24,8 +24,8 @@ public class SearchingClient { return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount); } - public Call search3(String query, int songCount, int albumCount, int artistCount) { + public Call search3(String query, int songCount, int songOffset, int albumCount, int albumOffset, int artistCount, int artistOffset) { Log.d(TAG, "search3()"); - return searchingService.search3(subsonic.getParams(), query, songCount, albumCount, artistCount); + return searchingService.search3(subsonic.getParams(), query, songCount, songOffset, albumCount, albumOffset, artistCount, artistOffset); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java index d8dc1492..e2486b28 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java +++ b/app/src/main/java/com/cappielloantonio/tempo/subsonic/api/searching/SearchingService.java @@ -14,5 +14,5 @@ public interface SearchingService { Call search2(@QueryMap Map params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount); @GET("search3") - Call search3(@QueryMap Map params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount); + Call search3(@QueryMap Map params, @Query("query") String query, @Query("songCount") int songCount, @Query("songOffset") int songOffset, @Query("albumCount") int albumCount, @Query("albumOffset") int albumOffset, @Query("artistCount") int artistCount, @Query("artistOffset") int artistOffset); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java index 14efc149..bb7f8c13 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SearchFragment.java @@ -26,16 +26,20 @@ import com.cappielloantonio.tempo.helper.recyclerview.CustomLinearSnapHelper; import com.cappielloantonio.tempo.interfaces.ClickCallback; import com.cappielloantonio.tempo.service.MediaManager; import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.Playlist; import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; +import com.cappielloantonio.tempo.ui.adapter.PlaylistHorizontalAdapter; import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel; import com.cappielloantonio.tempo.viewmodel.SearchViewModel; +import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs; import com.google.common.util.concurrent.ListenableFuture; import java.util.Collections; +import java.util.List; @UnstableApi public class SearchFragment extends Fragment implements ClickCallback { @@ -49,6 +53,7 @@ public class SearchFragment extends Fragment implements ClickCallback { private ArtistAdapter artistAdapter; private AlbumAdapter albumAdapter; private SongHorizontalAdapter songHorizontalAdapter; + private PlaylistHorizontalAdapter playlistHorizontalAdapter; private ListenableFuture mediaBrowserListenableFuture; @@ -126,6 +131,12 @@ public class SearchFragment extends Fragment implements ClickCallback { reapplyPlayback(); bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter); + + bind.allsongsview.setLayoutManager(new LinearLayoutManager(requireContext())); + bind.allsongsview.setHasFixedSize(true); + + playlistHorizontalAdapter = new PlaylistHorizontalAdapter(this); + bind.allsongsview.setAdapter(playlistHorizontalAdapter); } private void initSearchView() { @@ -216,13 +227,23 @@ public class SearchFragment extends Fragment implements ClickCallback { public void search(String query) { searchViewModel.setQuery(query); + bind.allSongs.setText(this.getView().getContext().getString(R.string.search_all_songs_loading)); + playlistHorizontalAdapter.setItems(Collections.emptyList()); bind.searchBar.setText(query); bind.searchView.hide(); performSearch(query); } + public void updateUI(List allSongs) { + if (!allSongs.isEmpty()) { + playlistHorizontalAdapter.setItems(allSongs); + } else { + playlistHorizontalAdapter.setItems(Collections.emptyList()); + } + bind.allSongs.setText(this.getView().getContext().getString(R.string.search_all_songs_play,String.valueOf(allSongs.getFirst().getName()))); + } private void performSearch(String query) { - searchViewModel.search3(query).observe(getViewLifecycleOwner(), result -> { + searchViewModel.search3(this, query).observe(getViewLifecycleOwner(), result -> { if (bind != null) { if (result.getArtists() != null) { bind.searchArtistSector.setVisibility(!result.getArtists().isEmpty() ? View.VISIBLE : View.GONE); @@ -281,6 +302,19 @@ public class SearchFragment extends Fragment implements ClickCallback { Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle); } + @Override + public void onPlaylistClick(Bundle bundle) { + PlaylistWithSongs playlistWithSongs = bundle.getParcelable(Constants.PLAYLIST_OBJECT); + if (playlistWithSongs != null) { + MediaManager.startQueue(mediaBrowserListenableFuture, playlistWithSongs.getEntries(), 0); + } + } + + @Override + public void onPlaylistLongClick(Bundle bundle) { + Navigation.findNavController(requireView()).navigate(R.id.playlistBottomSheetDialog, bundle); + } + @Override public void onAlbumClick(Bundle bundle) { Navigation.findNavController(requireView()).navigate(R.id.albumPageFragment, bundle); diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java index dd810b8e..80308e4f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/AlbumBottomSheetDialog.java @@ -299,4 +299,4 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements homeViewModel.refreshShares(requireActivity()); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PlaylistBottomSheetDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PlaylistBottomSheetDialog.java new file mode 100644 index 00000000..84ccd08c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/bottomsheetdialog/PlaylistBottomSheetDialog.java @@ -0,0 +1,112 @@ +package com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog; + +import android.content.ComponentName; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaBrowser; +import androidx.media3.session.SessionToken; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.glide.CustomGlideRequest; +import com.cappielloantonio.tempo.service.MediaManager; +import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.subsonic.models.PlaylistWithSongs; +import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.util.Constants; +import com.cappielloantonio.tempo.util.MusicUtil; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.common.util.concurrent.ListenableFuture; + +@UnstableApi +public class PlaylistBottomSheetDialog extends BottomSheetDialogFragment implements View.OnClickListener { + private PlaylistWithSongs playlist; + private ListenableFuture mediaBrowserListenableFuture; + private static final String TAG = "PlaylistBottomSheetDialog"; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.bottom_sheet_playlist_dialog, container, false); + + playlist = requireArguments().getParcelable(Constants.PLAYLIST_OBJECT); + + init(view); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + + initializeMediaBrowser(); + } + + @Override + public void onStop() { + releaseMediaBrowser(); + super.onStop(); + } + + private void init(View view) { + ImageView coverPlaylist = view.findViewById(R.id.playlist_cover_image_view); + + CustomGlideRequest.Builder + .from(view.getContext(), playlist.getCoverArtId(), CustomGlideRequest.ResourceType.Playlist) + .build() + .into(coverPlaylist); + + TextView titlePlaylist = view.findViewById(R.id.playlist_title_text_view); + titlePlaylist.setText(playlist.getName()); + + titlePlaylist.setSelected(true); + + TextView countPlaylist = view.findViewById(R.id.playlist_count_text_view); + countPlaylist.setText(view.getContext().getString(R.string.playlist_counted_tracks, playlist.getSongCount(), MusicUtil.getReadableDurationString(playlist.getDuration(), false))); + + TextView playNext = view.findViewById(R.id.play_next_text_view); + playNext.setOnClickListener(v -> { + MediaManager.enqueue(mediaBrowserListenableFuture, playlist.getEntries(), true); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + dismissBottomSheet(); + }); + + TextView addToQueue = view.findViewById(R.id.add_to_queue_text_view); + addToQueue.setOnClickListener(v -> { + MediaManager.enqueue(mediaBrowserListenableFuture, playlist.getEntries(), false); + ((MainActivity) requireActivity()).setBottomSheetInPeek(true); + dismissBottomSheet(); + }); + } + + @Override + public void onClick(View v) { + dismissBottomSheet(); + } + + private void dismissBottomSheet() { + dismiss(); + } + + private void initializeMediaBrowser() { + mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); + } + + private void releaseMediaBrowser() { + MediaBrowser.releaseFuture(mediaBrowserListenableFuture); + } + +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java index f878999e..7bd4cf4d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/SearchViewModel.java @@ -5,11 +5,13 @@ import android.app.Application; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; +import androidx.media3.common.util.UnstableApi; import com.cappielloantonio.tempo.model.RecentSearch; import com.cappielloantonio.tempo.repository.SearchingRepository; import com.cappielloantonio.tempo.subsonic.models.SearchResult2; import com.cappielloantonio.tempo.subsonic.models.SearchResult3; +import com.cappielloantonio.tempo.ui.fragment.SearchFragment; import java.util.ArrayList; import java.util.List; @@ -43,8 +45,9 @@ public class SearchViewModel extends AndroidViewModel { return searchingRepository.search2(title); } - public LiveData search3(String title) { - return searchingRepository.search3(title); + @UnstableApi + public LiveData search3(SearchFragment sf, String title) { + return searchingRepository.search3(sf, title); } public void insertNewSearch(String search) { diff --git a/app/src/main/res/layout/bottom_sheet_album_dialog.xml b/app/src/main/res/layout/bottom_sheet_album_dialog.xml index b37a5f90..51ce847a 100644 --- a/app/src/main/res/layout/bottom_sheet_album_dialog.xml +++ b/app/src/main/res/layout/bottom_sheet_album_dialog.xml @@ -200,4 +200,4 @@ android:text="@string/album_bottom_sheet_share" android:visibility="gone"/> - \ No newline at end of file + diff --git a/app/src/main/res/layout/bottom_sheet_artist_dialog.xml b/app/src/main/res/layout/bottom_sheet_artist_dialog.xml index 75ef2cef..4bbfed8e 100644 --- a/app/src/main/res/layout/bottom_sheet_artist_dialog.xml +++ b/app/src/main/res/layout/bottom_sheet_artist_dialog.xml @@ -92,4 +92,4 @@ android:paddingBottom="12dp" android:text="@string/artist_bottom_sheet_shuffle" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/bottom_sheet_playlist_dialog.xml b/app/src/main/res/layout/bottom_sheet_playlist_dialog.xml new file mode 100644 index 00000000..5aa9ee62 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_playlist_dialog.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index eec6e6fb..ebfd7564 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -108,6 +108,22 @@ android:clipToPadding="false" android:paddingTop="8dp" android:paddingBottom="8dp" /> + + + + + @@ -146,4 +162,4 @@ android:orientation="vertical"/> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_horizontal_track.xml b/app/src/main/res/layout/item_horizontal_track.xml index dfead0fd..97dc379a 100644 --- a/app/src/main/res/layout/item_horizontal_track.xml +++ b/app/src/main/res/layout/item_horizontal_track.xml @@ -225,4 +225,4 @@ android:background="@drawable/ic_more_vert" android:foreground="?android:attr/selectableItemBackgroundBorderless" /> - \ 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 d234d544..d6226b40 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -329,6 +329,11 @@ android:name="com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog" android:label="SongBottomSheetDialog" tools:layout="@layout/bottom_sheet_song_dialog" /> + Sort recent searches chronologically If enabled, sort searches chronologically. Sort by name if disabled. + Getting all songs ... + all %1$s songs + Play %1$s