feat: folder navigation

This commit is contained in:
antonio 2023-05-27 11:57:59 +02:00
parent e85d7f9198
commit 24d2d201ad
29 changed files with 1238 additions and 9 deletions

View file

@ -39,4 +39,16 @@ public interface ClickCallback {
default void onInternetRadioStationClick(Bundle bundle) {} default void onInternetRadioStationClick(Bundle bundle) {}
default void onInternetRadioStationLongClick(Bundle bundle) {} default void onInternetRadioStationLongClick(Bundle bundle) {}
default void onMusicFolderClick(Bundle bundle) {}
default void onMusicFolderLongClick(Bundle bundle) {}
default void onMusicDirectoryClick(Bundle bundle) {}
default void onMusicDirectoryLongClick(Bundle bundle) {}
default void onMusicIndexClick(Bundle bundle) {}
default void onMusicIndexLongClick(Bundle bundle) {}
} }

View file

@ -0,0 +1,89 @@
package com.cappielloantonio.play.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.subsonic.base.ApiResponse;
import com.cappielloantonio.play.subsonic.models.Directory;
import com.cappielloantonio.play.subsonic.models.Indexes;
import com.cappielloantonio.play.subsonic.models.MusicFolder;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class DirectoryRepository {
private static final String TAG = "DirectoryRepository";
public MutableLiveData<List<MusicFolder>> getMusicFolders() {
MutableLiveData<List<MusicFolder>> liveMusicFolders = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getMusicFolders()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getMusicFolders() != null) {
liveMusicFolders.setValue(response.body().getSubsonicResponse().getMusicFolders().getMusicFolders());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return liveMusicFolders;
}
public MutableLiveData<Indexes> getIndexes(String musicFolderId, Long ifModifiedSince) {
MutableLiveData<Indexes> liveIndexes = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getIndexes(musicFolderId, ifModifiedSince)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) {
liveIndexes.setValue(response.body().getSubsonicResponse().getIndexes());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return liveIndexes;
}
public MutableLiveData<Directory> getMusicDirectory(String id) {
MutableLiveData<Directory> liveMusicDirectory = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getMusicDirectory(id)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null) {
liveMusicDirectory.setValue(response.body().getSubsonicResponse().getDirectory());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
t.printStackTrace();
}
});
return liveMusicDirectory;
}
}

View file

@ -24,9 +24,9 @@ public class BrowsingClient {
return browsingService.getMusicFolders(subsonic.getParams()); return browsingService.getMusicFolders(subsonic.getParams());
} }
public Call<ApiResponse> getIndexes() { public Call<ApiResponse> getIndexes(String musicFolderId, Long ifModifiedSince) {
Log.d(TAG, "getIndexes()"); Log.d(TAG, "getIndexes()");
return browsingService.getIndexes(subsonic.getParams()); return browsingService.getIndexes(subsonic.getParams(), musicFolderId, ifModifiedSince);
} }
public Call<ApiResponse> getMusicDirectory(String id) { public Call<ApiResponse> getMusicDirectory(String id) {

View file

@ -15,7 +15,7 @@ public interface BrowsingService {
Call<ApiResponse> getMusicFolders(@QueryMap Map<String, String> params); Call<ApiResponse> getMusicFolders(@QueryMap Map<String, String> params);
@GET("getIndexes") @GET("getIndexes")
Call<ApiResponse> getIndexes(@QueryMap Map<String, String> params); Call<ApiResponse> getIndexes(@QueryMap Map<String, String> params, @Query("musicFolderId") String musicFolderId, @Query("ifModifiedSince") Long ifModifiedSince);
@GET("getMusicDirectory") @GET("getMusicDirectory")
Call<ApiResponse> getMusicDirectory(@QueryMap Map<String, String> params, @Query("id") String id); Call<ApiResponse> getMusicDirectory(@QueryMap Map<String, String> params, @Query("id") String id);

View file

@ -1,8 +1,11 @@
package com.cappielloantonio.play.subsonic.models package com.cappielloantonio.play.subsonic.models
import java.util.* import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.util.Date
class Artist { @Parcelize
class Artist : Parcelable {
var id: String? = null var id: String? = null
var name: String? = null var name: String? = null
var starred: Date? = null var starred: Date? = null

View file

@ -1,10 +1,16 @@
package com.cappielloantonio.play.subsonic.models package com.cappielloantonio.play.subsonic.models
import java.util.* import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import java.util.Date
class Directory { @Parcelize
class Directory : Parcelable {
@SerializedName("child")
var children: List<Child>? = null var children: List<Child>? = null
var id: String? = null var id: String? = null
@SerializedName("parent")
var parentId: String? = null var parentId: String? = null
var name: String? = null var name: String? = null
var starred: Date? = null var starred: Date? = null

View file

@ -1,6 +1,9 @@
package com.cappielloantonio.play.subsonic.models package com.cappielloantonio.play.subsonic.models
import com.google.gson.annotations.SerializedName
class Index { class Index {
@SerializedName("artist")
var artists: List<Artist>? = null var artists: List<Artist>? = null
var name: String? = null var name: String? = null
} }

View file

@ -1,7 +1,10 @@
package com.cappielloantonio.play.subsonic.models package com.cappielloantonio.play.subsonic.models
import com.google.gson.annotations.SerializedName
class Indexes { class Indexes {
var shortcuts: List<Artist>? = null var shortcuts: List<Artist>? = null
@SerializedName("index")
var indices: List<Index>? = null var indices: List<Index>? = null
var children: List<Child>? = null var children: List<Child>? = null
var lastModified: Long = 0 var lastModified: Long = 0

View file

@ -1,6 +1,10 @@
package com.cappielloantonio.play.subsonic.models package com.cappielloantonio.play.subsonic.models
class MusicFolder { import android.os.Parcelable
var id = 0 import kotlinx.parcelize.Parcelize
@Parcelize
class MusicFolder : Parcelable {
var id: String? = null
var name: String? = null var name: String? = null
} }

View file

@ -1,5 +1,8 @@
package com.cappielloantonio.play.subsonic.models package com.cappielloantonio.play.subsonic.models
import com.google.gson.annotations.SerializedName
class MusicFolders { class MusicFolders {
@SerializedName("musicFolder")
var musicFolders: List<MusicFolder>? = null var musicFolders: List<MusicFolder>? = null
} }

View file

@ -0,0 +1,99 @@
package com.cappielloantonio.play.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.databinding.ItemLibraryMusicDirectoryBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.util.Constants;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@UnstableApi
public class MusicDirectoryAdapter extends RecyclerView.Adapter<MusicDirectoryAdapter.ViewHolder> {
private final ClickCallback click;
private List<Child> children;
public MusicDirectoryAdapter(ClickCallback click) {
this.click = click;
this.children = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemLibraryMusicDirectoryBinding view = ItemLibraryMusicDirectoryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Child child = children.get(position);
holder.item.musicDirectoryTitleTextView.setText(child.getTitle());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), child.getCoverArtId())
.build()
.into(holder.item.musicDirectoryCoverImageView);
}
@Override
public int getItemCount() {
return children.size();
}
public void setItems(List<Child> children) {
this.children = children != null ? children : Collections.emptyList();
notifyDataSetChanged();
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryMusicDirectoryBinding item;
ViewHolder(ItemLibraryMusicDirectoryBinding item) {
super(item.getRoot());
this.item = item;
item.musicDirectoryTitleTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.musicDirectoryMoreButton.setOnClickListener(v -> onLongClick());
}
public void onClick() {
Bundle bundle = new Bundle();
if (children.get(getBindingAdapterPosition()).isDir()) {
bundle.putParcelable(Constants.MUSIC_DIRECTORY_OBJECT, children.get(getBindingAdapterPosition()));
click.onMusicDirectoryClick(bundle);
} else {
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(children));
bundle.putInt(Constants.ITEM_POSITION, getBindingAdapterPosition());
click.onMediaClick(bundle);
}
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.MUSIC_DIRECTORY_OBJECT, children.get(getBindingAdapterPosition()));
click.onMusicDirectoryLongClick(bundle);
return true;
}
}
}

View file

@ -0,0 +1,95 @@
package com.cappielloantonio.play.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.databinding.ItemLibraryMusicFolderBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.MusicFolder;
import com.cappielloantonio.play.util.Constants;
import java.util.Collections;
import java.util.List;
@UnstableApi
public class MusicFolderAdapter extends RecyclerView.Adapter<MusicFolderAdapter.ViewHolder> {
private final ClickCallback click;
private List<MusicFolder> musicFolders;
public MusicFolderAdapter(ClickCallback click) {
this.click = click;
this.musicFolders = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemLibraryMusicFolderBinding view = ItemLibraryMusicFolderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
MusicFolder musicFolder = musicFolders.get(position);
holder.item.musicFolderTitleTextView.setText(musicFolder.getName());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), musicFolder.getName())
.build()
.into(holder.item.musicFolderCoverImageView);
}
@Override
public int getItemCount() {
return musicFolders.size();
}
public void setItems(List<MusicFolder> musicFolders) {
this.musicFolders = musicFolders;
notifyDataSetChanged();
}
public MusicFolder getItem(int position) {
return musicFolders.get(position);
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryMusicFolderBinding item;
ViewHolder(ItemLibraryMusicFolderBinding item) {
super(item.getRoot());
this.item = item;
item.musicFolderTitleTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.musicFolderMoreButton.setOnClickListener(v -> onLongClick());
}
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.MUSIC_FOLDER_OBJECT, musicFolders.get(getBindingAdapterPosition()));
click.onMusicFolderClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.MUSIC_FOLDER_OBJECT, musicFolders.get(getBindingAdapterPosition()));
click.onMusicFolderLongClick(bundle);
return true;
}
}
}

