feat: podcast

This commit is contained in:
antonio 2023-05-07 23:43:36 +02:00
parent e3a28fa914
commit e85d7f9198
23 changed files with 1390 additions and 131 deletions

View file

@ -28,9 +28,13 @@ public interface ClickCallback {
default void onServerLongClick(Bundle bundle) {}
default void onPodcastClick(Bundle bundle) {}
default void onPodcastEpisodeClick(Bundle bundle) {}
default void onPodcastLongClick(Bundle bundle) {}
default void onPodcastEpisodeLongClick(Bundle bundle) {}
default void onPodcastChannelClick(Bundle bundle) {}
default void onPodcastChannelLongClick(Bundle bundle) {}
default void onInternetRadioStationClick(Bundle bundle) {}

View file

@ -10,6 +10,7 @@ import com.cappielloantonio.play.repository.QueueRepository;
import com.cappielloantonio.play.repository.SongRepository;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import com.cappielloantonio.play.subsonic.models.PodcastEpisode;
import com.cappielloantonio.play.util.MappingUtil;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -146,6 +147,23 @@ public class MediaManager {
}
}
public static void startPodcast(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, PodcastEpisode podcastEpisode) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapMediaItem(podcastEpisode));
mediaBrowserListenableFuture.get().prepare();
mediaBrowserListenableFuture.get().play();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, MoreExecutors.directExecutor());
}
}
public static void enqueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, boolean playImmediatelyAfter) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {

View file

@ -1,6 +1,7 @@
package com.cappielloantonio.play.subsonic.models
import android.os.Parcelable
import androidx.room.ColumnInfo
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
import java.util.*
@ -29,11 +30,14 @@ class PodcastEpisode : Parcelable {
var transcodedContentType: String? = null
var transcodedSuffix: String? = null
var duration: Int? = null
var bitRate: Int? = null
@ColumnInfo("bitrate")
@SerializedName("bitRate")
var bitrate: Int? = null
var path: String? = null
@ColumnInfo(name = "is_video")
@SerializedName("isVideo")
var video: Boolean? = null
var isVideo: Boolean = false
var userRating: Int? = null
var averageRating: Double? = null
var playCount: Long? = null

View file

@ -0,0 +1,146 @@
package com.cappielloantonio.play.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.databinding.ItemHomeCataloguePodcastChannelBinding;
import com.cappielloantonio.play.databinding.ItemLibraryCatalogueAlbumBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.AlbumID3;
import com.cappielloantonio.play.subsonic.models.PodcastChannel;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.util.MusicUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter<PodcastChannelCatalogueAdapter.ViewHolder> implements Filterable {
private final ClickCallback click;
private final Filter filtering = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<PodcastChannel> filteredList = new ArrayList<>();
if (constraint == null || constraint.length() == 0) {
filteredList.addAll(podcastChannelsFull);
} else {
String filterPattern = constraint.toString().toLowerCase().trim();
for (PodcastChannel item : podcastChannelsFull) {
if (item.getTitle().toLowerCase().contains(filterPattern)) {
filteredList.add(item);
}
}
}
FilterResults results = new FilterResults();
results.values = filteredList;
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
podcastChannels.clear();
podcastChannels.addAll((List) results.values);
notifyDataSetChanged();
}
};
private List<PodcastChannel> podcastChannels;
private List<PodcastChannel> podcastChannelsFull;
public PodcastChannelCatalogueAdapter(ClickCallback click) {
this.click = click;
this.podcastChannels = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemHomeCataloguePodcastChannelBinding view = ItemHomeCataloguePodcastChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
PodcastChannel podcastChannel = podcastChannels.get(position);
holder.item.podcastChannelTitleLabel.setText(MusicUtil.getReadableString(podcastChannel.getTitle()));
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), podcastChannel.getCoverArtId())
.build()
.into(holder.item.podcastChannelCatalogueCoverImageView);
}
@Override
public int getItemCount() {
return podcastChannels.size();
}
public PodcastChannel getItem(int position) {
return podcastChannels.get(position);
}
public void setItems(List<PodcastChannel> podcastChannels) {
this.podcastChannels = podcastChannels;
this.podcastChannelsFull = new ArrayList<>(podcastChannels);
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public Filter getFilter() {
return filtering;
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemHomeCataloguePodcastChannelBinding item;
ViewHolder(ItemHomeCataloguePodcastChannelBinding item) {
super(item.getRoot());
this.item = item;
item.podcastChannelTitleLabel.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
}
private void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition()));
click.onAlbumClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition()));
click.onAlbumLongClick(bundle);
return true;
}
}
}

