diff --git a/app/src/main/java/com/cappielloantonio/play/interfaces/ClickCallback.java b/app/src/main/java/com/cappielloantonio/play/interfaces/ClickCallback.java index cbb47b6a..99c8abf0 100644 --- a/app/src/main/java/com/cappielloantonio/play/interfaces/ClickCallback.java +++ b/app/src/main/java/com/cappielloantonio/play/interfaces/ClickCallback.java @@ -39,4 +39,16 @@ public interface ClickCallback { default void onInternetRadioStationClick(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) {} } diff --git a/app/src/main/java/com/cappielloantonio/play/repository/DirectoryRepository.java b/app/src/main/java/com/cappielloantonio/play/repository/DirectoryRepository.java new file mode 100644 index 00000000..8a366cd6 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/repository/DirectoryRepository.java @@ -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> getMusicFolders() { + MutableLiveData> liveMusicFolders = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicFolders() + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response 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 call, @NonNull Throwable t) { + + } + }); + + return liveMusicFolders; + } + + public MutableLiveData getIndexes(String musicFolderId, Long ifModifiedSince) { + MutableLiveData liveIndexes = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getIndexes(musicFolderId, ifModifiedSince) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getIndexes() != null) { + liveIndexes.setValue(response.body().getSubsonicResponse().getIndexes()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + } + }); + + return liveIndexes; + } + + public MutableLiveData getMusicDirectory(String id) { + MutableLiveData liveMusicDirectory = new MutableLiveData<>(); + + App.getSubsonicClientInstance(false) + .getBrowsingClient() + .getMusicDirectory(id) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getDirectory() != null) { + liveMusicDirectory.setValue(response.body().getSubsonicResponse().getDirectory()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + t.printStackTrace(); + } + }); + + return liveMusicDirectory; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingClient.java b/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingClient.java index 5114362a..ae269674 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingClient.java +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingClient.java @@ -24,9 +24,9 @@ public class BrowsingClient { return browsingService.getMusicFolders(subsonic.getParams()); } - public Call getIndexes() { + public Call getIndexes(String musicFolderId, Long ifModifiedSince) { Log.d(TAG, "getIndexes()"); - return browsingService.getIndexes(subsonic.getParams()); + return browsingService.getIndexes(subsonic.getParams(), musicFolderId, ifModifiedSince); } public Call getMusicDirectory(String id) { diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingService.java b/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingService.java index 10ebbe4d..f9db93b8 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingService.java +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/api/browsing/BrowsingService.java @@ -15,7 +15,7 @@ public interface BrowsingService { Call getMusicFolders(@QueryMap Map params); @GET("getIndexes") - Call getIndexes(@QueryMap Map params); + Call getIndexes(@QueryMap Map params, @Query("musicFolderId") String musicFolderId, @Query("ifModifiedSince") Long ifModifiedSince); @GET("getMusicDirectory") Call getMusicDirectory(@QueryMap Map params, @Query("id") String id); diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Artist.kt b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Artist.kt index ca1c34fe..01763365 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Artist.kt +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Artist.kt @@ -1,8 +1,11 @@ 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 name: String? = null var starred: Date? = null diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Directory.kt b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Directory.kt index 630c6754..6967caf5 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Directory.kt +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Directory.kt @@ -1,10 +1,16 @@ 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? = null var id: String? = null + @SerializedName("parent") var parentId: String? = null var name: String? = null var starred: Date? = null diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Index.kt b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Index.kt index f92a133c..e4bc92d5 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Index.kt +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Index.kt @@ -1,6 +1,9 @@ package com.cappielloantonio.play.subsonic.models +import com.google.gson.annotations.SerializedName + class Index { + @SerializedName("artist") var artists: List? = null var name: String? = null } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Indexes.kt b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Indexes.kt index 9533356c..10900731 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/models/Indexes.kt +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/models/Indexes.kt @@ -1,7 +1,10 @@ package com.cappielloantonio.play.subsonic.models +import com.google.gson.annotations.SerializedName + class Indexes { var shortcuts: List? = null + @SerializedName("index") var indices: List? = null var children: List? = null var lastModified: Long = 0 diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolder.kt b/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolder.kt index 469a2a39..4f52320e 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolder.kt +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolder.kt @@ -1,6 +1,10 @@ package com.cappielloantonio.play.subsonic.models -class MusicFolder { - var id = 0 +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class MusicFolder : Parcelable { + var id: String? = null var name: String? = null } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolders.kt b/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolders.kt index 50f372c9..a70342ae 100644 --- a/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolders.kt +++ b/app/src/main/java/com/cappielloantonio/play/subsonic/models/MusicFolders.kt @@ -1,5 +1,8 @@ package com.cappielloantonio.play.subsonic.models +import com.google.gson.annotations.SerializedName + class MusicFolders { + @SerializedName("musicFolder") var musicFolders: List? = null } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicDirectoryAdapter.java b/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicDirectoryAdapter.java new file mode 100644 index 00000000..614da24c --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicDirectoryAdapter.java @@ -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 { + private final ClickCallback click; + + private List 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 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; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicFolderAdapter.java b/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicFolderAdapter.java new file mode 100644 index 00000000..523c246f --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicFolderAdapter.java @@ -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 { + private final ClickCallback click; + + private List 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 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; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicIndexAdapter.java b/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicIndexAdapter.java new file mode 100644 index 00000000..4555bda7 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/adapter/MusicIndexAdapter.java @@ -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 { + private final ClickCallback click; + + private List 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 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; + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/DirectoryFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/DirectoryFragment.java new file mode 100644 index 00000000..7453fc44 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/DirectoryFragment.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/IndexFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/IndexFragment.java new file mode 100644 index 00000000..eb6699ac --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/IndexFragment.java @@ -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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java index 9c9610b8..fb582bf4 100644 --- a/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java +++ b/app/src/main/java/com/cappielloantonio/play/ui/fragment/LibraryFragment.java @@ -1,12 +1,14 @@ package com.cappielloantonio.play.ui.fragment; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; 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.ArtistAdapter; 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.dialog.PlaylistEditorDialog; import com.cappielloantonio.play.util.Constants; @@ -35,10 +38,13 @@ import java.util.Objects; @UnstableApi public class LibraryFragment extends Fragment implements ClickCallback { + private static final String TAG = "LibraryFragment"; + private FragmentLibraryBinding bind; private MainActivity activity; private LibraryViewModel libraryViewModel; + private MusicFolderAdapter musicFolderAdapter; private AlbumAdapter albumAdapter; private ArtistAdapter artistAdapter; private GenreAdapter genreAdapter; @@ -77,6 +83,7 @@ public class LibraryFragment extends Fragment implements ClickCallback { super.onViewCreated(view, savedInstanceState); initAppBar(); + initMusicFolderView(); initAlbumView(); initArtistView(); 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)); } + 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() { bind.albumRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)); bind.albumRecyclerView.setHasFixedSize(true); @@ -273,4 +302,14 @@ public class LibraryFragment extends Fragment implements ClickCallback { dialog.setArguments(bundle); 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(); + } } diff --git a/app/src/main/java/com/cappielloantonio/play/util/Constants.kt b/app/src/main/java/com/cappielloantonio/play/util/Constants.kt index 9e3d049f..0093caf4 100644 --- a/app/src/main/java/com/cappielloantonio/play/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/play/util/Constants.kt @@ -14,6 +14,9 @@ object Constants { const val PODCAST_OBJECT = "PODCAST_OBJECT" const val PODCAST_CHANNEL_OBJECT = "PODCAST_CHANNEL_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_MOST_PLAYED = "ALBUM_MOST_PLAYED" diff --git a/app/src/main/java/com/cappielloantonio/play/util/IndexUtil.java b/app/src/main/java/com/cappielloantonio/play/util/IndexUtil.java new file mode 100644 index 00000000..bbaa69e5 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/util/IndexUtil.java @@ -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 getArtist(Indexes indexes) { + if (indexes.getIndices() == null) return Collections.emptyList(); + + ArrayList toReturn = new ArrayList<>(); + + for (Index index : indexes.getIndices()) { + if (index.getArtists() != null) { + toReturn.addAll(index.getArtists()); + } + } + + return toReturn; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/DirectoryViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/DirectoryViewModel.java new file mode 100644 index 00000000..a2e8ed61 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/DirectoryViewModel.java @@ -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 id = new MutableLiveData<>(null); + private MutableLiveData name = new MutableLiveData<>(null); + + private MutableLiveData directory = new MutableLiveData<>(null); + + public DirectoryViewModel(@NonNull Application application) { + super(application); + + directoryRepository = new DirectoryRepository(); + } + + public LiveData 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()); + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/IndexViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/IndexViewModel.java new file mode 100644 index 00000000..c85b5aeb --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/IndexViewModel.java @@ -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 = new MutableLiveData<>(null); + + public IndexViewModel(@NonNull Application application) { + super(application); + + directoryRepository = new DirectoryRepository(); + } + + public MutableLiveData getIndexes() { + return directoryRepository.getIndexes(null, null); + } + + public String getMusicFolderName() { + return musicFolder != null ? musicFolder.getName() : ""; + } + + public void setMusicFolder(MusicFolder musicFolder) { + this.musicFolder = musicFolder; + } +} diff --git a/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java b/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java index b7777d99..7d0d9c87 100644 --- a/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java +++ b/app/src/main/java/com/cappielloantonio/play/viewmodel/LibraryViewModel.java @@ -10,11 +10,14 @@ import androidx.lifecycle.MutableLiveData; import com.cappielloantonio.play.repository.AlbumRepository; import com.cappielloantonio.play.repository.ArtistRepository; +import com.cappielloantonio.play.repository.DirectoryRepository; import com.cappielloantonio.play.repository.GenreRepository; import com.cappielloantonio.play.repository.PlaylistRepository; import com.cappielloantonio.play.subsonic.models.AlbumID3; import com.cappielloantonio.play.subsonic.models.ArtistID3; 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 java.util.List; @@ -22,11 +25,14 @@ import java.util.List; public class LibraryViewModel extends AndroidViewModel { private static final String TAG = "LibraryViewModel"; + private final DirectoryRepository directoryRepository; private final AlbumRepository albumRepository; private final ArtistRepository artistRepository; private final GenreRepository genreRepository; private final PlaylistRepository playlistRepository; + private final MutableLiveData> musicFolders = new MutableLiveData<>(null); + private final MutableLiveData indexes = new MutableLiveData<>(null); private final MutableLiveData> playlistSample = new MutableLiveData<>(null); private final MutableLiveData> sampleAlbum = new MutableLiveData<>(null); private final MutableLiveData> sampleArtist = new MutableLiveData<>(null); @@ -35,12 +41,29 @@ public class LibraryViewModel extends AndroidViewModel { public LibraryViewModel(@NonNull Application application) { super(application); + directoryRepository = new DirectoryRepository(); albumRepository = new AlbumRepository(); artistRepository = new ArtistRepository(); genreRepository = new GenreRepository(); playlistRepository = new PlaylistRepository(); } + public LiveData> getMusicFolders(LifecycleOwner owner) { + if (musicFolders.getValue() == null) { + directoryRepository.getMusicFolders().observe(owner, musicFolders::postValue); + } + + return musicFolders; + } + + public LiveData getIndexes(LifecycleOwner owner) { + if (indexes.getValue() == null) { + directoryRepository.getIndexes("0", null).observe(owner, indexes::postValue); + } + + return indexes; + } + public LiveData> getAlbumSample(LifecycleOwner owner) { if (sampleAlbum.getValue() == null) { albumRepository.getAlbums("random", 10, null, null).observe(owner, sampleAlbum::postValue); diff --git a/app/src/main/res/layout/fragment_directory.xml b/app/src/main/res/layout/fragment_directory.xml new file mode 100644 index 00000000..b2a430a5 --- /dev/null +++ b/app/src/main/res/layout/fragment_directory.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + +