Added all-songs feature

This commit is contained in:
Unknown0816
2026-03-22 20:19:37 +01:00
parent 7a17e91690
commit 23a68011e2
15 changed files with 380 additions and 16 deletions

View File

@@ -789,7 +789,7 @@ public class AutomotiveRepository {
App.getSubsonicClientInstance(false)
.getSearchingClient()
.search3(query, 20, 20, 20)
.search3(query, 20, 0, 20, 0, 20, 0)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {

View File

@@ -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<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
@@ -49,12 +59,63 @@ public class SearchingRepository {
return result;
}
public MutableLiveData<SearchResult3> search3(String query) {
@UnstableApi
public MutableLiveData<SearchResult3> search3(SearchFragment sf, String query) {
MutableLiveData<SearchResult3> result = new MutableLiveData<>();
Executors.newSingleThreadExecutor().execute(() -> {
List<Child> allSongs = new ArrayList<>();
int offset = 0;
int limit = 1000;
boolean hasMore = true;
while (hasMore) {
try {
Response<ApiResponse> 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<Child> 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<Playlist> 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<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> 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<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {

View File

@@ -24,8 +24,8 @@ public class SearchingClient {
return searchingService.search2(subsonic.getParams(), query, songCount, albumCount, artistCount);
}
public Call<ApiResponse> search3(String query, int songCount, int albumCount, int artistCount) {
public Call<ApiResponse> 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);
}
}

View File

@@ -14,5 +14,5 @@ public interface SearchingService {
Call<ApiResponse> search2(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
@GET("search3")
Call<ApiResponse> search3(@QueryMap Map<String, String> params, @Query("query") String query, @Query("songCount") int songCount, @Query("albumCount") int albumCount, @Query("artistCount") int artistCount);
Call<ApiResponse> search3(@QueryMap Map<String, String> 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);
}

View File

@@ -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<MediaBrowser> 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<Playlist> 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);

View File

@@ -299,4 +299,4 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
homeViewModel.refreshShares(requireActivity());
}
}
}

View File

@@ -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<MediaBrowser> 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);
}
}

View File

@@ -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<SearchResult3> search3(String title) {
return searchingRepository.search3(title);
@UnstableApi
public LiveData<SearchResult3> search3(SearchFragment sf, String title) {
return searchingRepository.search3(sf, title);
}
public void insertNewSearch(String search) {

View File

@@ -200,4 +200,4 @@
android:text="@string/album_bottom_sheet_share"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -92,4 +92,4 @@
android:paddingBottom="12dp"
android:text="@string/artist_bottom_sheet_shuffle" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:clipChildren="false">
<!-- Header -->
<ImageView
android:id="@+id/playlist_cover_image_view"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_margin="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ToggleButton
android:id="@+id/button_favorite"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:background="@drawable/button_favorite_selector"
android:checked="false"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center_vertical"
android:text=""
android:textOff=""
android:textOn=""
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/playlist_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="marquee"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@id/button_favorite"
app:layout_constraintStart_toEndOf="@+id/playlist_cover_image_view"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/playlist_count_text_view"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@id/button_favorite"
app:layout_constraintStart_toEndOf="@+id/playlist_cover_image_view"
app:layout_constraintTop_toBottomOf="@+id/playlist_title_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/song_asset_link_row"
layout="@layout/view_asset_link_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="12dp" />
<LinearLayout
android:id="@+id/option_linear_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"
android:paddingBottom="12dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<TextView
android:id="@+id/play_next_text_view"
style="@style/LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:paddingStart="20dp"
android:paddingTop="12dp"
android:paddingEnd="20dp"
android:paddingBottom="12dp"
android:text="@string/song_bottom_sheet_play_next" />
<TextView
android:id="@+id/add_to_queue_text_view"
style="@style/LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:paddingStart="20dp"
android:paddingTop="12dp"
android:paddingEnd="20dp"
android:paddingBottom="12dp"
android:text="@string/song_bottom_sheet_add_to_queue" />
<TextView
android:id="@+id/share_text_view"
style="@style/LabelMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:paddingStart="20dp"
android:paddingTop="12dp"
android:paddingEnd="20dp"
android:paddingBottom="12dp"
android:text="@string/song_bottom_sheet_share"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>

View File

@@ -108,6 +108,22 @@
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView
android:id="@+id/allSongs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:text="@string/search_all_songs_loading"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textSize="16sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/allsongsview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
@@ -146,4 +162,4 @@
android:orientation="vertical"/>
</ScrollView>
</com.google.android.material.search.SearchView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -225,4 +225,4 @@
android:background="@drawable/ic_more_vert"
android:foreground="?android:attr/selectableItemBackgroundBorderless" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -329,6 +329,11 @@
android:name="com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog"
android:label="SongBottomSheetDialog"
tools:layout="@layout/bottom_sheet_song_dialog" />
<dialog
android:id="@+id/playlistBottomSheetDialog"
android:name="com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.PlaylistBottomSheetDialog"
android:label="PlaylistBottomSheetDialog"
tools:layout="@layout/bottom_sheet_playlist_dialog" />
<dialog
android:id="@+id/artistBottomSheetDialog"
android:name="com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.ArtistBottomSheetDialog"

View File

@@ -603,4 +603,7 @@
<string name="search_sort_title">Sort recent searches chronologically</string>
<string name="search_sort_summary">If enabled, sort searches chronologically. Sort by name if disabled.</string>
<string name="search_all_songs_loading">Getting all songs ...</string>
<string name="search_all_songs">all %1$s songs</string>
<string name="search_all_songs_play">Play %1$s</string>
</resources>