View file

@ -0,0 +1,91 @@
package com.cappielloantonio.play.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.databinding.ItemLibraryMusicIndexBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.Artist;
import com.cappielloantonio.play.util.Constants;
import java.util.Collections;
import java.util.List;
@UnstableApi
public class MusicIndexAdapter extends RecyclerView.Adapter<MusicIndexAdapter.ViewHolder> {
private final ClickCallback click;
private List<Artist> artists;
public MusicIndexAdapter(ClickCallback click) {
this.click = click;
this.artists = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemLibraryMusicIndexBinding view = ItemLibraryMusicIndexBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Artist artist = artists.get(position);
holder.item.musicIndexTitleTextView.setText(artist.getName());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), artist.getName())
.build()
.into(holder.item.musicIndexCoverImageView);
}
@Override
public int getItemCount() {
return artists.size();
}
public void setItems(List<Artist> artists) {
this.artists = artists;
notifyDataSetChanged();
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryMusicIndexBinding item;
ViewHolder(ItemLibraryMusicIndexBinding item) {
super(item.getRoot());
this.item = item;
item.musicIndexTitleTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.musicIndexMoreButton.setOnClickListener(v -> onLongClick());
}
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.MUSIC_INDEX_OBJECT, artists.get(getBindingAdapterPosition()));
click.onMusicIndexClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.MUSIC_INDEX_OBJECT, artists.get(getBindingAdapterPosition()));
click.onMusicIndexLongClick(bundle);
return true;
}
}
}

View file

@ -0,0 +1,178 @@
package com.cappielloantonio.play.ui.fragment;
import android.content.ComponentName;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.FragmentDirectoryBinding;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.service.MediaManager;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.subsonic.models.Artist;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.ui.adapter.MusicDirectoryAdapter;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.viewmodel.DirectoryViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Collections;
import java.util.Objects;
@UnstableApi
public class DirectoryFragment extends Fragment implements ClickCallback {
private static final String TAG = "DirectoryFragment";
private FragmentDirectoryBinding bind;
private MainActivity activity;
private DirectoryViewModel directoryViewModel;
private MusicDirectoryAdapter musicDirectoryAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentDirectoryBinding.inflate(inflater, container, false);
View view = bind.getRoot();
directoryViewModel = new ViewModelProvider(requireActivity()).get(DirectoryViewModel.class);
initAppBar();
initButtons();
initDirectoryListView();
init();
return view;
}
@Override
public void onStart() {
super.onStart();
initializeMediaBrowser();
}
@Override
public void onStop() {
releaseMediaBrowser();
super.onStop();
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void init() {
Artist artist = getArguments().getParcelable(Constants.MUSIC_INDEX_OBJECT);
if (artist != null) {
directoryViewModel.setMusicDirectoryId(artist.getId());
directoryViewModel.setMusicDirectoryName(artist.getName());
}
directoryViewModel.loadMusicDirectory(getViewLifecycleOwner());
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (bind != null)
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
if (bind != null)
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.directoryInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
bind.toolbar.setTitle(directory.getName());
}
});
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
bind.directoryTitleLabel.setText(directory.getName());
}
});
}
private void initButtons() {
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null && directory.getParentId() != null && !Objects.equals(directory.getParentId(), "-1")) {
bind.directoryBackImageView.setVisibility(View.VISIBLE);
} else {
bind.directoryBackImageView.setVisibility(View.GONE);
}
});
bind.directoryBackImageView.setOnClickListener(v -> directoryViewModel.goBack());
}
private void initDirectoryListView() {
bind.directoryRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.directoryRecyclerView.setHasFixedSize(true);
musicDirectoryAdapter = new MusicDirectoryAdapter(this);
bind.directoryRecyclerView.setAdapter(musicDirectoryAdapter);
directoryViewModel.getDirectory().observe(getViewLifecycleOwner(), directory -> {
if (directory != null) {
musicDirectoryAdapter.setItems(directory.getChildren());
} else {
musicDirectoryAdapter.setItems(Collections.emptyList());
}
});
}
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
private void releaseMediaBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
@Override
public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
}
@Override
public void onMusicDirectoryClick(Bundle bundle) {
Child child = bundle.getParcelable(Constants.MUSIC_DIRECTORY_OBJECT);
if (child != null) {
directoryViewModel.setMusicDirectoryId(child.getId());
directoryViewModel.setMusicDirectoryName(child.getTitle());
}
}
@Override
public void onMusicDirectoryLongClick(Bundle bundle) {
Toast.makeText(requireContext(), "Long click!", Toast.LENGTH_SHORT).show();
}
}

View file

@ -0,0 +1,111 @@
package com.cappielloantonio.play.ui.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.FragmentIndexBinding;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.MusicFolder;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.ui.adapter.MusicIndexAdapter;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.util.IndexUtil;
import com.cappielloantonio.play.viewmodel.IndexViewModel;
@UnstableApi
public class IndexFragment extends Fragment implements ClickCallback {
private static final String TAG = "IndexFragment";
private FragmentIndexBinding bind;
private MainActivity activity;
private IndexViewModel indexViewModel;
private MusicIndexAdapter musicIndexAdapter;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentIndexBinding.inflate(inflater, container, false);
View view = bind.getRoot();
indexViewModel = new ViewModelProvider(requireActivity()).get(IndexViewModel.class);
initAppBar();
initDirectoryListView();
init();
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void init() {
MusicFolder musicFolder = getArguments().getParcelable(Constants.MUSIC_FOLDER_OBJECT);
if (musicFolder != null) {
indexViewModel.setMusicFolder(musicFolder);
bind.indexTitleLabel.setText(musicFolder.getName());
}
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
if (bind != null)
bind.toolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
if (bind != null)
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.indexInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
bind.toolbar.setTitle(indexViewModel.getMusicFolderName());
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
}
private void initDirectoryListView() {
bind.indexRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.indexRecyclerView.setHasFixedSize(true);
musicIndexAdapter = new MusicIndexAdapter(this);
bind.indexRecyclerView.setAdapter(musicIndexAdapter);
indexViewModel.getIndexes().observe(getViewLifecycleOwner(), indexes -> {
if (indexes != null) {
musicIndexAdapter.setItems(IndexUtil.getArtist(indexes));
}
});
}
@Override
public void onMusicIndexClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.directoryFragment, bundle);
}
@Override
public void onMusicIndexLongClick(Bundle bundle) {
Toast.makeText(requireContext(), "Long click!", Toast.LENGTH_SHORT).show();
}
}

View file