View file

@ -0,0 +1,97 @@
package com.cappielloantonio.play.ui.adapter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.databinding.ItemHorizontalPodcastChannelBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.PodcastChannel;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.util.MusicUtil;
import java.util.Collections;
import java.util.List;
public class PodcastChannelHorizontalAdapter extends RecyclerView.Adapter<PodcastChannelHorizontalAdapter.ViewHolder> {
private final ClickCallback click;
private List<PodcastChannel> podcastChannels;
public PodcastChannelHorizontalAdapter(ClickCallback click) {
this.click = click;
this.podcastChannels = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemHorizontalPodcastChannelBinding view = ItemHorizontalPodcastChannelBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
PodcastChannel podcastChannel = podcastChannels.get(position);
holder.item.podcastChannelTitleTextView.setText(MusicUtil.getReadableString(podcastChannel.getTitle()));
holder.item.podcastChannelDescriptionTextView.setText(MusicUtil.getReadableString(podcastChannel.getDescription()));
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), podcastChannel.getOriginalImageUrl())
.build()
.into(holder.item.podcastChannelCoverImageView);
}
@Override
public int getItemCount() {
return podcastChannels.size();
}
public void setItems(List<PodcastChannel> podcastChannels) {
this.podcastChannels = podcastChannels;
notifyDataSetChanged();
}
public PodcastChannel getItem(int id) {
return podcastChannels.get(id);
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemHorizontalPodcastChannelBinding item;
ViewHolder(ItemHorizontalPodcastChannelBinding item) {
super(item.getRoot());
this.item = item;
item.podcastChannelTitleTextView.setSelected(true);
item.podcastChannelDescriptionTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.podcastChannelMoreButton.setOnClickListener(v -> onLongClick());
}
private void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition()));
click.onPodcastChannelClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_CHANNEL_OBJECT, podcastChannels.get(getBindingAdapterPosition()));
click.onPodcastChannelLongClick(bundle);
return true;
}
}
}

View file

@ -7,9 +7,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.ItemHomePodcastEpisodeBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
@ -47,7 +44,7 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
holder.item.podcastTitleLabel.setText(MusicUtil.getReadableString(podcastEpisode.getTitle()));
holder.item.podcastSubtitleLabel.setText(MusicUtil.getReadableString(podcastEpisode.getArtist()));
holder.item.podcastReleasesAndDurationLabel.setText(holder.itemView.getContext().getString(R.string.podcast_release_date_duration_formatter, simpleDateFormat.format(podcastEpisode.getPublishDate()), MusicUtil.getReadablePodcastDurationString(podcastEpisode.getDuration())));
holder.item.podcastDescriptionLabel.setText(MusicUtil.getReadableString(podcastEpisode.getDescription()));
holder.item.podcastDescriptionText.setText(MusicUtil.getReadableString(podcastEpisode.getDescription()));
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), podcastEpisode.getCoverArtId())
@ -74,22 +71,24 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
this.item = item;
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> openMore());
item.podcastMoreButton.setOnLongClickListener(v -> openMore());
item.podcastPlayButton.setOnClickListener(v -> onClick());
item.podcastMoreButton.setOnClickListener(v -> openMore());
}
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
click.onPodcastClick(bundle);
click.onPodcastEpisodeClick(bundle);
}
private boolean openMore() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.PODCAST_OBJECT, podcastEpisodes.get(getBindingAdapterPosition()));
click.onPodcastLongClick(bundle);
click.onPodcastEpisodeLongClick(bundle);
return true;
}

View file

@ -1,32 +1,52 @@
package com.cappielloantonio.play.ui.fragment;
import android.content.ComponentName;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
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.annotation.Nullable;
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.navigation.Navigation;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.viewpager2.widget.ViewPager2;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.FragmentHomeTabPodcastBinding;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.service.MediaManager;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.viewmodel.HomeViewModel;
import com.cappielloantonio.play.ui.adapter.PodcastChannelHorizontalAdapter;
import com.cappielloantonio.play.ui.adapter.PodcastEpisodeAdapter;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.util.UIUtil;
import com.cappielloantonio.play.viewmodel.PodcastViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@UnstableApi
public class HomeTabPodcastFragment extends Fragment {
public class HomeTabPodcastFragment extends Fragment implements ClickCallback {
private static final String TAG = "HomeTabPodcastFragment";
private FragmentHomeTabPodcastBinding bind;
private MainActivity activity;
private HomeViewModel homeViewModel;
private PodcastViewModel podcastViewModel;
private PodcastEpisodeAdapter podcastEpisodeAdapter;
private PodcastChannelHorizontalAdapter podcastChannelHorizontalAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -37,11 +57,21 @@ public class HomeTabPodcastFragment extends Fragment {
bind = FragmentHomeTabPodcastBinding.inflate(inflater, container, false);
View view = bind.getRoot();
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
podcastViewModel = new ViewModelProvider(requireActivity()).get(PodcastViewModel.class);
init();
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initNewestPodcastsView();
initPodcastChannelsView();
}
@Override
public void onStart() {
super.onStart();
@ -61,6 +91,47 @@ public class HomeTabPodcastFragment extends Fragment {
bind = null;
}
private void init() {
bind.podcastChannelsTextViewClickable.setOnClickListener(v -> activity.navController.navigate(R.id.action_homeFragment_to_podcastChannelCatalogueFragment));
}
private void initPodcastChannelsView() {
bind.podcastChannelsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
podcastChannelHorizontalAdapter = new PodcastChannelHorizontalAdapter(this);
bind.podcastChannelsRecyclerView.setAdapter(podcastChannelHorizontalAdapter);
podcastViewModel.getPodcastChannels(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), podcastChannels -> {
if (podcastChannels == null) {
if (bind != null) bind.homePodcastChannelsSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.homePodcastChannelsSector.setVisibility(!podcastChannels.isEmpty() ? View.VISIBLE : View.GONE);
podcastChannelHorizontalAdapter.setItems(podcastChannels);
}
});
}
private void initNewestPodcastsView() {
bind.newestPodcastsRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.newestPodcastsRecyclerView.addItemDecoration(UIUtil.getDividerItemDecoration(requireContext()));
podcastEpisodeAdapter = new PodcastEpisodeAdapter(this);
bind.newestPodcastsRecyclerView.setAdapter(podcastEpisodeAdapter);
podcastViewModel.getNewestPodcastEpisodes(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), podcastEpisodes -> {
if (podcastEpisodes == null) {
if (bind != null) bind.homeNewestPodcastsSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.homeNewestPodcastsSector.setVisibility(!podcastEpisodes.isEmpty() ? View.VISIBLE : View.GONE);
podcastEpisodeAdapter.setItems(podcastEpisodes);
}
});
}
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
@ -68,4 +139,25 @@ public class HomeTabPodcastFragment extends Fragment {
private void releaseMediaBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
@Override
public void onPodcastEpisodeClick(Bundle bundle) {
MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT));
activity.setBottomSheetInPeek(true);
}
@Override
public void onPodcastEpisodeLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.podcastBottomSheetDialog, bundle);
}
@Override
public void onPodcastChannelClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.podcastChannelPageFragment, bundle);
}
@Override
public void onPodcastChannelLongClick(Bundle bundle) {
Toast.makeText(requireContext(), "Long click!", Toast.LENGTH_SHORT).show();
}
}

View file

