mirror of
https://github.com/antebudimir/tempus.git
synced 2026-01-01 09:53:33 +00:00
feat: podcast
This commit is contained in:
parent
e3a28fa914
commit
e85d7f9198
23 changed files with 1390 additions and 131 deletions
|
|
@ -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) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() -> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
||||
127
app/src/main/res/layout/fragment_podcast_channel_page.xml
Normal file
127
app/src/main/res/layout/fragment_podcast_channel_page.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
64
app/src/main/res/layout/item_horizontal_podcast_channel.xml
Normal file
64
app/src/main/res/layout/item_horizontal_podcast_channel.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue