mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +00:00
implemented the playlist management logic
This commit is contained in:
parent
d2092f5239
commit
4adde9e951
13 changed files with 448 additions and 27 deletions
4
.idea/misc.xml
generated
4
.idea/misc.xml
generated
|
|
@ -22,10 +22,11 @@
|
||||||
<entry key="app/src/main/res/layout/bottom_sheet_song_dialog.xml" value="0.21666666666666667" />
|
<entry key="app/src/main/res/layout/bottom_sheet_song_dialog.xml" value="0.21666666666666667" />
|
||||||
<entry key="app/src/main/res/layout/dialog_playlist_chooser.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/dialog_playlist_chooser.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/dialog_playlist_creator.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/dialog_playlist_creator.xml" value="0.3229166666666667" />
|
||||||
|
<entry key="app/src/main/res/layout/dialog_playlist_editor.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/dialog_rating.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/dialog_rating.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/dialog_server_signup.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/dialog_server_signup.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/fragment_album_catalogue.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/fragment_album_catalogue.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/fragment_album_page.xml" value="0.8" />
|
<entry key="app/src/main/res/layout/fragment_album_page.xml" value="0.2769409038238702" />
|
||||||
<entry key="app/src/main/res/layout/fragment_artist_page.xml" value="0.1" />
|
<entry key="app/src/main/res/layout/fragment_artist_page.xml" value="0.1" />
|
||||||
<entry key="app/src/main/res/layout/fragment_home.xml" value="0.2212962962962963" />
|
<entry key="app/src/main/res/layout/fragment_home.xml" value="0.2212962962962963" />
|
||||||
<entry key="app/src/main/res/layout/fragment_login.xml" value="0.3166496424923391" />
|
<entry key="app/src/main/res/layout/fragment_login.xml" value="0.3166496424923391" />
|
||||||
|
|
@ -34,6 +35,7 @@
|
||||||
<entry key="app/src/main/res/layout/item_home_discover_song.xml" value="0.3166496424923391" />
|
<entry key="app/src/main/res/layout/item_home_discover_song.xml" value="0.3166496424923391" />
|
||||||
<entry key="app/src/main/res/layout/item_horizontal_album.xml" value="0.3166496424923391" />
|
<entry key="app/src/main/res/layout/item_horizontal_album.xml" value="0.3166496424923391" />
|
||||||
<entry key="app/src/main/res/layout/item_horizontal_playlist.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/item_horizontal_playlist.xml" value="0.3229166666666667" />
|
||||||
|
<entry key="app/src/main/res/layout/item_horizontal_playlist_dialog_track.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/item_horizontal_track.xml" value="4.0" />
|
<entry key="app/src/main/res/layout/item_horizontal_track.xml" value="4.0" />
|
||||||
<entry key="app/src/main/res/layout/item_library_album.xml" value="0.3229166666666667" />
|
<entry key="app/src/main/res/layout/item_library_album.xml" value="0.3229166666666667" />
|
||||||
<entry key="app/src/main/res/layout/item_login_server.xml" value="0.25" />
|
<entry key="app/src/main/res/layout/item_login_server.xml" value="0.25" />
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import com.cappielloantonio.play.R;
|
||||||
import com.cappielloantonio.play.glide.CustomGlideRequest;
|
import com.cappielloantonio.play.glide.CustomGlideRequest;
|
||||||
import com.cappielloantonio.play.model.Playlist;
|
import com.cappielloantonio.play.model.Playlist;
|
||||||
import com.cappielloantonio.play.ui.activity.MainActivity;
|
import com.cappielloantonio.play.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.play.ui.fragment.dialog.PlaylistEditorDialog;
|
||||||
import com.cappielloantonio.play.util.MusicUtil;
|
import com.cappielloantonio.play.util.MusicUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -70,7 +71,7 @@ public class PlaylistAdapter extends RecyclerView.Adapter<PlaylistAdapter.ViewHo
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||||
TextView textPlaylistName;
|
TextView textPlaylistName;
|
||||||
TextView textPlaylistSongCount;
|
TextView textPlaylistSongCount;
|
||||||
ImageView cover;
|
ImageView cover;
|
||||||
|
|
@ -83,6 +84,7 @@ public class PlaylistAdapter extends RecyclerView.Adapter<PlaylistAdapter.ViewHo
|
||||||
cover = itemView.findViewById(R.id.playlist_cover_image_view);
|
cover = itemView.findViewById(R.id.playlist_cover_image_view);
|
||||||
|
|
||||||
itemView.setOnClickListener(this);
|
itemView.setOnClickListener(this);
|
||||||
|
itemView.setOnLongClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -90,7 +92,18 @@ public class PlaylistAdapter extends RecyclerView.Adapter<PlaylistAdapter.ViewHo
|
||||||
Bundle bundle = new Bundle();
|
Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable("playlist_object", playlists.get(getBindingAdapterPosition()));
|
bundle.putParcelable("playlist_object", playlists.get(getBindingAdapterPosition()));
|
||||||
Navigation.findNavController(view).navigate(R.id.action_libraryFragment_to_playlistPageFragment, bundle);
|
Navigation.findNavController(view).navigate(R.id.action_libraryFragment_to_playlistPageFragment, bundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View view) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelable("playlist_object", playlists.get(getBindingAdapterPosition()));
|
||||||
|
|
||||||
|
PlaylistEditorDialog dialog = new PlaylistEditorDialog();
|
||||||
|
dialog.setArguments(bundle);
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
import com.cappielloantonio.play.R;
|
import com.cappielloantonio.play.R;
|
||||||
import com.cappielloantonio.play.glide.CustomGlideRequest;
|
import com.cappielloantonio.play.glide.CustomGlideRequest;
|
||||||
import com.cappielloantonio.play.model.Playlist;
|
import com.cappielloantonio.play.model.Playlist;
|
||||||
|
import com.cappielloantonio.play.model.Song;
|
||||||
import com.cappielloantonio.play.ui.activity.MainActivity;
|
import com.cappielloantonio.play.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.play.ui.fragment.dialog.PlaylistChooserDialog;
|
||||||
|
import com.cappielloantonio.play.ui.fragment.dialog.PlaylistEditorDialog;
|
||||||
import com.cappielloantonio.play.util.MusicUtil;
|
import com.cappielloantonio.play.util.MusicUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -108,7 +111,7 @@ public class PlaylistCatalogueAdapter extends RecyclerView.Adapter<PlaylistCatal
|
||||||
return filtering;
|
return filtering;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
|
||||||
TextView textPlaylistName;
|
TextView textPlaylistName;
|
||||||
ImageView cover;
|
ImageView cover;
|
||||||
|
|
||||||
|
|
@ -119,6 +122,7 @@ public class PlaylistCatalogueAdapter extends RecyclerView.Adapter<PlaylistCatal
|
||||||
cover = itemView.findViewById(R.id.playlist_cover_image_view);
|
cover = itemView.findViewById(R.id.playlist_cover_image_view);
|
||||||
|
|
||||||
itemView.setOnClickListener(this);
|
itemView.setOnClickListener(this);
|
||||||
|
itemView.setOnLongClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -130,5 +134,17 @@ public class PlaylistCatalogueAdapter extends RecyclerView.Adapter<PlaylistCatal
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onLongClick(View view) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putParcelable("playlist_object", playlists.get(getBindingAdapterPosition()));
|
||||||
|
|
||||||
|
PlaylistEditorDialog dialog = new PlaylistEditorDialog();
|
||||||
|
dialog.setArguments(bundle);
|
||||||
|
dialog.show(activity.getSupportFragmentManager(), null);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package com.cappielloantonio.play.adapter;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
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.fragment.app.FragmentManager;
|
||||||
|
import androidx.navigation.Navigation;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
|
import com.cappielloantonio.play.App;
|
||||||
|
import com.cappielloantonio.play.R;
|
||||||
|
import com.cappielloantonio.play.glide.CustomGlideRequest;
|
||||||
|
import com.cappielloantonio.play.model.Playlist;
|
||||||
|
import com.cappielloantonio.play.model.Song;
|
||||||
|
import com.cappielloantonio.play.repository.QueueRepository;
|
||||||
|
import com.cappielloantonio.play.service.MusicPlayerRemote;
|
||||||
|
import com.cappielloantonio.play.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.play.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.play.util.MusicUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PlaylistDialogSongHorizontalAdapter extends RecyclerView.Adapter<PlaylistDialogSongHorizontalAdapter.ViewHolder> {
|
||||||
|
private static final String TAG = "PlaylistDialogSongHorizontalAdapter";
|
||||||
|
|
||||||
|
private List<Song> songs;
|
||||||
|
private LayoutInflater mInflater;
|
||||||
|
private MainActivity mainActivity;
|
||||||
|
private Context context;
|
||||||
|
private FragmentManager fragmentManager;
|
||||||
|
|
||||||
|
public PlaylistDialogSongHorizontalAdapter(MainActivity mainActivity, Context context, FragmentManager fragmentManager) {
|
||||||
|
this.mainActivity = mainActivity;
|
||||||
|
this.context = context;
|
||||||
|
this.fragmentManager = fragmentManager;
|
||||||
|
this.mInflater = LayoutInflater.from(context);
|
||||||
|
this.songs = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
View view = mInflater.inflate(R.layout.item_horizontal_playlist_dialog_track, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
Song song = songs.get(position);
|
||||||
|
|
||||||
|
holder.songTitle.setText(MusicUtil.getReadableString(song.getTitle()));
|
||||||
|
holder.songArtist.setText(MusicUtil.getReadableString(song.getArtistName()));
|
||||||
|
holder.songDuration.setText(MusicUtil.getReadableDurationString(song.getDuration(), false));
|
||||||
|
|
||||||
|
CustomGlideRequest.Builder
|
||||||
|
.from(context, song.getPrimary(), CustomGlideRequest.SONG_PIC, null)
|
||||||
|
.build()
|
||||||
|
.transform(new RoundedCorners(CustomGlideRequest.CORNER_RADIUS))
|
||||||
|
.into(holder.cover);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return songs.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Song> getItems() {
|
||||||
|
return this.songs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<Song> songs) {
|
||||||
|
this.songs = songs;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Song getItem(int id) {
|
||||||
|
return songs.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView songTitle;
|
||||||
|
TextView songArtist;
|
||||||
|
TextView songDuration;
|
||||||
|
ImageView cover;
|
||||||
|
|
||||||
|
ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
songTitle = itemView.findViewById(R.id.playlist_dialog_song_title_text_view);
|
||||||
|
songArtist = itemView.findViewById(R.id.playlist_dialog_album_artist_text_view);
|
||||||
|
songDuration = itemView.findViewById(R.id.playlist_dialog_song_duration_text_view);
|
||||||
|
cover = itemView.findViewById(R.id.playlist_dialog_song_cover_image_view);
|
||||||
|
|
||||||
|
songTitle.setSelected(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -78,10 +78,10 @@ public class PlaylistRepository {
|
||||||
return listLivePlaylistSongs;
|
return listLivePlaylistSongs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId, String songId) {
|
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(application, false)
|
App.getSubsonicClientInstance(application, false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.updatePlaylist(playlistId, null, true, songId, null)
|
.updatePlaylist(playlistId, null, true, songsId, null)
|
||||||
.enqueue(new Callback<SubsonicResponse>() {
|
.enqueue(new Callback<SubsonicResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
|
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
|
||||||
|
|
@ -97,10 +97,48 @@ public class PlaylistRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createPlaylist(String name, String songId) {
|
public void createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
App.getSubsonicClientInstance(application, false)
|
App.getSubsonicClientInstance(application, false)
|
||||||
.getPlaylistClient()
|
.getPlaylistClient()
|
||||||
.createPlaylist(null, name, songId)
|
.createPlaylist(playlistId, name, songsId)
|
||||||
|
.enqueue(new Callback<SubsonicResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
|
||||||
|
if (response.body().getStatus().getValue().equals(ResponseStatus.OK)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<SubsonicResponse> call, Throwable t) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList<String> songIdToAdd, ArrayList<Integer> songIndexToRemove) {
|
||||||
|
App.getSubsonicClientInstance(application, false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.updatePlaylist(playlistId, name, isPublic, songIdToAdd, songIndexToRemove)
|
||||||
|
.enqueue(new Callback<SubsonicResponse>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
|
||||||
|
if (response.body().getStatus().getValue().equals(ResponseStatus.OK)) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<SubsonicResponse> call, Throwable t) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePlaylist(String playlistId) {
|
||||||
|
App.getSubsonicClientInstance(application, false)
|
||||||
|
.getPlaylistClient()
|
||||||
|
.deletePlaylist(playlistId)
|
||||||
.enqueue(new Callback<SubsonicResponse>() {
|
.enqueue(new Callback<SubsonicResponse>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
|
public void onResponse(Call<SubsonicResponse> call, Response<SubsonicResponse> response) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import com.cappielloantonio.play.subsonic.Subsonic;
|
||||||
import com.cappielloantonio.play.subsonic.models.SubsonicResponse;
|
import com.cappielloantonio.play.subsonic.models.SubsonicResponse;
|
||||||
import com.tickaroo.tikxml.retrofit.TikXmlConverterFactory;
|
import com.tickaroo.tikxml.retrofit.TikXmlConverterFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.logging.HttpLoggingInterceptor;
|
import okhttp3.logging.HttpLoggingInterceptor;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
|
|
@ -40,12 +42,12 @@ public class PlaylistClient {
|
||||||
return playlistService.getPlaylist(subsonic.getParams(), id);
|
return playlistService.getPlaylist(subsonic.getParams(), id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Call<SubsonicResponse> createPlaylist(String playlistId, String name, String songId) {
|
public Call<SubsonicResponse> createPlaylist(String playlistId, String name, ArrayList<String> songsId) {
|
||||||
Log.d(TAG, "createPlaylist()");
|
Log.d(TAG, "createPlaylist()");
|
||||||
return playlistService.createPlaylist(subsonic.getParams(), playlistId, name, songId);
|
return playlistService.createPlaylist(subsonic.getParams(), playlistId, name, songsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Call<SubsonicResponse> updatePlaylist(String playlistId, String name, boolean isPublic, String songIdToAdd, String songIndexToRemove) {
|
public Call<SubsonicResponse> updatePlaylist(String playlistId, String name, boolean isPublic, ArrayList<String> songIdToAdd, ArrayList<Integer> songIndexToRemove) {
|
||||||
Log.d(TAG, "updatePlaylist()");
|
Log.d(TAG, "updatePlaylist()");
|
||||||
return playlistService.updatePlaylist(subsonic.getParams(), playlistId, name, isPublic, songIdToAdd, songIndexToRemove);
|
return playlistService.updatePlaylist(subsonic.getParams(), playlistId, name, isPublic, songIdToAdd, songIndexToRemove);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.cappielloantonio.play.subsonic.api.playlist;
|
||||||
|
|
||||||
import com.cappielloantonio.play.subsonic.models.SubsonicResponse;
|
import com.cappielloantonio.play.subsonic.models.SubsonicResponse;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
|
|
@ -17,10 +18,10 @@ public interface PlaylistService {
|
||||||
Call<SubsonicResponse> getPlaylist(@QueryMap Map<String, String> params, @Query("id") String id);
|
Call<SubsonicResponse> getPlaylist(@QueryMap Map<String, String> params, @Query("id") String id);
|
||||||
|
|
||||||
@GET("createPlaylist")
|
@GET("createPlaylist")
|
||||||
Call<SubsonicResponse> createPlaylist(@QueryMap Map<String, String> params, @Query("playlistId") String playlistId, @Query("name") String name, @Query("songId") String songId);
|
Call<SubsonicResponse> createPlaylist(@QueryMap Map<String, String> params, @Query("playlistId") String playlistId, @Query("name") String name, @Query("songId") ArrayList<String> songsId);
|
||||||
|
|
||||||
@GET("updatePlaylist")
|
@GET("updatePlaylist")
|
||||||
Call<SubsonicResponse> updatePlaylist(@QueryMap Map<String, String> params, @Query("playlistId") String playlistId, @Query("name") String name, @Query("public") boolean isPublic, @Query("songIdToAdd") String songIdToAdd, @Query("songIndexToRemove") String songIndexToRemove);
|
Call<SubsonicResponse> updatePlaylist(@QueryMap Map<String, String> params, @Query("playlistId") String playlistId, @Query("name") String name, @Query("public") boolean isPublic, @Query("songIdToAdd") ArrayList<String> songIdToAdd, @Query("songIndexToRemove") ArrayList<Integer> songIndexToRemove);
|
||||||
|
|
||||||
@GET("deletePlaylist")
|
@GET("deletePlaylist")
|
||||||
Call<SubsonicResponse> deletePlaylist(@QueryMap Map<String, String> params, @Query("id") String id);
|
Call<SubsonicResponse> deletePlaylist(@QueryMap Map<String, String> params, @Query("id") String id);
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,21 @@ import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.cappielloantonio.play.R;
|
import com.cappielloantonio.play.R;
|
||||||
|
import com.cappielloantonio.play.adapter.PlaylistDialogSongHorizontalAdapter;
|
||||||
import com.cappielloantonio.play.databinding.DialogPlaylistEditorBinding;
|
import com.cappielloantonio.play.databinding.DialogPlaylistEditorBinding;
|
||||||
import com.cappielloantonio.play.ui.activity.MainActivity;
|
import com.cappielloantonio.play.ui.activity.MainActivity;
|
||||||
|
import com.cappielloantonio.play.util.MusicUtil;
|
||||||
import com.cappielloantonio.play.viewmodel.PlaylistEditorViewModel;
|
import com.cappielloantonio.play.viewmodel.PlaylistEditorViewModel;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class PlaylistEditorDialog extends DialogFragment {
|
public class PlaylistEditorDialog extends DialogFragment {
|
||||||
|
|
@ -26,6 +33,7 @@ public class PlaylistEditorDialog extends DialogFragment {
|
||||||
private PlaylistEditorViewModel playlistEditorViewModel;
|
private PlaylistEditorViewModel playlistEditorViewModel;
|
||||||
|
|
||||||
private String playlistName;
|
private String playlistName;
|
||||||
|
private PlaylistDialogSongHorizontalAdapter playlistDialogSongHorizontalAdapter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
|
|
@ -41,6 +49,7 @@ public class PlaylistEditorDialog extends DialogFragment {
|
||||||
.setTitle("Create playlist")
|
.setTitle("Create playlist")
|
||||||
.setPositiveButton("Save", (dialog, id) -> {
|
.setPositiveButton("Save", (dialog, id) -> {
|
||||||
})
|
})
|
||||||
|
.setNeutralButton("Delete", (dialog, id) -> dialog.cancel())
|
||||||
.setNegativeButton("Cancel", (dialog, id) -> dialog.cancel());
|
.setNegativeButton("Cancel", (dialog, id) -> dialog.cancel());
|
||||||
|
|
||||||
return builder.create();
|
return builder.create();
|
||||||
|
|
@ -50,15 +59,28 @@ public class PlaylistEditorDialog extends DialogFragment {
|
||||||
public void onStart() {
|
public void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
setSongInfo();
|
setParameterInfo();
|
||||||
setButtonAction();
|
setButtonAction();
|
||||||
|
initSongsView();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setSongInfo() {
|
private void setParameterInfo() {
|
||||||
if (getArguments() != null) {
|
if (getArguments() != null) {
|
||||||
playlistEditorViewModel.setSongToAdd(getArguments().getParcelable("song_object"));
|
if (getArguments().getParcelable("song_object") != null) {
|
||||||
|
playlistEditorViewModel.setSongToAdd(getArguments().getParcelable("song_object"));
|
||||||
|
playlistEditorViewModel.setPlaylistToEdit(null);
|
||||||
|
}
|
||||||
|
else if (getArguments().getParcelable("playlist_object") != null) {
|
||||||
|
playlistEditorViewModel.setSongToAdd(null);
|
||||||
|
playlistEditorViewModel.setPlaylistToEdit(getArguments().getParcelable("playlist_object"));
|
||||||
|
|
||||||
|
if (playlistEditorViewModel.getPlaylistToEdit() != null) {
|
||||||
|
bind.playlistNameTextView.setText(MusicUtil.getReadableString(playlistEditorViewModel.getPlaylistToEdit().getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
playlistEditorViewModel.setSongToAdd(null);
|
playlistEditorViewModel.setSongToAdd(null);
|
||||||
|
playlistEditorViewModel.setPlaylistToEdit(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,10 +91,74 @@ public class PlaylistEditorDialog extends DialogFragment {
|
||||||
|
|
||||||
((AlertDialog) Objects.requireNonNull(getDialog())).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
((AlertDialog) Objects.requireNonNull(getDialog())).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
|
||||||
if (validateInput()) {
|
if (validateInput()) {
|
||||||
playlistEditorViewModel.createPlaylist(playlistName);
|
if (playlistEditorViewModel.getSongToAdd() != null) {
|
||||||
|
playlistEditorViewModel.createPlaylist(playlistName);
|
||||||
|
} else if (playlistEditorViewModel.getPlaylistToEdit() != null) {
|
||||||
|
playlistEditorViewModel.updatePlaylist(playlistName);
|
||||||
|
}
|
||||||
|
|
||||||
Objects.requireNonNull(getDialog()).dismiss();
|
Objects.requireNonNull(getDialog()).dismiss();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
((AlertDialog) Objects.requireNonNull(getDialog())).getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
|
||||||
|
playlistEditorViewModel.deletePlaylist();
|
||||||
|
Objects.requireNonNull(getDialog()).dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSongsView() {
|
||||||
|
bind.playlistSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||||
|
bind.playlistSongRecyclerView.setHasFixedSize(true);
|
||||||
|
|
||||||
|
playlistDialogSongHorizontalAdapter = new PlaylistDialogSongHorizontalAdapter(activity, requireContext(), getChildFragmentManager());
|
||||||
|
bind.playlistSongRecyclerView.setAdapter(playlistDialogSongHorizontalAdapter);
|
||||||
|
|
||||||
|
playlistEditorViewModel.getPlaylistSongLiveList().observe(requireActivity(), songs -> {
|
||||||
|
playlistDialogSongHorizontalAdapter.setItems(songs);
|
||||||
|
});
|
||||||
|
|
||||||
|
new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT) {
|
||||||
|
int originalPosition = -1;
|
||||||
|
int fromPosition = -1;
|
||||||
|
int toPosition = -1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
|
||||||
|
if (originalPosition == -1)
|
||||||
|
originalPosition = viewHolder.getBindingAdapterPosition();
|
||||||
|
|
||||||
|
fromPosition = viewHolder.getBindingAdapterPosition();
|
||||||
|
toPosition = target.getBindingAdapterPosition();
|
||||||
|
|
||||||
|
Collections.swap(playlistDialogSongHorizontalAdapter.getItems(), fromPosition, toPosition);
|
||||||
|
recyclerView.getAdapter().notifyItemMoved(fromPosition, toPosition);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||||
|
super.clearView(recyclerView, viewHolder);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Qui vado a riscivere tutta la table Queue, quando teoricamente potrei solo swappare l'ordine degli elementi interessati
|
||||||
|
* Nel caso la coda contenesse parecchi brani, potrebbero verificarsi rallentamenti pesanti
|
||||||
|
*/
|
||||||
|
playlistEditorViewModel.orderPlaylistSongLiveListAfterSwap(playlistDialogSongHorizontalAdapter.getItems());
|
||||||
|
|
||||||
|
originalPosition = -1;
|
||||||
|
fromPosition = -1;
|
||||||
|
toPosition = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||||
|
playlistEditorViewModel.removeFromPlaylistSongLiveList(playlistDialogSongHorizontalAdapter.getItem(viewHolder.getBindingAdapterPosition()));
|
||||||
|
bind.playlistSongRecyclerView.getAdapter().notifyItemRemoved(viewHolder.getBindingAdapterPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).attachToRecyclerView(bind.playlistSongRecyclerView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateInput() {
|
private boolean validateInput() {
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,9 @@ public class PlaylistCatalogueViewModel extends AndroidViewModel {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
||||||
playlistRepository = new PlaylistRepository(application);
|
playlistRepository = new PlaylistRepository(application);
|
||||||
|
|
||||||
playlists = playlistRepository.getPlaylists(false, -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<Playlist>> getPlaylistList() {
|
public LiveData<List<Playlist>> getPlaylistList() {
|
||||||
return playlists;
|
return playlistRepository.getPlaylists(false, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import com.cappielloantonio.play.model.Playlist;
|
||||||
import com.cappielloantonio.play.model.Song;
|
import com.cappielloantonio.play.model.Song;
|
||||||
import com.cappielloantonio.play.repository.PlaylistRepository;
|
import com.cappielloantonio.play.repository.PlaylistRepository;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PlaylistChooserViewModel extends AndroidViewModel {
|
public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
|
|
@ -32,7 +34,7 @@ public class PlaylistChooserViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addSongToPlaylist(String playlistId) {
|
public void addSongToPlaylist(String playlistId) {
|
||||||
playlistRepository.addSongToPlaylist(playlistId, toAdd.getId());
|
playlistRepository.addSongToPlaylist(playlistId, new ArrayList(Collections.singletonList(toAdd.getId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSongToAdd(Song song) {
|
public void setSongToAdd(Song song) {
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,97 @@
|
||||||
package com.cappielloantonio.play.viewmodel;
|
package com.cappielloantonio.play.viewmodel;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
|
||||||
import com.cappielloantonio.play.model.Server;
|
import com.cappielloantonio.play.model.Playlist;
|
||||||
import com.cappielloantonio.play.model.Song;
|
import com.cappielloantonio.play.model.Song;
|
||||||
import com.cappielloantonio.play.repository.PlaylistRepository;
|
import com.cappielloantonio.play.repository.PlaylistRepository;
|
||||||
import com.cappielloantonio.play.repository.ServerRepository;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class PlaylistEditorViewModel extends AndroidViewModel {
|
public class PlaylistEditorViewModel extends AndroidViewModel {
|
||||||
|
private static final String TAG = "PlaylistEditorViewModel";
|
||||||
|
|
||||||
private PlaylistRepository playlistRepository;
|
private PlaylistRepository playlistRepository;
|
||||||
|
|
||||||
private Song toAdd;
|
private Song toAdd;
|
||||||
|
private Playlist toEdit;
|
||||||
|
|
||||||
|
private MutableLiveData<List<Song>> songLiveList = new MutableLiveData<>();
|
||||||
|
|
||||||
public PlaylistEditorViewModel(@NonNull Application application) {
|
public PlaylistEditorViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
|
|
||||||
playlistRepository = new PlaylistRepository(application);
|
playlistRepository = new PlaylistRepository(application);
|
||||||
|
|
||||||
|
Log.d(TAG, "PlaylistEditorViewModel()");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createPlaylist(String name) {
|
public void createPlaylist(String name) {
|
||||||
playlistRepository.createPlaylist(name, toAdd.getId());
|
playlistRepository.createPlaylist(null, name, new ArrayList(Collections.singletonList(toAdd.getId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updatePlaylist(String playlistId) {
|
public void updatePlaylist(String name) {
|
||||||
|
playlistRepository.deletePlaylist(toEdit.getId());
|
||||||
|
playlistRepository.createPlaylist(toEdit.getId(), name, getPlaylistSongIds());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePlaylist() {
|
||||||
|
if (toEdit != null) playlistRepository.deletePlaylist(toEdit.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Song getSongToAdd() {
|
||||||
|
return toAdd;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSongToAdd(Song song) {
|
public void setSongToAdd(Song song) {
|
||||||
toAdd = song;
|
this.toAdd = song;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Playlist getPlaylistToEdit() {
|
||||||
|
return toEdit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaylistToEdit(Playlist playlist) {
|
||||||
|
this.toEdit = playlist;
|
||||||
|
|
||||||
|
if (playlist != null) {
|
||||||
|
this.songLiveList = playlistRepository.getPlaylistSongs(toEdit.getId());
|
||||||
|
} else {
|
||||||
|
this.songLiveList = new MutableLiveData<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<Song>> getPlaylistSongLiveList() {
|
||||||
|
return songLiveList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFromPlaylistSongLiveList(Song song) {
|
||||||
|
List<Song> songs = songLiveList.getValue();
|
||||||
|
Objects.requireNonNull(songs).remove(song);
|
||||||
|
songLiveList.postValue(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void orderPlaylistSongLiveListAfterSwap(List<Song> songs) {
|
||||||
|
songLiveList.postValue(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<String> getPlaylistSongIds() {
|
||||||
|
List<Song> songs = songLiveList.getValue();
|
||||||
|
ArrayList<String> ids = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Song song : songs) {
|
||||||
|
ids.add(song.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@
|
||||||
android:hint="Playlist Name"
|
android:hint="Playlist Name"
|
||||||
android:inputType="textNoSuggestions"
|
android:inputType="textNoSuggestions"
|
||||||
android:textCursorDrawable="@null" />
|
android:textCursorDrawable="@null" />
|
||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/playlist_song_recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:nestedScrollingEnabled="false"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:paddingEnd="12dp" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:background="?attr/selectableItemBackground"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingTop="3dp"
|
||||||
|
android:paddingBottom="3dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/playlist_dialog_song_cover_image_view"
|
||||||
|
android:layout_width="52dp"
|
||||||
|
android:layout_height="52dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_margin="2dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playlist_dialog_song_title_text_view"
|
||||||
|
style="@style/ItemTitleTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:focusable="true"
|
||||||
|
android:focusableInTouchMode="true"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingTop="10dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:scrollHorizontally="true"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:text="@string/label_placeholder"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/playlist_dialog_song_handle_button"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/playlist_dialog_song_cover_image_view"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/playlist_dialog_song_handle_button"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/playlist_dialog_song_cover_image_view"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/playlist_dialog_song_title_text_view">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playlist_dialog_album_artist_text_view"
|
||||||
|
style="@style/ItemSubtitleTextView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingStart="12dp"
|
||||||
|
android:paddingEnd="4dp"
|
||||||
|
android:text="@string/label_placeholder" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/queue_separator_text_view"
|
||||||
|
style="@style/ItemSubtitleTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/label_dot_separator" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playlist_dialog_song_duration_text_view"
|
||||||
|
style="@style/ItemSubtitleTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingEnd="12dp"
|
||||||
|
android:text="@string/label_placeholder" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/playlist_dialog_song_handle_button"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/ic_drag_handle"
|
||||||
|
android:foreground="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue