From 77104c9038abf579bc5652fc0ec5f941c0f9799f Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Sat, 27 Aug 2022 11:19:34 +0200 Subject: Home Screen (#5864) Co-authored-by: ueen --- .../danoeh/antennapod/activity/MainActivity.java | 27 ++- .../antennapod/adapter/EpisodeItemListAdapter.java | 15 +- .../adapter/FeedSearchResultAdapter.java | 76 --------- .../adapter/HorizontalFeedListAdapter.java | 93 +++++++++++ .../adapter/HorizontalItemListAdapter.java | 138 +++++++++++++++ .../danoeh/antennapod/adapter/NavListAdapter.java | 3 + .../antennapod/fragment/NavDrawerFragment.java | 17 +- .../danoeh/antennapod/fragment/SearchFragment.java | 6 +- .../de/danoeh/antennapod/ui/home/HomeFragment.java | 185 +++++++++++++++++++++ .../de/danoeh/antennapod/ui/home/HomeSection.java | 86 ++++++++++ .../ui/home/HomeSectionsSettingsDialog.java | 42 +++++ .../ui/home/sections/DownloadsSection.java | 125 ++++++++++++++ .../ui/home/sections/EpisodesSurpriseSection.java | 155 +++++++++++++++++ .../antennapod/ui/home/sections/InboxSection.java | 122 ++++++++++++++ .../antennapod/ui/home/sections/QueueSection.java | 150 +++++++++++++++++ .../ui/home/sections/SubscriptionsSection.java | 89 ++++++++++ .../view/viewholder/EpisodeItemViewHolder.java | 24 +++ .../view/viewholder/HorizontalItemViewHolder.java | 105 ++++++++++++ app/src/main/res/layout/home_fragment.xml | 75 +++++++++ app/src/main/res/layout/home_section.xml | 91 ++++++++++ app/src/main/res/layout/horizontal_feed_item.xml | 33 ++++ .../main/res/layout/horizontal_itemlist_item.xml | 123 ++++++++++++++ app/src/main/res/layout/queue_fragment.xml | 3 +- app/src/main/res/layout/searchlist_item_feed.xml | 21 --- app/src/main/res/layout/simple_list_fragment.xml | 4 +- app/src/main/res/menu/home.xml | 25 +++ 26 files changed, 1705 insertions(+), 128 deletions(-) delete mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java create mode 100644 app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java create mode 100644 app/src/main/res/layout/home_fragment.xml create mode 100644 app/src/main/res/layout/home_section.xml create mode 100644 app/src/main/res/layout/horizontal_feed_item.xml create mode 100644 app/src/main/res/layout/horizontal_itemlist_item.xml delete mode 100644 app/src/main/res/layout/searchlist_item_feed.xml create mode 100644 app/src/main/res/menu/home.xml (limited to 'app/src/main') diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index 837dcd731..2a09de724 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -9,8 +9,6 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; @@ -19,7 +17,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.Toast; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBarDrawerToggle; @@ -32,14 +29,11 @@ import androidx.fragment.app.FragmentContainerView; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.recyclerview.widget.RecyclerView; - import com.bumptech.glide.Glide; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.fragment.AllEpisodesFragment; -import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; -import de.danoeh.antennapod.playback.cast.CastEnabledActivity; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.Validate; import org.greenrobot.eventbus.EventBus; @@ -47,14 +41,15 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.download.AutoUpdateManager; import de.danoeh.antennapod.dialog.RatingDialog; +import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.fragment.AddFeedFragment; import de.danoeh.antennapod.fragment.AudioPlayerFragment; +import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; import de.danoeh.antennapod.fragment.InboxFragment; import de.danoeh.antennapod.fragment.FeedItemlistFragment; import de.danoeh.antennapod.fragment.NavDrawerFragment; @@ -63,9 +58,11 @@ import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SearchFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; import de.danoeh.antennapod.fragment.TransitionEffect; +import de.danoeh.antennapod.playback.cast.CastEnabledActivity; import de.danoeh.antennapod.preferences.PreferenceUpgrader; import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.common.ThemeUtils; +import de.danoeh.antennapod.ui.home.HomeFragment; import de.danoeh.antennapod.view.LockableBottomSheetBehavior; /** @@ -222,13 +219,6 @@ public class MainActivity extends CastEnabledActivity { private void checkFirstLaunch() { SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { - loadFragment(AddFeedFragment.TAG, null); - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (drawerLayout != null) { // Tablet layout does not have a drawer - drawerLayout.openDrawer(navDrawer); - } - }, 1500); - // for backward compatibility, we only change defaults for fresh installs UserPreferences.setUpdateInterval(12); AutoUpdateManager.restartUpdateAlarm(this); @@ -264,6 +254,9 @@ public class MainActivity extends CastEnabledActivity { Log.d(TAG, "loadFragment(tag: " + tag + ", args: " + args + ")"); Fragment fragment; switch (tag) { + case HomeFragment.TAG: + fragment = new HomeFragment(); + break; case QueueFragment.TAG: fragment = new QueueFragment(); break; @@ -286,9 +279,9 @@ public class MainActivity extends CastEnabledActivity { fragment = new SubscriptionFragment(); break; default: - // default to the queue - fragment = new QueueFragment(); - tag = QueueFragment.TAG; + // default to home screen + fragment = new HomeFragment(); + tag = HomeFragment.TAG; args = null; break; } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java index 088caf70a..9c2ff2586 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java @@ -38,6 +38,7 @@ public class EpisodeItemListAdapter extends SelectableAdapter episodes = new ArrayList<>(); private FeedItem longPressedItem; int longPressedPosition = 0; // used to init actionMode + private int dummyViews = 0; public EpisodeItemListAdapter(MainActivity mainActivity) { super(mainActivity); @@ -45,6 +46,10 @@ public class EpisodeItemListAdapter extends SelectableAdapter items) { episodes = items; notifyDataSetChanged(); @@ -64,6 +69,11 @@ public class EpisodeItemListAdapter extends SelectableAdapter= episodes.size()) { + holder.bindDummy(); + return; + } + // Reset state of recycled views holder.coverHolder.setVisibility(View.VISIBLE); holder.dragHandle.setVisibility(View.GONE); @@ -155,13 +165,16 @@ public class EpisodeItemListAdapter extends SelectableAdapter= episodes.size()) { + return RecyclerView.NO_ID; // Dummy views + } FeedItem item = episodes.get(position); return item != null ? item.getId() : RecyclerView.NO_POSITION; } @Override public int getItemCount() { - return episodes.size(); + return dummyViews + episodes.size(); } protected FeedItem getItem(int index) { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java deleted file mode 100644 index 92865e211..000000000 --- a/app/src/main/java/de/danoeh/antennapod/adapter/FeedSearchResultAdapter.java +++ /dev/null @@ -1,76 +0,0 @@ -package de.danoeh.antennapod.adapter; - -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.fragment.FeedItemlistFragment; -import de.danoeh.antennapod.ui.common.SquareImageView; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.List; - -public class FeedSearchResultAdapter extends RecyclerView.Adapter { - - private final WeakReference mainActivityRef; - private final List data = new ArrayList<>(); - - public FeedSearchResultAdapter(MainActivity mainActivity) { - this.mainActivityRef = new WeakReference<>(mainActivity); - } - - public void updateData(List newData) { - data.clear(); - data.addAll(newData); - notifyDataSetChanged(); - } - - @NonNull - @Override - public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View convertView = View.inflate(mainActivityRef.get(), R.layout.searchlist_item_feed, null); - return new Holder(convertView); - } - - @Override - public void onBindViewHolder(@NonNull Holder holder, int position) { - final Feed podcast = data.get(position); - holder.imageView.setContentDescription(podcast.getTitle()); - holder.imageView.setOnClickListener(v -> - mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId()))); - - Glide.with(mainActivityRef.get()) - .load(podcast.getImageUrl()) - .apply(new RequestOptions() - .placeholder(R.color.light_gray) - .fitCenter() - .dontAnimate()) - .into(holder.imageView); - } - - @Override - public long getItemId(int position) { - return data.get(position).getId(); - } - - @Override - public int getItemCount() { - return data.size(); - } - - static class Holder extends RecyclerView.ViewHolder { - SquareImageView imageView; - - public Holder(@NonNull View itemView) { - super(itemView); - imageView = itemView.findViewById(R.id.discovery_cover); - imageView.setDirection(SquareImageView.DIRECTION_HEIGHT); - } - } -} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java new file mode 100644 index 000000000..3e0190ee5 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalFeedListAdapter.java @@ -0,0 +1,93 @@ +package de.danoeh.antennapod.adapter; + +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.fragment.FeedItemlistFragment; +import de.danoeh.antennapod.ui.common.SquareImageView; +import de.danoeh.antennapod.ui.common.ThemeUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +public class HorizontalFeedListAdapter extends RecyclerView.Adapter { + private final WeakReference mainActivityRef; + private final List data = new ArrayList<>(); + private int dummyViews = 0; + + public HorizontalFeedListAdapter(MainActivity mainActivity) { + this.mainActivityRef = new WeakReference<>(mainActivity); + } + + public void setDummyViews(int dummyViews) { + this.dummyViews = dummyViews; + } + + public void updateData(List newData) { + data.clear(); + data.addAll(newData); + notifyDataSetChanged(); + } + + @NonNull + @Override + public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null); + return new Holder(convertView); + } + + @Override + public void onBindViewHolder(@NonNull Holder holder, int position) { + if (position >= data.size()) { + holder.itemView.setAlpha(0.1f); + Glide.with(mainActivityRef.get()).clear(holder.imageView); + holder.imageView.setImageResource( + ThemeUtils.getDrawableFromAttr(mainActivityRef.get(), android.R.attr.textColorSecondary)); + return; + } + + holder.itemView.setAlpha(1.0f); + final Feed podcast = data.get(position); + holder.imageView.setContentDescription(podcast.getTitle()); + holder.imageView.setOnClickListener(v -> + mainActivityRef.get().loadChildFragment(FeedItemlistFragment.newInstance(podcast.getId()))); + + Glide.with(mainActivityRef.get()) + .load(podcast.getImageUrl()) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .fitCenter() + .dontAnimate()) + .into(holder.imageView); + } + + @Override + public long getItemId(int position) { + if (position >= data.size()) { + return RecyclerView.NO_ID; // Dummy views + } + return data.get(position).getId(); + } + + @Override + public int getItemCount() { + return dummyViews + data.size(); + } + + static class Holder extends RecyclerView.ViewHolder { + SquareImageView imageView; + + public Holder(@NonNull View itemView) { + super(itemView); + imageView = itemView.findViewById(R.id.discovery_cover); + imageView.setDirection(SquareImageView.DIRECTION_HEIGHT); + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java new file mode 100644 index 000000000..4e8a2b05e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/adapter/HorizontalItemListAdapter.java @@ -0,0 +1,138 @@ +package de.danoeh.antennapod.adapter; + +import android.view.ContextMenu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.fragment.ItemPagerFragment; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder; +import org.apache.commons.lang3.ArrayUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +public class HorizontalItemListAdapter extends RecyclerView.Adapter + implements View.OnCreateContextMenuListener { + private final WeakReference mainActivityRef; + private List data = new ArrayList<>(); + private FeedItem longPressedItem; + private int dummyViews = 0; + + public HorizontalItemListAdapter(MainActivity mainActivity) { + this.mainActivityRef = new WeakReference<>(mainActivity); + setHasStableIds(true); + } + + public void setDummyViews(int dummyViews) { + this.dummyViews = dummyViews; + } + + public void updateData(List newData) { + data = newData; + notifyDataSetChanged(); + } + + @NonNull + @Override + public HorizontalItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new HorizontalItemViewHolder(mainActivityRef.get(), parent); + } + + @Override + public void onBindViewHolder(@NonNull HorizontalItemViewHolder holder, int position) { + if (position >= data.size()) { + holder.bindDummy(); + return; + } + + final FeedItem item = data.get(position); + holder.bind(item); + + holder.card.setOnCreateContextMenuListener(this); + holder.card.setOnLongClickListener(v -> { + longPressedItem = item; + return false; + }); + holder.secondaryActionIcon.setOnCreateContextMenuListener(this); + holder.secondaryActionIcon.setOnLongClickListener(v -> { + longPressedItem = item; + return false; + }); + holder.card.setOnClickListener(v -> { + MainActivity activity = mainActivityRef.get(); + if (activity != null) { + long[] ids = FeedItemUtil.getIds(data); + int clickPosition = ArrayUtils.indexOf(ids, item.getId()); + activity.loadChildFragment(ItemPagerFragment.newInstance(ids, clickPosition)); + } + }); + } + + @Override + public long getItemId(int position) { + if (position >= data.size()) { + return RecyclerView.NO_ID; // Dummy views + } + return data.get(position).getId(); + } + + @Override + public int getItemCount() { + return dummyViews + data.size(); + } + + @Override + public void onViewRecycled(@NonNull HorizontalItemViewHolder holder) { + super.onViewRecycled(holder); + // Set all listeners to null. This is required to prevent leaking fragments that have set a listener. + // Activity -> recycledViewPool -> ViewHolder -> Listener -> Fragment (can not be garbage collected) + holder.card.setOnClickListener(null); + holder.card.setOnCreateContextMenuListener(null); + holder.card.setOnLongClickListener(null); + holder.secondaryActionIcon.setOnClickListener(null); + holder.secondaryActionIcon.setOnCreateContextMenuListener(null); + holder.secondaryActionIcon.setOnLongClickListener(null); + } + + /** + * {@link #notifyItemChanged(int)} is final, so we can not override. + * Calling {@link #notifyItemChanged(int)} may bind the item to a new ViewHolder and execute a transition. + * This causes flickering and breaks the download animation that stores the old progress in the View. + * Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}. + * We actually ignore the payload and always do a full bind but calling the partial bind method ensures + * that ViewHolders are always re-used. + * + * @param position Position of the item that has changed + */ + public void notifyItemChangedCompat(int position) { + notifyItemChanged(position, "foo"); + } + + @Nullable + public FeedItem getLongPressedItem() { + return longPressedItem; + } + + @Override + public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + MenuInflater inflater = mainActivityRef.get().getMenuInflater(); + if (longPressedItem == null) { + return; + } + menu.clear(); + inflater.inflate(R.menu.feeditemlist_context, menu); + menu.setHeaderTitle(longPressedItem.getTitle()); + FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item); + } + + +} diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java index a6252522a..c1df44da3 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/NavListAdapter.java @@ -36,6 +36,7 @@ import de.danoeh.antennapod.fragment.NavDrawerFragment; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; +import de.danoeh.antennapod.ui.home.HomeFragment; import org.apache.commons.lang3.ArrayUtils; import java.lang.ref.WeakReference; @@ -112,6 +113,8 @@ public class NavListAdapter extends RecyclerView.Adapter private @DrawableRes int getDrawable(String tag) { switch (tag) { + case HomeFragment.TAG: + return R.drawable.ic_home; case QueueFragment.TAG: return R.drawable.ic_playlist_play; case InboxFragment.TAG: diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index 95f08a838..7d8cadd02 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -13,7 +13,6 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -29,18 +28,19 @@ import de.danoeh.antennapod.activity.PreferenceActivity; import de.danoeh.antennapod.adapter.NavListAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.QueueEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.dialog.TagSettingsDialog; -import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.storage.NavDrawerData; import de.danoeh.antennapod.dialog.RemoveFeedDialog; -import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; import de.danoeh.antennapod.dialog.RenameItemDialog; +import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; +import de.danoeh.antennapod.dialog.TagSettingsDialog; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.ui.home.HomeFragment; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -65,6 +65,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS public static final String TAG = "NavDrawerFragment"; public static final String[] NAV_DRAWER_TAGS = { + HomeFragment.TAG, QueueFragment.TAG, InboxFragment.TAG, AllEpisodesFragment.TAG, @@ -430,7 +431,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS public static String getLastNavFragment(Context context) { SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); - String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, QueueFragment.TAG); + String lastFragment = prefs.getString(PREF_LAST_FRAGMENT_TAG, HomeFragment.TAG); Log.d(TAG, "getLastNavFragment() -> " + lastFragment); return lastFragment; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java index 0142498a8..b40fa4281 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java @@ -27,7 +27,7 @@ import com.google.android.material.chip.Chip; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; -import de.danoeh.antennapod.adapter.FeedSearchResultAdapter; +import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.DownloaderUpdate; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; @@ -65,7 +65,7 @@ public class SearchFragment extends Fragment { private static final int SEARCH_DEBOUNCE_INTERVAL = 1500; private EpisodeItemListAdapter adapter; - private FeedSearchResultAdapter adapterFeeds; + private HorizontalFeedListAdapter adapterFeeds; private Disposable disposable; private ProgressBar progressBar; private EmptyViewHandler emptyViewHandler; @@ -144,7 +144,7 @@ public class SearchFragment extends Fragment { LinearLayoutManager layoutManagerFeeds = new LinearLayoutManager(getActivity()); layoutManagerFeeds.setOrientation(RecyclerView.HORIZONTAL); recyclerViewFeeds.setLayoutManager(layoutManagerFeeds); - adapterFeeds = new FeedSearchResultAdapter((MainActivity) getActivity()); + adapterFeeds = new HorizontalFeedListAdapter((MainActivity) getActivity()); recyclerViewFeeds.setAdapter(adapterFeeds); emptyViewHandler = new EmptyViewHandler(getContext()); diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java new file mode 100644 index 000000000..27419c8a2 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java @@ -0,0 +1,185 @@ +package de.danoeh.antennapod.ui.home; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentContainerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.download.AutoUpdateManager; +import de.danoeh.antennapod.databinding.HomeFragmentBinding; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.fragment.SearchFragment; +import de.danoeh.antennapod.ui.home.sections.DownloadsSection; +import de.danoeh.antennapod.ui.home.sections.EpisodesSurpriseSection; +import de.danoeh.antennapod.ui.home.sections.InboxSection; +import de.danoeh.antennapod.ui.home.sections.QueueSection; +import de.danoeh.antennapod.ui.home.sections.SubscriptionsSection; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Shows unread or recently published episodes + */ +public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickListener { + + public static final String TAG = "HomeFragment"; + public static final String PREF_NAME = "PrefHomeFragment"; + public static final String PREF_HIDDEN_SECTIONS = "PrefHomeSectionsString"; + + private static final String KEY_UP_ARROW = "up_arrow"; + private boolean displayUpArrow; + private HomeFragmentBinding viewBinding; + private boolean isUpdatingFeeds = false; + private Disposable disposable; + + @NonNull + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + viewBinding = HomeFragmentBinding.inflate(inflater); + viewBinding.toolbar.inflateMenu(R.menu.home); + viewBinding.toolbar.setOnMenuItemClickListener(this); + if (savedInstanceState != null) { + displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW); + } + ((MainActivity) requireActivity()).setupToolbarToggle(viewBinding.toolbar, displayUpArrow); + refreshToolbarState(); + populateSectionList(); + updateWelcomeScreenVisibility(); + return viewBinding.getRoot(); + } + + private void populateSectionList() { + viewBinding.homeContainer.removeAllViews(); + + List hiddenSections = getHiddenSections(getContext()); + String[] sectionTags = getResources().getStringArray(R.array.home_section_tags); + for (String sectionTag : sectionTags) { + if (hiddenSections.contains(sectionTag)) { + continue; + } + addSection(getSection(sectionTag)); + } + } + + private void addSection(Fragment section) { + FragmentContainerView containerView = new FragmentContainerView(getContext()); + containerView.setId(View.generateViewId()); + viewBinding.homeContainer.addView(containerView); + getChildFragmentManager().beginTransaction().add(containerView.getId(), section).commit(); + } + + private Fragment getSection(String tag) { + switch (tag) { + case QueueSection.TAG: + return new QueueSection(); + case InboxSection.TAG: + return new InboxSection(); + case EpisodesSurpriseSection.TAG: + return new EpisodesSurpriseSection(); + case SubscriptionsSection.TAG: + return new SubscriptionsSection(); + case DownloadsSection.TAG: + return new DownloadsSection(); + default: + return null; + } + } + + public static List getHiddenSections(Context context) { + SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); + String hiddenSectionsString = prefs.getString(HomeFragment.PREF_HIDDEN_SECTIONS, ""); + return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenSectionsString, ","))); + } + + private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker = + () -> DownloadService.isRunning && DownloadService.isDownloadingFeeds(); + + private void refreshToolbarState() { + isUpdatingFeeds = MenuItemUtils.updateRefreshMenuItem(viewBinding.toolbar.getMenu(), + R.id.refresh_item, updateRefreshMenuItemChecker); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(DownloadEvent event) { + Log.d(TAG, "onEventMainThread() called with DownloadEvent"); + if (event.hasChangedFeedUpdateStatus(isUpdatingFeeds)) { + refreshToolbarState(); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.homesettings_items) { + HomeSectionsSettingsDialog.open(getContext(), (dialogInterface, i) -> populateSectionList()); + return true; + } else if (item.getItemId() == R.id.refresh_item) { + AutoUpdateManager.runImmediate(requireContext()); + return true; + } else if (item.getItemId() == R.id.action_search) { + ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + outState.putBoolean(KEY_UP_ARROW, displayUpArrow); + super.onSaveInstanceState(outState); + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + updateWelcomeScreenVisibility(); + } + + private void updateWelcomeScreenVisibility() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getNavDrawerData().items.size()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(numSubscriptions -> { + viewBinding.welcomeContainer.setVisibility(numSubscriptions == 0 ? View.VISIBLE : View.GONE); + viewBinding.homeContainer.setVisibility(numSubscriptions == 0 ? View.GONE : View.VISIBLE); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java new file mode 100644 index 000000000..dd48f0ada --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSection.java @@ -0,0 +1,86 @@ +package de.danoeh.antennapod.ui.home; + +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; +import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; +import de.danoeh.antennapod.databinding.HomeSectionBinding; +import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler; +import de.danoeh.antennapod.model.feed.FeedItem; +import org.greenrobot.eventbus.EventBus; + +/** + * Section on the HomeFragment + */ +public abstract class HomeSection extends Fragment implements View.OnCreateContextMenuListener { + public static final String TAG = "HomeSection"; + protected HomeSectionBinding viewBinding; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + viewBinding = HomeSectionBinding.inflate(inflater); + viewBinding.titleLabel.setText(getSectionTitle()); + viewBinding.moreButton.setText(getString(R.string.navigate_arrows, getMoreLinkTitle())); + viewBinding.moreButton.setOnClickListener((view) -> handleMoreClick()); + if (TextUtils.isEmpty(getMoreLinkTitle())) { + viewBinding.moreButton.setVisibility(View.INVISIBLE); + } + return viewBinding.getRoot(); + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + if (!getUserVisibleHint() || !isVisible() || !isMenuVisible()) { + // The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones. + // Apparently, none of the visibility check method works reliably on its own, so we just use all. + return false; + } + FeedItem longPressedItem; + if (viewBinding.recyclerView.getAdapter() instanceof EpisodeItemListAdapter) { + EpisodeItemListAdapter adapter = (EpisodeItemListAdapter) viewBinding.recyclerView.getAdapter(); + longPressedItem = adapter.getLongPressedItem(); + } else if (viewBinding.recyclerView.getAdapter() instanceof HorizontalItemListAdapter) { + HorizontalItemListAdapter adapter = (HorizontalItemListAdapter) viewBinding.recyclerView.getAdapter(); + longPressedItem = adapter.getLongPressedItem(); + } else { + return false; + } + + if (longPressedItem == null) { + Log.i(TAG, "Selected item or listAdapter was null, ignoring selection"); + return super.onContextItemSelected(item); + } + return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), longPressedItem); + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + registerForContextMenu(viewBinding.recyclerView); + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + unregisterForContextMenu(viewBinding.recyclerView); + } + + protected abstract String getSectionTitle(); + + protected abstract String getMoreLinkTitle(); + + protected abstract void handleMoreClick(); +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java new file mode 100644 index 000000000..aa8ef8de3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeSectionsSettingsDialog.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.ui.home; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.text.TextUtils; +import androidx.appcompat.app.AlertDialog; +import de.danoeh.antennapod.R; + +import java.util.List; + +public class HomeSectionsSettingsDialog { + public static void open(Context context, DialogInterface.OnClickListener onSettingsChanged) { + final List hiddenSections = HomeFragment.getHiddenSections(context); + String[] sectionLabels = context.getResources().getStringArray(R.array.home_section_titles); + String[] sectionTags = context.getResources().getStringArray(R.array.home_section_tags); + final boolean[] checked = new boolean[sectionLabels.length]; + for (int i = 0; i < sectionLabels.length; i++) { + String tag = sectionTags[i]; + if (!hiddenSections.contains(tag)) { + checked[i] = true; + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.configure_home); + builder.setMultiChoiceItems(sectionLabels, checked, (dialog, which, isChecked) -> { + if (isChecked) { + hiddenSections.remove(sectionTags[which]); + } else { + hiddenSections.add(sectionTags[which]); + } + }); + builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> { + SharedPreferences prefs = context.getSharedPreferences(HomeFragment.PREF_NAME, Context.MODE_PRIVATE); + prefs.edit().putString(HomeFragment.PREF_HIDDEN_SECTIONS, TextUtils.join(",", hiddenSections)).apply(); + onSettingsChanged.onClick(dialog, which); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.create().show(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java new file mode 100644 index 000000000..42b97294f --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/DownloadsSection.java @@ -0,0 +1,125 @@ +package de.danoeh.antennapod.ui.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; +import de.danoeh.antennapod.core.event.DownloadLogEvent; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.fragment.CompletedDownloadsFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.ui.home.HomeSection; +import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +public class DownloadsSection extends HomeSection { + public static final String TAG = "DownloadsSection"; + private static final int NUM_EPISODES = 2; + private EpisodeItemListAdapter adapter; + private List items; + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.recyclerView.setPadding(0, 0, 0, 0); + viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); + viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false)); + viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool()); + adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, DownloadsSection.this::onContextItemSelected); + } + }; + adapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setAdapter(adapter); + loadItems(); + return view; + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new CompletedDownloadsFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (adapter == null) { + return; + } + for (int i = 0; i < adapter.getItemCount(); i++) { + EpisodeItemViewHolder holder = (EpisodeItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onDownloadLogChanged(DownloadLogEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_downloads_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.downloads_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(DBReader::getDownloadedItems) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(downloads -> { + if (downloads.size() > NUM_EPISODES) { + downloads = downloads.subList(0, NUM_EPISODES); + } + items = downloads; + adapter.setDummyViews(0); + adapter.updateItems(items); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java new file mode 100644 index 000000000..680bb5ef4 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/EpisodesSurpriseSection.java @@ -0,0 +1,155 @@ +package de.danoeh.antennapod.ui.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.DownloaderUpdate; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.fragment.AllEpisodesFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.ui.home.HomeSection; +import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; +import java.util.Random; + +public class EpisodesSurpriseSection extends HomeSection { + public static final String TAG = "EpisodesSurpriseSection"; + private static final int NUM_EPISODES = 8; + private static int seed = 0; + private HorizontalItemListAdapter listAdapter; + private Disposable disposable; + private List episodes; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.shuffleButton.setVisibility(View.VISIBLE); + viewBinding.shuffleButton.setOnClickListener(v -> { + seed = new Random().nextInt(); + viewBinding.recyclerView.scrollToPosition(0); + loadItems(); + }); + listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, EpisodesSurpriseSection.this::onContextItemSelected); + } + }; + listAdapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false)); + viewBinding.recyclerView.setAdapter(listAdapter); + if (seed == 0) { + seed = new Random().nextInt(); + } + loadItems(); + return view; + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new AllEpisodesFragment()); + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_surprise_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.episodes_label); + } + + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (episodes == null) { + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(episodes, item.getId()); + if (pos >= 0) { + episodes.remove(pos); + episodes.add(pos, item); + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(DownloadEvent event) { + Log.d(TAG, "onEventMainThread() called with DownloadEvent"); + DownloaderUpdate update = event.update; + if (listAdapter != null && update.mediaIds.length > 0) { + for (long mediaId : update.mediaIds) { + int pos = FeedItemUtil.indexOfItemWithMediaId(episodes, mediaId); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (listAdapter == null) { + return; + } + for (int i = 0; i < listAdapter.getItemCount(); i++) { + HorizontalItemViewHolder holder = (HorizontalItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getRandomEpisodes(NUM_EPISODES, seed)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(episodes -> { + this.episodes = episodes; + listAdapter.setDummyViews(0); + listAdapter.updateData(episodes); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java new file mode 100644 index 000000000..d05735acb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/InboxSection.java @@ -0,0 +1,122 @@ +package de.danoeh.antennapod.ui.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.DownloaderUpdate; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; +import de.danoeh.antennapod.fragment.InboxFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.storage.database.PodDBAdapter; +import de.danoeh.antennapod.ui.home.HomeSection; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +public class InboxSection extends HomeSection { + public static final String TAG = "InboxSection"; + private static final int NUM_EPISODES = 2; + private EpisodeItemListAdapter adapter; + private List items; + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.recyclerView.setPadding(0, 0, 0, 0); + viewBinding.recyclerView.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER); + viewBinding.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false)); + viewBinding.recyclerView.setRecycledViewPool(((MainActivity) requireActivity()).getRecycledViewPool()); + adapter = new EpisodeItemListAdapter((MainActivity) requireActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, InboxSection.this::onContextItemSelected); + } + }; + adapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setAdapter(adapter); + loadItems(); + return view; + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new InboxFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + loadItems(); + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(DownloadEvent event) { + Log.d(TAG, "onEventMainThread() called with DownloadEvent"); + DownloaderUpdate update = event.update; + if (adapter != null && update.mediaIds.length > 0) { + for (long mediaId : update.mediaIds) { + int pos = FeedItemUtil.indexOfItemWithMediaId(items, mediaId); + if (pos >= 0) { + adapter.notifyItemChangedCompat(pos); + } + } + } + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_new_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.inbox_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> + new Pair<>(DBReader.getNewItemsList(0, NUM_EPISODES), + PodDBAdapter.getInstance().getNumberOfNewItems())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(data -> { + items = data.first; + adapter.setDummyViews(0); + adapter.updateItems(items); + viewBinding.numNewItemsLabel.setVisibility(View.VISIBLE); + viewBinding.numNewItemsLabel.setText(String.valueOf(data.second)); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java new file mode 100644 index 000000000..efff7927e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/QueueSection.java @@ -0,0 +1,150 @@ +package de.danoeh.antennapod.ui.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.HorizontalItemListAdapter; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.DownloaderUpdate; +import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.FeedItemEvent; +import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.event.QueueEvent; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.fragment.QueueFragment; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.ui.home.HomeSection; +import de.danoeh.antennapod.view.viewholder.HorizontalItemViewHolder; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.List; + +public class QueueSection extends HomeSection { + public static final String TAG = "QueueSection"; + private static final int NUM_EPISODES = 8; + private HorizontalItemListAdapter listAdapter; + private Disposable disposable; + private List queue; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + listAdapter = new HorizontalItemListAdapter((MainActivity) getActivity()) { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuItemUtils.setOnClickListeners(menu, QueueSection.this::onContextItemSelected); + } + }; + listAdapter.setDummyViews(NUM_EPISODES); + viewBinding.recyclerView.setLayoutManager( + new LinearLayoutManager(getContext(), RecyclerView.HORIZONTAL, false)); + viewBinding.recyclerView.setAdapter(listAdapter); + loadItems(); + return view; + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new QueueFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onQueueChanged(QueueEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onPlayerStatusChanged(PlayerStatusEvent event) { + loadItems(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(FeedItemEvent event) { + Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]"); + if (queue == null) { + return; + } + for (int i = 0, size = event.items.size(); i < size; i++) { + FeedItem item = event.items.get(i); + int pos = FeedItemUtil.indexOfItemWithId(queue, item.getId()); + if (pos >= 0) { + queue.remove(pos); + queue.add(pos, item); + listAdapter.notifyItemChangedCompat(pos); + } + } + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + public void onEventMainThread(DownloadEvent event) { + Log.d(TAG, "onEventMainThread() called with DownloadEvent"); + DownloaderUpdate update = event.update; + if (listAdapter != null && update.mediaIds.length > 0) { + for (long mediaId : update.mediaIds) { + int pos = FeedItemUtil.indexOfItemWithMediaId(queue, mediaId); + if (pos >= 0) { + listAdapter.notifyItemChangedCompat(pos); + } + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEventMainThread(PlaybackPositionEvent event) { + if (listAdapter == null) { + return; + } + for (int i = 0; i < listAdapter.getItemCount(); i++) { + HorizontalItemViewHolder holder = (HorizontalItemViewHolder) + viewBinding.recyclerView.findViewHolderForAdapterPosition(i); + if (holder != null && holder.isCurrentlyPlayingItem()) { + holder.notifyPlaybackPositionUpdated(event); + break; + } + } + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_continue_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.queue_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getPausedQueue(NUM_EPISODES)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(queue -> { + this.queue = queue; + listAdapter.setDummyViews(0); + listAdapter.updateData(queue); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java new file mode 100644 index 000000000..81dddbff3 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/sections/SubscriptionsSection.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.ui.home.sections; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.HorizontalFeedListAdapter; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.event.FeedListUpdateEvent; +import de.danoeh.antennapod.fragment.SubscriptionFragment; +import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.ui.home.HomeSection; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SubscriptionsSection extends HomeSection { + public static final String TAG = "SubscriptionsSection"; + private static final int NUM_FEEDS = 8; + private HorizontalFeedListAdapter listAdapter; + private Disposable disposable; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + final View view = super.onCreateView(inflater, container, savedInstanceState); + viewBinding.recyclerView.setLayoutManager( + new LinearLayoutManager(getActivity(), RecyclerView.HORIZONTAL, false)); + listAdapter = new HorizontalFeedListAdapter((MainActivity) getActivity()); + listAdapter.setDummyViews(NUM_FEEDS); + viewBinding.recyclerView.setAdapter(listAdapter); + loadItems(); + return view; + } + + @Override + protected void handleMoreClick() { + ((MainActivity) requireActivity()).loadChildFragment(new SubscriptionFragment()); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onFeedListChanged(FeedListUpdateEvent event) { + loadItems(); + } + + @Override + protected String getSectionTitle() { + return getString(R.string.home_classics_title); + } + + @Override + protected String getMoreLinkTitle() { + return getString(R.string.subscriptions_label); + } + + private void loadItems() { + if (disposable != null) { + disposable.dispose(); + } + disposable = Observable.fromCallable(() -> DBReader.getStatistics(true, 0, Long.MAX_VALUE).feedTime) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(statisticsData -> { + Collections.sort(statisticsData, (item1, item2) -> + Long.compare(item2.timePlayed, item1.timePlayed)); + List feeds = new ArrayList<>(); + for (int i = 0; i < statisticsData.size() && i < NUM_FEEDS; i++) { + feeds.add(statisticsData.get(i).feed); + } + listAdapter.setDummyViews(0); + listAdapter.updateData(feeds); + }, error -> Log.e(TAG, Log.getStackTraceString(error))); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java index 3839241d7..bc29740b0 100644 --- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java @@ -198,6 +198,30 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder { } } + public void bindDummy() { + container.setAlpha(0.1f); + secondaryActionIcon.setImageDrawable(null); + isInbox.setVisibility(View.VISIBLE); + isVideo.setVisibility(View.GONE); + isFavorite.setVisibility(View.GONE); + isInQueue.setVisibility(View.GONE); + title.setText("███████"); + pubDate.setText("████"); + duration.setText("████"); + secondaryActionProgress.setPercentage(0, null); + progressBar.setVisibility(View.GONE); + position.setVisibility(View.GONE); + dragHandle.setVisibility(View.GONE); + size.setText(""); + itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground)); + placeholder.setText(""); + new CoverLoader(activity) + .withResource(ThemeUtils.getDrawableFromAttr(activity, android.R.attr.textColorSecondary)) + .withPlaceholderView(placeholder) + .withCoverView(cover) + .load(); + } + private void updateDuration(PlaybackPositionEvent event) { int currentPosition = event.getPosition(); int timeDuration = event.getDuration(); diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java new file mode 100644 index 000000000..9723417ce --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/HorizontalItemViewHolder.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.view.viewholder; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.adapter.CoverLoader; +import de.danoeh.antennapod.adapter.actionbutton.ItemActionButton; +import de.danoeh.antennapod.core.feed.util.ImageResourceUtils; +import de.danoeh.antennapod.core.service.download.DownloadRequest; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.util.DateFormatter; +import de.danoeh.antennapod.core.util.FeedItemUtil; +import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; +import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.ui.common.CircularProgressBar; +import de.danoeh.antennapod.ui.common.SquareImageView; + +public class HorizontalItemViewHolder extends RecyclerView.ViewHolder { + public final View card; + public final ImageView secondaryActionIcon; + private final SquareImageView cover; + private final TextView title; + private final TextView date; + private final ProgressBar progressBar; + private final CircularProgressBar circularProgressBar; + + private final MainActivity activity; + private FeedItem item; + + public HorizontalItemViewHolder(MainActivity activity, ViewGroup parent) { + super(LayoutInflater.from(activity).inflate(R.layout.horizontal_itemlist_item, parent, false)); + this.activity = activity; + + card = itemView.findViewById(R.id.card); + cover = itemView.findViewById(R.id.cover); + title = itemView.findViewById(R.id.titleLabel); + date = itemView.findViewById(R.id.dateLabel); + secondaryActionIcon = itemView.findViewById(R.id.secondaryActionIcon); + circularProgressBar = itemView.findViewById(R.id.circularProgressBar); + progressBar = itemView.findViewById(R.id.progressBar); + itemView.setTag(this); + } + + public void bind(FeedItem item) { + this.item = item; + + card.setAlpha(1.0f); + new CoverLoader(activity) + .withUri(ImageResourceUtils.getEpisodeListImageLocation(item)) + .withFallbackUri(item.getFeed().getImageUrl()) + .withCoverView(cover) + .load(); + title.setText(item.getTitle()); + date.setText(DateFormatter.formatAbbrev(activity, item.getPubDate())); + ItemActionButton actionButton = ItemActionButton.forItem(item); + actionButton.configure(secondaryActionIcon, secondaryActionIcon, activity); + secondaryActionIcon.setFocusable(false); + + FeedMedia media = item.getMedia(); + if (media == null) { + circularProgressBar.setPercentage(0, item); + } else { + if (item.getMedia().getDuration() > 0) { + progressBar.setProgress(100 * item.getMedia().getPosition() / item.getMedia().getDuration()); + } + if (DownloadService.isDownloadingFile(media.getDownload_url())) { + final DownloadRequest downloadRequest = DownloadService.findRequest(media.getDownload_url()); + float percent = 0.01f * downloadRequest.getProgressPercent(); + circularProgressBar.setPercentage(Math.max(percent, 0.01f), item); + } else if (media.isDownloaded()) { + circularProgressBar.setPercentage(1, item); // Do not animate 100% -> 0% + } else { + circularProgressBar.setPercentage(0, item); // Animate X% -> 0% + } + } + } + + public void bindDummy() { + card.setAlpha(0.1f); + new CoverLoader(activity) + .withResource(android.R.color.transparent) + .withCoverView(cover) + .load(); + title.setText("████ █████"); + date.setText("███"); + secondaryActionIcon.setImageDrawable(null); + circularProgressBar.setPercentage(0, null); + progressBar.setProgress(50); + } + + public boolean isCurrentlyPlayingItem() { + return item.getMedia() != null && FeedItemUtil.isCurrentlyPlaying(item.getMedia()); + } + + public void notifyPlaybackPositionUpdated(PlaybackPositionEvent event) { + progressBar.setProgress((int) (100.0 * event.getPosition() / event.getDuration())); + } +} diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml new file mode 100644 index 000000000..040e3df34 --- /dev/null +++ b/app/src/main/res/layout/home_fragment.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/home_section.xml b/app/src/main/res/layout/home_section.xml new file mode 100644 index 000000000..783688f92 --- /dev/null +++ b/app/src/main/res/layout/home_section.xml @@ -0,0 +1,91 @@ + + + + + + + + + +