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;
}
}

View file

@ -1,6 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content">
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom">
<LinearLayout
android:id="@+id/home_podcast_channels_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="8dp">
<!-- Label and button -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="@style/TitleLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_title_podcast_channels" />
<TextView
android:id="@+id/podcast_channels_text_view_clickable"
style="@style/TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="12dp"
android:paddingEnd="16dp"
android:text="@string/home_title_podcast_channels_see_all_button" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/podcast_channels_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginHorizontal="16dp" />
<LinearLayout
android:id="@+id/home_newest_podcasts_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:paddingTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/home_title_newest_podcasts" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/newest_podcasts_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingTop="8dp" />
</LinearLayout>
<include
android:id="@+id/podcast_episodes_placeholder"
layout="@layout/item_placeholder_horizontal"
android:visibility="gone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

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/podcast_channel_info_sector"
android:layout_width="match_parent"
android:layout_height="172dp"
android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="24dp"
android:text="@string/podcast_channel_catalogue_title_expanded"
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/podcast_channel_catalogue_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="@dimen/appbar_header_height">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:expandedTitleMarginStart="@dimen/activity_margin_content"
app:contentScrim="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:id="@+id/podcast_channel_backdrop_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/appbar_header_height"
android:layout_gravity="top"
android:background="@drawable/gradient_backdrop_background_image" />
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/anim_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_artist_page_nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="18dp"
android:paddingBottom="@dimen/global_padding_bottom">
<LinearLayout
android:id="@+id/podcast_channel_page_bio_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="22dp">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:text="@string/podcast_channel_page_title_description_section" />
<TextView
android:id="@+id/podcast_channel_description_text_view"
style="@style/TitleMedium"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp" />
</LinearLayout>
<include
android:id="@+id/podcast_channel_page_description_placeholder"
layout="@layout/item_placehoder_biography"
android:visibility="gone" />
<View
android:id="@+id/upper_button_divider"
style="@style/Divider"
android:layout_marginHorizontal="16dp" />
<!-- Label and button -->
<LinearLayout
android:id="@+id/podcast_channel_page_episodes_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="22dp">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:text="@string/podcast_channel_page_title_episode_section" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/most_streamed_song_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingTop="8dp" />
</LinearLayout>
<include
android:id="@+id/podcast_channel_page_episodes_placeholder"
layout="@layout/item_placeholder_horizontal"
android:visibility="gone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<ImageView
android:id="@+id/podcast_channel_catalogue_cover_image_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="W, 1:1"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/podcast_channel_title_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingTop="8dp"
android:singleLine="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_channel_catalogue_cover_image_view" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,121 +1,108 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:cardCornerRadius="4dp">
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="horizontal"
android:paddingHorizontal="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
<ImageView
android:id="@+id/podcast_cover_image_view"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
app:layout_constraintBottom_toTopOf="@+id/podcast_upper_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/podcast_title_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="5"
android:layout_marginStart="12dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@+id/podcast_subtitle_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/podcast_cover_image_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/podcast_subtitle_label"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:layout_marginStart="12dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@id/podcast_upper_divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/podcast_cover_image_view"
app:layout_constraintTop_toBottomOf="@+id/podcast_title_label" />
<View
android:id="@+id/podcast_upper_divider"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="0.5dp"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_cover_image_view" />
<ImageView
android:id="@+id/podcast_cover_image_view"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:layout_margin="12dp"
app:layout_constraintBottom_toTopOf="@+id/podcast_upper_divider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/podcast_description_text"
style="@style/LabelSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="7"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_upper_divider" />
<TextView
android:id="@+id/podcast_title_label"
style="@style/LabelLarge"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="12dp"
android:paddingEnd="16dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@+id/podcast_subtitle_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/podcast_cover_image_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<Button
android:id="@+id/podcast_play_button"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:icon="@drawable/ic_play"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/podcast_description_text" />
<TextView
android:id="@+id/podcast_releases_and_duration_label"
style="@style/LabelSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="@+id/podcast_play_button"
app:layout_constraintStart_toEndOf="@+id/podcast_play_button"
app:layout_constraintTop_toTopOf="@+id/podcast_play_button" />
<TextView
android:id="@+id/podcast_subtitle_label"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="12dp"
android:paddingEnd="16dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@id/podcast_upper_divider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/podcast_cover_image_view"
app:layout_constraintTop_toBottomOf="@+id/podcast_title_label" />
<Button
android:id="@+id/podcast_more_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_more_vert"
app:layout_constraintBottom_toBottomOf="@+id/podcast_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/podcast_play_button" />
<View
android:id="@+id/podcast_upper_divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_cover_image_view"/>
<TextView
android:id="@+id/podcast_description_label"
style="@style/LabelSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/podcast_upper_divider" />
<Button
android:id="@+id/podcast_play_button"
style="@style/Widget.Material3.Button.TonalButton.Icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
app:icon="@drawable/ic_play"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:cornerRadius="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/podcast_description_label"/>
<TextView
android:id="@+id/podcast_releases_and_duration_label"
style="@style/LabelSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="@+id/podcast_play_button"
app:layout_constraintStart_toEndOf="@+id/podcast_play_button"
app:layout_constraintTop_toTopOf="@+id/podcast_play_button" />
<Button
android:id="@+id/podcast_more_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_more_vert"
app:layout_constraintBottom_toBottomOf="@+id/podcast_play_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/podcast_play_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,64 @@
<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/podcast_channel_cover_image_view"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="center"
android:layout_margin="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/podcast_channel_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingStart="12dp"
android:paddingTop="10dp"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@+id/podcast_channel_more_button"
app:layout_constraintStart_toEndOf="@+id/podcast_channel_cover_image_view"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/podcast_channel_description_text_view"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:singleLine="true"
android:paddingStart="12dp"
android:paddingEnd="16dp"
android:text="@string/label_placeholder"
app:layout_constraintEnd_toStartOf="@+id/podcast_channel_more_button"
app:layout_constraintStart_toEndOf="@+id/podcast_channel_cover_image_view"
app:layout_constraintTop_toBottomOf="@+id/podcast_channel_title_text_view" />
<ImageView
android:id="@+id/podcast_channel_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"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -65,6 +65,9 @@
<action
android:id="@+id/action_homeFragment_to_playlistPageFragment"
app:destination="@id/playlistPageFragment" />
<action
android:id="@+id/action_homeFragment_to_podcastChannelCatalogueFragment"
app:destination="@id/podcastChannelCatalogueFragment" />
</fragment>
<fragment
android:id="@+id/libraryFragment"
@ -127,7 +130,7 @@
app:destination="@id/settingsFragment" />
<action
android:id="@+id/action_downloadFragment_to_searchFragment"
app:destination="@id/searchFragment"/>
app:destination="@id/searchFragment" />
<action
android:id="@+id/action_downloadFragment_to_playlistPageFragment"
app:destination="@id/playlistPageFragment" />
@ -262,6 +265,20 @@
android:name="com.cappielloantonio.play.ui.fragment.PlaylistPageFragment"
android:label="PlaylistPageFragment"
tools:layout="@layout/fragment_playlist_page" />
<fragment
android:id="@+id/podcastChannelCatalogueFragment"
android:name="com.cappielloantonio.play.ui.fragment.PodcastChannelCatalogueFragment"
android:label="PodcastChannelCatalogueFragment"
tools:layout="@layout/fragment_podcast_channel_catalogue">
<action
android:id="@+id/action_podcastChannelCatalogueFragment_to_podcastChannelPageFragment"
app:destination="@id/podcastChannelPageFragment" />
</fragment>
<fragment
android:id="@+id/podcastChannelPageFragment"
android:name="com.cappielloantonio.play.ui.fragment.PodcastChannelPageFragment"
android:label="PodcastChannelPageFragment"
tools:layout="@layout/fragment_podcast_channel_page" />
<dialog
android:id="@+id/songBottomSheetDialog"
android:name="com.cappielloantonio.play.ui.fragment.bottomsheetdialog.SongBottomSheetDialog"