@ -0,0 +1,155 @@
package com.cappielloantonio.play.ui.fragment;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
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.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
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.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.FragmentPodcastChannelCatalogueBinding;
import com.cappielloantonio.play.helper.recyclerview.GridItemDecoration;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.ui.adapter.PodcastChannelCatalogueAdapter;
import com.cappielloantonio.play.viewmodel.PodcastChannelCatalogueViewModel;
@OptIn(markerClass = UnstableApi.class)
public class PodcastChannelCatalogueFragment extends Fragment implements ClickCallback {
private static final String TAG = "PodcastChannelCatalogue";
private FragmentPodcastChannelCatalogueBinding bind;
private MainActivity activity;
private PodcastChannelCatalogueViewModel podcastChannelCatalogueViewModel;
private PodcastChannelCatalogueAdapter podcastChannelCatalogueAdapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentPodcastChannelCatalogueBinding.inflate(inflater, container, false);
View view = bind.getRoot();
podcastChannelCatalogueViewModel = new ViewModelProvider(requireActivity()).get(PodcastChannelCatalogueViewModel.class);
initAppBar();
initPodcastChannelCatalogueView();
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
bind = null;
}
private void initAppBar() {
activity.setSupportActionBar(bind.toolbar);
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
activity.getSupportActionBar().setDisplayShowHomeEnabled(true);
}
bind.toolbar.setNavigationOnClickListener(v -> {
hideKeyboard(v);
activity.navController.navigateUp();
});
bind.appBarLayout.addOnOffsetChangedListener((appBarLayout, verticalOffset) -> {
if ((bind.podcastChannelInfoSector.getHeight() + verticalOffset) < (2 * ViewCompat.getMinimumHeight(bind.toolbar))) {
bind.toolbar.setTitle(R.string.podcast_channel_catalogue_title);
} else {
bind.toolbar.setTitle(R.string.empty_string);
}
});
}
@SuppressLint("ClickableViewAccessibility")
private void initPodcastChannelCatalogueView() {
bind.podcastChannelCatalogueRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), 2));
bind.podcastChannelCatalogueRecyclerView.addItemDecoration(new GridItemDecoration(2, 20, false));
bind.podcastChannelCatalogueRecyclerView.setHasFixedSize(true);
podcastChannelCatalogueAdapter = new PodcastChannelCatalogueAdapter(this);
podcastChannelCatalogueAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
bind.podcastChannelCatalogueRecyclerView.setAdapter(podcastChannelCatalogueAdapter);
podcastChannelCatalogueViewModel.getPodcastChannels(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), albums -> {
if (albums != null) {
podcastChannelCatalogueAdapter.setItems(albums);
}
});
bind.podcastChannelCatalogueRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v);
return false;
});
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
inflater.inflate(R.menu.toolbar_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) searchItem.getActionView();
searchView.setImeOptions(EditorInfo.IME_ACTION_DONE);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
searchView.clearFocus();
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
podcastChannelCatalogueAdapter.getFilter().filter(newText);
return false;
}
});
searchView.setPadding(-32, 0, 0, 0);
}
private void hideKeyboard(View view) {
InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override
public void onPodcastChannelClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.podcastChannelPageFragment, bundle);
hideKeyboard(requireView());
}
@Override
public void onPodcastChannelLongClick(Bundle bundle) {
// Navigation.findNavController(requireView()).navigate(R.id.albumBottomSheetDialog, bundle);
}
}

View file

@ -0,0 +1,150 @@
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 androidx.viewpager2.widget.ViewPager2;
import com.cappielloantonio.play.R;
import com.cappielloantonio.play.databinding.FragmentPodcastChannelPageBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.service.MediaManager;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.ui.adapter.PodcastEpisodeAdapter;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.util.MusicUtil;
import com.cappielloantonio.play.util.UIUtil;
import com.cappielloantonio.play.viewmodel.PodcastChannelPageViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@UnstableApi
public class PodcastChannelPageFragment extends Fragment implements ClickCallback {
private FragmentPodcastChannelPageBinding bind;
private MainActivity activity;
private PodcastChannelPageViewModel podcastChannelPageViewModel;
private PodcastEpisodeAdapter podcastEpisodeAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
bind = FragmentPodcastChannelPageBinding.inflate(inflater, container, false);
View view = bind.getRoot();
podcastChannelPageViewModel = new ViewModelProvider(requireActivity()).get(PodcastChannelPageViewModel.class);
init();
initAppBar();
initPodcastChannelInfo();
initPodcastChannelEpisodesView();
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() {
podcastChannelPageViewModel.setPodcastChannel(requireArguments().getParcelable(Constants.PODCAST_CHANNEL_OBJECT));
}
private void initAppBar() {
activity.setSupportActionBar(bind.animToolbar);
if (activity.getSupportActionBar() != null)
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
bind.collapsingToolbar.setTitle(MusicUtil.getReadableString(podcastChannelPageViewModel.getPodcastChannel().getTitle()));
bind.animToolbar.setNavigationOnClickListener(v -> activity.navController.navigateUp());
bind.collapsingToolbar.setExpandedTitleColor(getResources().getColor(R.color.white, null));
}
private void initPodcastChannelInfo() {
String normalizePodcastChannelDescription = MusicUtil.forceReadableString(podcastChannelPageViewModel.getPodcastChannel().getDescription());
if (bind != null)
bind.podcastChannelDescriptionTextView.setVisibility(!normalizePodcastChannelDescription.trim().isEmpty() ? View.VISIBLE : View.GONE);
if (getContext() != null && bind != null) CustomGlideRequest.Builder
.from(requireContext(), podcastChannelPageViewModel.getPodcastChannel().getCoverArtId())
.build()
.into(bind.podcastChannelBackdropImageView);
if (bind != null)
bind.podcastChannelDescriptionTextView.setText(normalizePodcastChannelDescription);
}
private void initPodcastChannelEpisodesView() {
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.mostStreamedSongRecyclerView.addItemDecoration(UIUtil.getDividerItemDecoration(requireContext()));
podcastEpisodeAdapter = new PodcastEpisodeAdapter(this);
bind.mostStreamedSongRecyclerView.setAdapter(podcastEpisodeAdapter);
podcastChannelPageViewModel.getPodcastChannelEpisodes().observe(getViewLifecycleOwner(), channels -> {
if (channels == null) {
if (bind != null)
bind.podcastChannelPageEpisodesPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.podcastChannelPageEpisodesSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.podcastChannelPageEpisodesPlaceholder.placeholder.setVisibility(View.GONE);
if (channels.get(0) != null && channels.get(0).getEpisodes() != null) {
if (bind != null)
bind.podcastChannelPageEpisodesSector.setVisibility(!channels.get(0).getEpisodes().isEmpty() ? View.VISIBLE : View.GONE);
podcastEpisodeAdapter.setItems(channels.get(0).getEpisodes());
}
}
});
}
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 onPodcastEpisodeClick(Bundle bundle) {
MediaManager.startPodcast(mediaBrowserListenableFuture, bundle.getParcelable(Constants.PODCAST_OBJECT));
activity.setBottomSheetInPeek(true);
}
@Override
public void onPodcastEpisodeLongClick(Bundle bundle) {
Toast.makeText(requireContext(), "Long click!", Toast.LENGTH_SHORT).show();
}
}

View file