@ -1,12 +1,14 @@
package com.cappielloantonio.play.ui.fragment; package com.cappielloantonio.play.ui.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -25,6 +27,7 @@ import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.ui.adapter.AlbumAdapter; import com.cappielloantonio.play.ui.adapter.AlbumAdapter;
import com.cappielloantonio.play.ui.adapter.ArtistAdapter; import com.cappielloantonio.play.ui.adapter.ArtistAdapter;
import com.cappielloantonio.play.ui.adapter.GenreAdapter; import com.cappielloantonio.play.ui.adapter.GenreAdapter;
import com.cappielloantonio.play.ui.adapter.MusicFolderAdapter;
import com.cappielloantonio.play.ui.adapter.PlaylistHorizontalAdapter; import com.cappielloantonio.play.ui.adapter.PlaylistHorizontalAdapter;
import com.cappielloantonio.play.ui.dialog.PlaylistEditorDialog; import com.cappielloantonio.play.ui.dialog.PlaylistEditorDialog;
import com.cappielloantonio.play.util.Constants; import com.cappielloantonio.play.util.Constants;
@ -35,10 +38,13 @@ import java.util.Objects;
@UnstableApi @UnstableApi
public class LibraryFragment extends Fragment implements ClickCallback { public class LibraryFragment extends Fragment implements ClickCallback {
private static final String TAG = "LibraryFragment";
private FragmentLibraryBinding bind; private FragmentLibraryBinding bind;
private MainActivity activity; private MainActivity activity;
private LibraryViewModel libraryViewModel; private LibraryViewModel libraryViewModel;
private MusicFolderAdapter musicFolderAdapter;
private AlbumAdapter albumAdapter; private AlbumAdapter albumAdapter;
private ArtistAdapter artistAdapter; private ArtistAdapter artistAdapter;
private GenreAdapter genreAdapter; private GenreAdapter genreAdapter;
@ -77,6 +83,7 @@ public class LibraryFragment extends Fragment implements ClickCallback {
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
initAppBar(); initAppBar();
initMusicFolderView();
initAlbumView(); initAlbumView();
initArtistView(); initArtistView();
initGenreView(); initGenreView();
@ -141,6 +148,28 @@ public class LibraryFragment extends Fragment implements ClickCallback {
Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null)); Objects.requireNonNull(bind.toolbar.getOverflowIcon()).setTint(requireContext().getResources().getColor(R.color.titleTextColor, null));
} }
private void initMusicFolderView() {
bind.musicFolderRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.musicFolderRecyclerView.setHasFixedSize(true);
musicFolderAdapter = new MusicFolderAdapter(this);
bind.musicFolderRecyclerView.setAdapter(musicFolderAdapter);
libraryViewModel.getMusicFolders(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), musicFolders -> {
if (musicFolders == null) {
if (bind != null)
bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.libraryMusicFolderSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.libraryMusicFolderPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.libraryMusicFolderSector.setVisibility(!musicFolders.isEmpty() ? View.VISIBLE : View.GONE);
musicFolderAdapter.setItems(musicFolders);
}
});
}
private void initAlbumView() { private void initAlbumView() {
bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
bind.albumRecyclerView.setHasFixedSize(true); bind.albumRecyclerView.setHasFixedSize(true);
@ -273,4 +302,14 @@ public class LibraryFragment extends Fragment implements ClickCallback {
dialog.setArguments(bundle); dialog.setArguments(bundle);
dialog.show(activity.getSupportFragmentManager(), null); dialog.show(activity.getSupportFragmentManager(), null);
} }
@Override
public void onMusicFolderClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.indexFragment, bundle);
}
@Override
public void onMusicFolderLongClick(Bundle bundle) {
Toast.makeText(requireContext(), "Long click!", Toast.LENGTH_SHORT).show();
}
} }

View file

@ -14,6 +14,9 @@ object Constants {
const val PODCAST_OBJECT = "PODCAST_OBJECT" const val PODCAST_OBJECT = "PODCAST_OBJECT"
const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT" const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_OBJECT"
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT" const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
const val MUSIC_FOLDER_OBJECT = "MUSIC_FOLDER_OBJECT"
const val MUSIC_DIRECTORY_OBJECT = "MUSIC_DIRECTORY_OBJECT"
const val MUSIC_INDEX_OBJECT = "MUSIC_DIRECTORY_OBJECT"
const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED" const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED"
const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED" const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED"

View file

@ -0,0 +1,29 @@
package com.cappielloantonio.play.util;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.play.subsonic.models.Artist;
import com.cappielloantonio.play.subsonic.models.Index;
import com.cappielloantonio.play.subsonic.models.Indexes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@OptIn(markerClass = UnstableApi.class)
public class IndexUtil {
public static List<Artist> getArtist(Indexes indexes) {
if (indexes.getIndices() == null) return Collections.emptyList();
ArrayList<Artist> toReturn = new ArrayList<>();
for (Index index : indexes.getIndices()) {
if (index.getArtists() != null) {
toReturn.addAll(index.getArtists());
}
}
return toReturn;
}
}

View file

@ -0,0 +1,47 @@
package com.cappielloantonio.play.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.repository.DirectoryRepository;
import com.cappielloantonio.play.subsonic.models.Directory;
public class DirectoryViewModel extends AndroidViewModel {
private final DirectoryRepository directoryRepository;
private MutableLiveData<String> id = new MutableLiveData<>(null);
private MutableLiveData<String> name = new MutableLiveData<>(null);
private MutableLiveData<Directory> directory = new MutableLiveData<>(null);
public DirectoryViewModel(@NonNull Application application) {
super(application);
directoryRepository = new DirectoryRepository();
}
public LiveData<Directory> getDirectory() {
return directory;
}
public void setMusicDirectoryId(String id) {
this.id.setValue(id);
}
public void setMusicDirectoryName(String name) {
this.name.setValue(name);
}
public void loadMusicDirectory(LifecycleOwner owner) {
this.id.observe(owner, id -> directoryRepository.getMusicDirectory(id).observe(owner, directory -> this.directory.setValue(directory)));
}
public void goBack() {
this.id.setValue(this.directory.getValue().getParentId());
}
}

View file

@ -0,0 +1,37 @@
package com.cappielloantonio.play.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.repository.DirectoryRepository;
import com.cappielloantonio.play.subsonic.models.Indexes;
import com.cappielloantonio.play.subsonic.models.MusicFolder;
public class IndexViewModel extends AndroidViewModel {
private final DirectoryRepository directoryRepository;
private MusicFolder musicFolder;
private MutableLiveData<Indexes> indexes = new MutableLiveData<>(null);
public IndexViewModel(@NonNull Application application) {
super(application);
directoryRepository = new DirectoryRepository();
}
public MutableLiveData<Indexes> getIndexes() {
return directoryRepository.getIndexes(null, null);
}
public String getMusicFolderName() {
return musicFolder != null ? musicFolder.getName() : "";
}
public void setMusicFolder(MusicFolder musicFolder) {
this.musicFolder = musicFolder;
}
}

View file

@ -10,11 +10,14 @@ import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.repository.AlbumRepository; import com.cappielloantonio.play.repository.AlbumRepository;
import com.cappielloantonio.play.repository.ArtistRepository; import com.cappielloantonio.play.repository.ArtistRepository;
import com.cappielloantonio.play.repository.DirectoryRepository;
import com.cappielloantonio.play.repository.GenreRepository; import com.cappielloantonio.play.repository.GenreRepository;
import com.cappielloantonio.play.repository.PlaylistRepository; import com.cappielloantonio.play.repository.PlaylistRepository;
import com.cappielloantonio.play.subsonic.models.AlbumID3; import com.cappielloantonio.play.subsonic.models.AlbumID3;
import com.cappielloantonio.play.subsonic.models.ArtistID3; import com.cappielloantonio.play.subsonic.models.ArtistID3;
import com.cappielloantonio.play.subsonic.models.Genre; import com.cappielloantonio.play.subsonic.models.Genre;
import com.cappielloantonio.play.subsonic.models.Indexes;
import com.cappielloantonio.play.subsonic.models.MusicFolder;
import com.cappielloantonio.play.subsonic.models.Playlist; import com.cappielloantonio.play.subsonic.models.Playlist;
import java.util.List; import java.util.List;
@ -22,11 +25,14 @@ import java.util.List;
public class LibraryViewModel extends AndroidViewModel { public class LibraryViewModel extends AndroidViewModel {
private static final String TAG = "LibraryViewModel"; private static final String TAG = "LibraryViewModel";
private final DirectoryRepository directoryRepository;
private final AlbumRepository albumRepository; private final AlbumRepository albumRepository;
private final ArtistRepository artistRepository; private final ArtistRepository artistRepository;
private final GenreRepository genreRepository; private final GenreRepository genreRepository;
private final PlaylistRepository playlistRepository; private final PlaylistRepository playlistRepository;
private final MutableLiveData<List<MusicFolder>> musicFolders = new MutableLiveData<>(null);
private final MutableLiveData<Indexes> indexes = new MutableLiveData<>(null);
private final MutableLiveData<List<Playlist>> playlistSample = new MutableLiveData<>(null); private final MutableLiveData<List<Playlist>> playlistSample = new MutableLiveData<>(null);
private final MutableLiveData<List<AlbumID3>> sampleAlbum = new MutableLiveData<>(null); private final MutableLiveData<List<AlbumID3>> sampleAlbum = new MutableLiveData<>(null);
private final MutableLiveData<List<ArtistID3>> sampleArtist = new MutableLiveData<>(null); private final MutableLiveData<List<ArtistID3>> sampleArtist = new MutableLiveData<>(null);
@ -35,12 +41,29 @@ public class LibraryViewModel extends AndroidViewModel {
public LibraryViewModel(@NonNull Application application) { public LibraryViewModel(@NonNull Application application) {
super(application); super(application);
directoryRepository = new DirectoryRepository();
albumRepository = new AlbumRepository(); albumRepository = new AlbumRepository();
artistRepository = new ArtistRepository(); artistRepository = new ArtistRepository();
genreRepository = new GenreRepository(); genreRepository = new GenreRepository();
playlistRepository = new PlaylistRepository(); playlistRepository = new PlaylistRepository();
} }
public LiveData<List<MusicFolder>> getMusicFolders(LifecycleOwner owner) {
if (musicFolders.getValue() == null) {
directoryRepository.getMusicFolders().observe(owner, musicFolders::postValue);
}
return musicFolders;
}
public LiveData<Indexes> getIndexes(LifecycleOwner owner) {
if (indexes.getValue() == null) {
directoryRepository.getIndexes("0", null).observe(owner, indexes::postValue);
}
return indexes;
}
public LiveData<List<AlbumID3>> getAlbumSample(LifecycleOwner owner) { public LiveData<List<AlbumID3>> getAlbumSample(LifecycleOwner owner) {
if (sampleAlbum.getValue() == null) { if (sampleAlbum.getValue() == null) {
albumRepository.getAlbums("random", 10, null, null).observe(owner, sampleAlbum::postValue); albumRepository.getAlbums("random", 10, null, null).observe(owner, sampleAlbum::postValue);

View file

@ -0,0 +1,76 @@
<?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="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/directory_info_sector"
android:layout_width="match_parent"
android:layout_height="172dp"
android:background="?attr/colorSurface"
android:clipChildren="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<TextView
android:id="@+id/directory_title_label"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="24dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/directory_back_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="16dp"
android:layout_marginBottom="12dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_arrow_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/directory_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -0,0 +1,59 @@
<?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="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/index_info_sector"
android:layout_width="match_parent"
android:layout_height="172dp"
android:background="?attr/colorSurface"
android:clipChildren="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<TextView
android:id="@+id/index_title_label"
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="24dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/index_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -52,6 +52,41 @@
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom"> android:paddingBottom="@dimen/global_padding_bottom">
<!-- Music Folder -->
<LinearLayout
android:id="@+id/library_music_folder_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:text="@string/library_title_music_folder" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/music_folder_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
<include
android:id="@+id/library_music_folder_placeholder"
layout="@layout/item_placeholder_album"
android:visibility="gone" />
<!-- Album --> <!-- Album -->
<LinearLayout <LinearLayout
android:id="@+id/library_album_sector" android:id="@+id/library_album_sector"
@ -67,6 +102,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingStart="8dp" android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"> android:paddingEnd="8dp">
<TextView <TextView

View file

@ -0,0 +1,56 @@
<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/music_directory_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" />
<View
android:id="@+id/cover_image_separator"
android:layout_width="12dp"
android:layout_height="52dp"
app:layout_constraintBottom_toBottomOf="@+id/music_directory_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/music_directory_title_text_view"
app:layout_constraintStart_toEndOf="@+id/music_directory_cover_image_view"
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
<TextView
android:id="@+id/music_directory_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="@id/music_directory_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/music_directory_more_button"
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
app:layout_constraintTop_toTopOf="@+id/music_directory_cover_image_view" />
<ImageView
android:id="@+id/music_directory_more_button"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/ic_more_vert"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/music_directory_title_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/music_directory_title_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,52 @@
<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">
<ImageView
android:id="@+id/music_folder_cover_image_view"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/cover_image_separator"
android:layout_width="12dp"
android:layout_height="52dp"
app:layout_constraintBottom_toBottomOf="@+id/music_folder_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/music_folder_title_text_view"
app:layout_constraintStart_toEndOf="@+id/music_folder_cover_image_view"
app:layout_constraintTop_toTopOf="@+id/music_folder_cover_image_view" />
<TextView
android:id="@+id/music_folder_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="@id/music_folder_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/music_folder_more_button"
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
app:layout_constraintTop_toTopOf="@+id/music_folder_cover_image_view" />
<ImageView
android:id="@+id/music_folder_more_button"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="@drawable/ic_more_vert"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/music_folder_title_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/music_folder_title_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,56 @@
<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/music_index_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" />
<View
android:id="@+id/cover_image_separator"
android:layout_width="12dp"
android:layout_height="52dp"
app:layout_constraintBottom_toBottomOf="@+id/music_index_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/music_index_title_text_view"
app:layout_constraintStart_toEndOf="@+id/music_index_cover_image_view"
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
<TextView
android:id="@+id/music_index_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="@id/music_index_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/music_index_more_button"
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
app:layout_constraintTop_toTopOf="@+id/music_index_cover_image_view" />
<ImageView
android:id="@+id/music_index_more_button"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/ic_more_vert"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/music_index_title_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/music_index_title_text_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -107,6 +107,9 @@
<action <action
android:id="@+id/action_libraryFragment_to_albumListPageFragment" android:id="@+id/action_libraryFragment_to_albumListPageFragment"
app:destination="@id/albumListPageFragment" /> app:destination="@id/albumListPageFragment" />
<action
android:id="@+id/action_libraryFragment_to_indexFragment"
app:destination="@id/indexFragment" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/downloadFragment" android:id="@+id/downloadFragment"
@ -279,6 +282,21 @@
android:name="com.cappielloantonio.play.ui.fragment.PodcastChannelPageFragment" android:name="com.cappielloantonio.play.ui.fragment.PodcastChannelPageFragment"
android:label="PodcastChannelPageFragment" android:label="PodcastChannelPageFragment"
tools:layout="@layout/fragment_podcast_channel_page" /> tools:layout="@layout/fragment_podcast_channel_page" />
<fragment
android:id="@+id/directoryFragment"
android:name="com.cappielloantonio.play.ui.fragment.DirectoryFragment"
android:label="DirectoryFragment"
tools:layout="@layout/fragment_directory" />
<fragment
android:id="@+id/indexFragment"
android:name="com.cappielloantonio.play.ui.fragment.IndexFragment"
android:label="IndexFragment"
tools:layout="@layout/fragment_index">
<action
android:id="@+id/action_indexFragment_to_directoryFragment"
app:destination="@id/directoryFragment" />
</fragment>
<dialog <dialog
android:id="@+id/songBottomSheetDialog" android:id="@+id/songBottomSheetDialog"
android:name="com.cappielloantonio.play.ui.fragment.bottomsheetdialog.SongBottomSheetDialog" android:name="com.cappielloantonio.play.ui.fragment.bottomsheetdialog.SongBottomSheetDialog"

View file

@ -83,6 +83,7 @@
<string name="home_title_podcast_channels_see_all_button">See all</string> <string name="home_title_podcast_channels_see_all_button">See all</string>
<string name="label_dot_separator"></string> <string name="label_dot_separator"></string>
<string name="label_placeholder">--</string> <string name="label_placeholder">--</string>
<string name="library_title_music_folder">Music folders</string>
<string name="library_title_album">Albums</string> <string name="library_title_album">Albums</string>
<string name="library_title_album_see_all_button">See all</string> <string name="library_title_album_see_all_button">See all</string>
<string name="library_title_artist">Artists</string> <string name="library_title_artist">Artists</string>