View file

@ -80,6 +80,7 @@
<string name="home_title_starred_tracks">★ Starred tracks</string>
<string name="home_title_starred_tracks_see_all_button">See all</string>
<string name="home_title_internet_radio_station">Internet radio stations</string>
<string name="home_title_podcast_channels_see_all_button">See all</string>
<string name="label_dot_separator"></string>
<string name="label_placeholder">--</string>
<string name="library_title_album">Albums</string>
@ -118,6 +119,10 @@
<string name="playlist_page_play_button">Play</string>
<string name="playlist_page_shuffle_button">Shuffle</string>
<string name="playlist_song_count">Playlist • %1$d songs</string>
<string name="podcast_channel_catalogue_title_expanded">Browse Channels</string>
<string name="podcast_channel_catalogue_title">Channels</string>
<string name="podcast_channel_page_title_description_section">Description</string>
<string name="podcast_channel_page_title_episode_section">Episodes</string>
<string name="podcast_release_date_duration_formatter">%1$s • %2$s</string>
<string name="radio_editor_dialog_hint_name">Radio Name</string>
<string name="radio_editor_dialog_hint_stream_url">Radio Stream URL</string>
@ -219,6 +224,7 @@
<string name="home_title_best_of">Best of</string>
<string name="home_subtitle_best_of">Top songs of your favorite artists</string>
<string name="home_title_newest_podcasts">Newest podcasts</string>
<string name="home_title_podcast_channels">Channels</string>
<string name="artist_adapter_radio_station_starting">Searching…</string>
<string name="podcast_bottom_sheet_go_to_channel">Go to channel</string>
<string name="podcast_bottom_sheet_remove">Remove</string>