@ -12,6 +12,7 @@ object Constants {
const val GENRE_OBJECT = "GENRE_OBJECT"
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
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 ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED"

View file

@ -12,6 +12,7 @@ import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import com.cappielloantonio.play.subsonic.models.PodcastEpisode;
import java.util.ArrayList;
import java.util.List;
@ -152,9 +153,76 @@ public class MappingUtil {
.build();
}
public static MediaItem mapMediaItem(PodcastEpisode podcastEpisode) {
Uri uri = getUri(podcastEpisode);
Bundle bundle = new Bundle();
bundle.putString("id", podcastEpisode.getId());
bundle.putString("parentId", podcastEpisode.getParentId());
bundle.putBoolean("isDir", podcastEpisode.isDir());
bundle.putString("title", podcastEpisode.getTitle());
bundle.putString("album", podcastEpisode.getAlbum());
bundle.putString("artist", podcastEpisode.getArtist());
bundle.putInt("track", podcastEpisode.getTrack() != null ? podcastEpisode.getTrack() : 0);
bundle.putInt("year", podcastEpisode.getYear() != null ? podcastEpisode.getYear() : 0);
bundle.putString("genre", podcastEpisode.getGenre());
bundle.putString("coverArtId", podcastEpisode.getCoverArtId());
bundle.putLong("size", podcastEpisode.getSize() != null ? podcastEpisode.getSize() : 0);
bundle.putString("contentType", podcastEpisode.getContentType());
bundle.putString("suffix", podcastEpisode.getSuffix());
bundle.putString("transcodedContentType", podcastEpisode.getTranscodedContentType());
bundle.putString("transcodedSuffix", podcastEpisode.getTranscodedSuffix());
bundle.putInt("duration", podcastEpisode.getDuration() != null ? podcastEpisode.getDuration() : 0);
bundle.putInt("bitrate", podcastEpisode.getBitrate() != null ? podcastEpisode.getBitrate() : 0);
bundle.putString("path", podcastEpisode.getPath());
bundle.putBoolean("isVideo", podcastEpisode.isVideo());
bundle.putInt("userRating", podcastEpisode.getUserRating() != null ? podcastEpisode.getUserRating() : 0);
bundle.putDouble("averageRating", podcastEpisode.getAverageRating() != null ? podcastEpisode.getAverageRating() : 0);
bundle.putLong("playCount", podcastEpisode.getPlayCount() != null ? podcastEpisode.getTrack() : 0);
bundle.putInt("discNumber", podcastEpisode.getDiscNumber() != null ? podcastEpisode.getTrack() : 0);
bundle.putLong("created", podcastEpisode.getCreated() != null ? podcastEpisode.getCreated().getTime() : 0);
bundle.putLong("starred", podcastEpisode.getStarred() != null ? podcastEpisode.getStarred().getTime() : 0);
bundle.putString("albumId", podcastEpisode.getAlbumId());
bundle.putString("artistId", podcastEpisode.getArtistId());
bundle.putString("type", podcastEpisode.getType());
bundle.putLong("bookmarkPosition", podcastEpisode.getBookmarkPosition() != null ? podcastEpisode.getBookmarkPosition() : 0);
bundle.putInt("originalWidth", podcastEpisode.getOriginalWidth() != null ? podcastEpisode.getOriginalWidth() : 0);
bundle.putInt("originalHeight", podcastEpisode.getOriginalHeight() != null ? podcastEpisode.getOriginalHeight() : 0);
bundle.putString("uri", uri.toString());
return new MediaItem.Builder()
.setMediaId(podcastEpisode.getId())
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(MusicUtil.getReadableString(podcastEpisode.getTitle()))
.setTrackNumber(podcastEpisode.getTrack())
.setDiscNumber(podcastEpisode.getDiscNumber())
.setReleaseYear(podcastEpisode.getYear())
.setAlbumTitle(MusicUtil.getReadableString(podcastEpisode.getAlbum()))
.setArtist(MusicUtil.getReadableString(podcastEpisode.getArtist()))
.setExtras(bundle)
.build()
)
.setRequestMetadata(
new MediaItem.RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(bundle)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build();
}
private static Uri getUri(Child media) {
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
? MusicUtil.getDownloadUri(media.getId())
: MusicUtil.getStreamUri(media.getId());
}
private static Uri getUri(PodcastEpisode podcastEpisode) {
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(podcastEpisode.getId())
? MusicUtil.getDownloadUri(podcastEpisode.getId())
: MusicUtil.getStreamUri(podcastEpisode.getId());
}
}

View file

@ -1,6 +1,11 @@
package com.cappielloantonio.play.util;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import androidx.recyclerview.widget.DividerItemDecoration;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
@ -19,4 +24,18 @@ public class UIUtil {
public static boolean isCastApiAvailable(Context context) {
return GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
}
public static DividerItemDecoration getDividerItemDecoration(Context context) {
int[] ATTRS = new int[]{android.R.attr.listDivider};
TypedArray a = context.obtainStyledAttributes(ATTRS);
Drawable divider = a.getDrawable(0);
InsetDrawable insetDivider = new InsetDrawable(divider, 42, 0, 42, 42);
a.recycle();
DividerItemDecoration itemDecoration = new DividerItemDecoration(context, DividerItemDecoration.VERTICAL);
itemDecoration.setDrawable(insetDivider);
return itemDecoration;
}
}

View file

@ -0,0 +1,43 @@
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.App;
import com.cappielloantonio.play.interfaces.MediaCallback;
import com.cappielloantonio.play.repository.PodcastRepository;
import com.cappielloantonio.play.subsonic.base.ApiResponse;
import com.cappielloantonio.play.subsonic.models.AlbumID3;
import com.cappielloantonio.play.subsonic.models.PodcastChannel;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
public class PodcastChannelCatalogueViewModel extends AndroidViewModel {
private final PodcastRepository podcastRepository;
private final MutableLiveData<List<PodcastChannel>> podcastChannels = new MutableLiveData<>(null);
public PodcastChannelCatalogueViewModel(@NonNull Application application) {
super(application);
podcastRepository = new PodcastRepository();
}
public LiveData<List<PodcastChannel>> getPodcastChannels(LifecycleOwner owner) {
if (podcastChannels.getValue() == null) {
podcastRepository.getPodcastChannels(false, null).observe(owner, podcastChannels::postValue);
}
return podcastChannels;
}
}

View file

@ -0,0 +1,43 @@
package com.cappielloantonio.play.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import com.cappielloantonio.play.repository.AlbumRepository;
import com.cappielloantonio.play.repository.ArtistRepository;
import com.cappielloantonio.play.repository.PodcastRepository;
import com.cappielloantonio.play.subsonic.models.AlbumID3;
import com.cappielloantonio.play.subsonic.models.ArtistID3;
import com.cappielloantonio.play.subsonic.models.ArtistInfo2;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.subsonic.models.PodcastChannel;
import com.cappielloantonio.play.subsonic.models.PodcastEpisode;
import java.util.List;
public class PodcastChannelPageViewModel extends AndroidViewModel {
private final PodcastRepository podcastRepository;
private PodcastChannel podcastChannel;
public PodcastChannelPageViewModel(@NonNull Application application) {
super(application);
podcastRepository = new PodcastRepository();
}
public LiveData<List<PodcastChannel>> getPodcastChannelEpisodes() {
return podcastRepository.getPodcastChannels(true, podcastChannel.getId());
}
public PodcastChannel getPodcastChannel() {
return podcastChannel;
}
public void setPodcastChannel(PodcastChannel podcastChannel) {
this.podcastChannel = podcastChannel;
}
}

View file

@ -0,0 +1,44 @@
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.PodcastRepository;
import com.cappielloantonio.play.subsonic.models.PodcastChannel;
import com.cappielloantonio.play.subsonic.models.PodcastEpisode;
import java.util.List;
public class PodcastViewModel extends AndroidViewModel {
private final PodcastRepository podcastRepository;
private final MutableLiveData<List<PodcastEpisode>> newestPodcastEpisodes = new MutableLiveData<>(null);
private final MutableLiveData<List<PodcastChannel>> podcastChannels = new MutableLiveData<>(null);
public PodcastViewModel(@NonNull Application application) {
super(application);
podcastRepository = new PodcastRepository();
}
public LiveData<List<PodcastEpisode>> getNewestPodcastEpisodes(LifecycleOwner owner) {
if (newestPodcastEpisodes.getValue() == null) {
podcastRepository.getNewestPodcastEpisodes(20).observe(owner, newestPodcastEpisodes::postValue);
}
return newestPodcastEpisodes;
}
public LiveData<List<PodcastChannel>> getPodcastChannels(LifecycleOwner owner) {
if (podcastChannels.getValue() == null) {
podcastRepository.getPodcastChannels(false, null).observe(owner, podcastChannels::postValue);
}
return podcastChannels;
}
}