Home Screen (#5864)

Co-authored-by: ueen <ueli.sarnighausen@online.de>
This commit is contained in:
ByteHamster 2022-08-27 11:19:34 +02:00 committed by GitHub
parent ec92722c04
commit 77104c9038
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1761 additions and 58 deletions

View File

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

View File

@ -38,6 +38,7 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
private List<FeedItem> 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<EpisodeItemViewHol
setHasStableIds(true);
}
public void setDummyViews(int dummyViews) {
this.dummyViews = dummyViews;
}
public void updateItems(List<FeedItem> items) {
episodes = items;
notifyDataSetChanged();
@ -64,6 +69,11 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
@Override
public final void onBindViewHolder(EpisodeItemViewHolder holder, int pos) {
if (pos >= 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<EpisodeItemViewHol
@Override
public long getItemId(int position) {
if (position >= 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) {

View File

@ -11,20 +11,25 @@ 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 FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResultAdapter.Holder> {
public class HorizontalFeedListAdapter extends RecyclerView.Adapter<HorizontalFeedListAdapter.Holder> {
private final WeakReference<MainActivity> mainActivityRef;
private final List<Feed> data = new ArrayList<>();
private int dummyViews = 0;
public FeedSearchResultAdapter(MainActivity mainActivity) {
public HorizontalFeedListAdapter(MainActivity mainActivity) {
this.mainActivityRef = new WeakReference<>(mainActivity);
}
public void setDummyViews(int dummyViews) {
this.dummyViews = dummyViews;
}
public void updateData(List<Feed> newData) {
data.clear();
data.addAll(newData);
@ -34,12 +39,21 @@ public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResu
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View convertView = View.inflate(mainActivityRef.get(), R.layout.searchlist_item_feed, null);
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 ->
@ -56,12 +70,15 @@ public class FeedSearchResultAdapter extends RecyclerView.Adapter<FeedSearchResu
@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 data.size();
return dummyViews + data.size();
}
static class Holder extends RecyclerView.ViewHolder {

View File

@ -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<HorizontalItemViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<MainActivity> mainActivityRef;
private List<FeedItem> 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<FeedItem> 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);
}
}

View File

@ -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<NavListAdapter.Holder>
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:

View File

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

View File

@ -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());

View File

@ -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<String> 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<String> 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)));
}
}

View File

@ -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();
}

View File

@ -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<String> 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();
}
}

View File

@ -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<FeedItem> 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)));
}
}

View File

@ -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<FeedItem> 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)));
}
}

View File

@ -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<FeedItem> 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)));
}
}

View File

@ -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<FeedItem> 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)));
}
}

View File

@ -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<Feed> 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)));
}
}

View File

@ -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();

View File

@ -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()));
}
}

View File

@ -0,0 +1,75 @@
<?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">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:title="@string/home_label"
app:navigationIcon="?homeAsUpIndicator" />
<LinearLayout
android:id="@+id/welcomeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:paddingHorizontal="32dp">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginBottom="8dp"
android:layout_gravity="start"
android:src="@drawable/ic_curved_arrow" />
<ImageView
android:id="@+id/icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_welcome_title"
android:layout_marginBottom="8dp"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_welcome_text"
android:layout_gravity="center_horizontal"
android:textAlignment="center"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/homeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="12dp" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical"
android:paddingBottom="4dp">
<TextView
android:id="@+id/titleLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/moreButton"
android:layout_alignBottom="@id/moreButton"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginStart="16dp"
android:gravity="center"
android:textAlignment="center"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
tools:text="Title" />
<ImageButton
android:id="@+id/shuffleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/titleLabel"
android:layout_alignParentTop="true"
android:layout_marginVertical="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_toEndOf="@+id/titleLabel"
android:layout_toRightOf="@+id/titleLabel"
android:background="?attr/selectableItemBackgroundBorderless"
android:visibility="gone"
tools:visibility="visible"
app:srcCompat="@drawable/ic_shuffle" />
<TextView
android:id="@+id/numNewItemsLabel"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_alignBottom="@+id/titleLabel"
android:layout_alignParentTop="true"
android:layout_marginVertical="12dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_toEndOf="@+id/titleLabel"
android:layout_toRightOf="@+id/titleLabel"
android:background="@drawable/bg_pill"
android:gravity="center"
android:paddingHorizontal="8dp"
android:textAlignment="center"
android:textColor="?attr/colorPrimary"
android:textSize="16sp"
android:visibility="gone"
tools:visibility="visible"
tools:text="6" />
<Button
android:id="@+id/moreButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:paddingVertical="0dp"
android:layout_marginEnd="16dp"
tools:text="@string/discover_more"
style="@style/Widget.MaterialComponents.Button.TextButton" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/moreButton"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:clipToPadding="false"
android:clipToOutline="false"
android:clipChildren="false"
android:paddingHorizontal="16dp" />
</RelativeLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="96dp"
android:padding="4dp"
android:clipToPadding="false"
android:clipToOutline="false"
android:clipChildren="false">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="@color/non_square_icon_background"
app:cardCornerRadius="16dp"
app:cardPreventCornerOverlap="false"
app:cardElevation="2dp">
<de.danoeh.antennapod.ui.common.SquareImageView
android:id="@+id/discovery_cover"
android:layout_width="match_parent"
android:layout_height="96dp"
android:elevation="4dp"
android:outlineProvider="bounds"
android:foreground="?android:attr/selectableItemBackground"
android:background="?android:attr/windowBackground"
squareImageView:direction="height" />
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@ -0,0 +1,123 @@
<?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"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:padding="4dp">
<androidx.cardview.widget.CardView
android:id="@+id/card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
app:cardCornerRadius="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/background_elevated"
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#dddddd"
android:orientation="vertical">
<FrameLayout
android:layout_width="128dp"
android:layout_height="128dp"
android:background="@color/image_readability_tint">
<de.danoeh.antennapod.ui.common.SquareImageView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:outlineProvider="bounds"
tools:src="@tools:sample/avatars"
squareImageView:direction="width" />
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:padding="3dp"
app:srcCompat="@drawable/bg_circle" />
<de.danoeh.antennapod.ui.common.CircularProgressBar
android:id="@+id/circularProgressBar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="8dp"
android:layout_gravity="bottom|end"
app:foregroundColor="?attr/colorOnPrimary" />
<ImageView
android:id="@+id/secondaryActionIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="bottom|end"
android:layout_margin="8dp"
android:padding="12dp"
android:clickable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
app:tintMode="src_atop"
app:tint="?attr/colorOnPrimary"
app:srcCompat="@drawable/ic_play_24dp" />
</FrameLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="4dp"
android:max="100"
android:layout_gravity="bottom"
style="?attr/progressBarTheme"
tools:background="@android:color/holo_blue_light" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/titleLabel"
android:layout_width="128dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:paddingHorizontal="4dp"
android:layout_marginTop="4dp"
android:lines="2"
android:singleLine="false"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
tools:text="@sample/episodes.json/data/title" />
<TextView
android:id="@+id/dateLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:paddingHorizontal="4dp"
android:singleLine="true"
android:textAlignment="textStart"
android:textSize="14sp"
style="@style/AntennaPod.TextView.ListItemSecondaryTitle" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

View File

@ -13,7 +13,8 @@
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
android:layout_alignParentTop="true"
app:title="@string/queue_label" />
app:title="@string/queue_label"
app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/info_bar"

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
android:layout_width="match_parent"
android:layout_height="96dp"
android:padding="4dp"
android:clipToPadding="false">
<de.danoeh.antennapod.ui.common.SquareImageView
android:id="@+id/discovery_cover"
android:layout_width="match_parent"
android:layout_height="96dp"
android:elevation="4dp"
android:outlineProvider="bounds"
android:foreground="?android:attr/selectableItemBackground"
android:background="?android:attr/windowBackground"
squareImageView:direction="height" />
</LinearLayout>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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">
@ -10,7 +11,8 @@
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
android:layout_alignParentTop="true" />
android:layout_alignParentTop="true"
app:navigationIcon="?homeAsUpIndicator" />
<de.danoeh.antennapod.view.EpisodeItemListRecyclerView
android:id="@+id/recyclerView"

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
custom:showAsAction="always"
android:title="@string/search_label"/>
<item
android:id="@+id/refresh_item"
android:title="@string/refresh_label"
android:menuCategory="container"
custom:showAsAction="always"
android:icon="@drawable/ic_refresh"/>
<item
android:id="@+id/homesettings_items"
android:icon="@drawable/ic_settings"
android:menuCategory="container"
android:title="@string/configure_home"
custom:showAsAction="never"/>
</menu>

View File

@ -396,6 +396,18 @@ public final class DBReader {
}
}
public static List<FeedItem> getRandomEpisodes(int limit, int seed) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try (Cursor cursor = adapter.getRandomEpisodesCursor(limit, seed)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
} finally {
adapter.close();
}
}
public static int getTotalEpisodeCount(FeedItemFilter filter) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
@ -615,6 +627,19 @@ public final class DBReader {
}
}
@NonNull
public static List<FeedItem> getPausedQueue(int limit) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try (Cursor cursor = adapter.getPausedQueueCursor(limit)) {
List<FeedItem> items = extractItemlistFromCursor(adapter, cursor);
loadAdditionalFeedItemListData(items);
return items;
} finally {
adapter.close();
}
}
/**
* Loads a specific FeedItem from the database.
*

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorPrimary" />
<corners android:radius="30dp" />
<size android:width="60dp" android:height="60dp"/>
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="1dp"
android:color="?attr/colorPrimary" />
<corners android:radius="20dp" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path android:strokeColor="?attr/action_icon_color" android:strokeAlpha="0.4" android:strokeWidth="0.4" android:pathData="M 24.563 19.667 C 20.794 22.382 13.26 21.82 11.04 17.74 C 8.82 13.66 16.36 4.77 20.17 8.59 C 23.98 12.4 16.78 16.34 11.93 15.72 C 7.08 15.1 4.792 10.756 2.54 4.87"/>
<path android:fillColor="?attr/action_icon_color" android:fillAlpha="0.4" android:pathData="M 0.608 5.581 L 4.568 4.368 L 1.183 0.599" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/action_icon_color"
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/action_icon_color"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z"/>
</vector>

View File

@ -150,6 +150,7 @@
</string-array>
<string-array name="nav_drawer_titles">
<item>@string/home_label</item>
<item>@string/queue_label</item>
<item>@string/inbox_label</item>
<item>@string/episodes_label</item>
@ -188,6 +189,22 @@
<item>3</item>
</string-array>
<string-array name="home_section_titles">
<item>@string/home_continue_title</item>
<item>@string/home_new_title</item>
<item>@string/home_surprise_title</item>
<item>@string/home_classics_title</item>
<item>@string/home_downloads_title</item>
</string-array>
<string-array name="home_section_tags">
<item>QueueSection</item>
<item>InboxSection</item>
<item>EpisodesSurpriseSection</item>
<item>SubscriptionsSection</item>
<item>DownloadsSection</item>
</string-array>
<string-array name="media_player_options">
<item>@string/media_player_exoplayer_recommended</item>
<item>@string/media_player_builtin</item>
@ -265,6 +282,7 @@
</string-array>
<string-array name="back_button_go_to_pages">
<item>@string/home_label</item>
<item>@string/queue_label</item>
<item>@string/inbox_label</item>
<item>@string/episodes_label</item>
@ -272,6 +290,7 @@
</string-array>
<string-array name="back_button_go_to_pages_tags">
<item>HomeFragment</item>
<item>QueueFragment</item>
<item>InboxFragment</item>
<item>EpisodesFragment</item>

View File

@ -45,7 +45,7 @@ public class FeedItemFilter implements Serializable {
this(TextUtils.split(properties, ","));
}
public FeedItemFilter(String[] properties) {
public FeedItemFilter(String... properties) {
this.properties = properties;
// see R.arrays.feed_filter_values

View File

@ -994,6 +994,17 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
public final Cursor getPausedQueueCursor(int limit) {
//playback position > 0 (paused), rank by last played, then rest of queue
final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ " INNER JOIN " + TABLE_NAME_QUEUE
+ " ON " + SELECT_KEY_ITEM_ID + " = " + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM
+ " ORDER BY " + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + ">0 DESC , "
+ TABLE_NAME_FEED_MEDIA + "." + KEY_LAST_PLAYED_TIME + " DESC , " + TABLE_NAME_QUEUE + "." + KEY_ID
+ " LIMIT " + limit;
return db.rawQuery(query, null);
}
public final Cursor getFavoritesCursor(int offset, int limit) {
final String query = SELECT_FEED_ITEMS_AND_MEDIA
+ " INNER JOIN " + TABLE_NAME_FAVORITES
@ -1051,6 +1062,25 @@ public class PodDBAdapter {
return db.rawQuery(query, null);
}
public Cursor getRandomEpisodesCursor(int limit, int seed) {
final String allItemsRandomOrder = SELECT_FEED_ITEMS_AND_MEDIA
+ " WHERE (" + KEY_READ + " = " + FeedItem.NEW + " OR " + KEY_READ + " = " + FeedItem.UNPLAYED + ") "
// Only from the last two years. Older episodes frequently contain broken covers and stuff like that
+ " AND " + KEY_PUBDATE + " > " + (System.currentTimeMillis() - 1000L * 3600L * 24L * 356L * 2)
+ " ORDER BY " + randomEpisodeNumber(seed);
final String query = "SELECT * FROM (" + allItemsRandomOrder + ")"
+ " GROUP BY " + KEY_FEED
+ " ORDER BY " + randomEpisodeNumber(seed * 3) + " DESC LIMIT " + limit;
return db.rawQuery(query, null);
}
/**
* SQLite does not support random seeds. Create our own "random" number based on that seed and the item ID
*/
private String randomEpisodeNumber(int seed) {
return "((" + SELECT_KEY_ITEM_ID + " * " + seed + ") % 46471)";
}
public final Cursor getTotalEpisodeCountCursor(FeedItemFilter filter) {
String filterQuery = FeedItemFilterQuery.generateFrom(filter);
String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery;

View File

@ -10,6 +10,7 @@
<string name="statistics_label">Statistics</string>
<string name="add_feed_label">Add Podcast</string>
<string name="episodes_label">Episodes</string>
<string name="home_label">Home</string>
<string name="queue_label">Queue</string>
<string name="inbox_label">Inbox</string>
<string name="favorite_episodes_label">Favorites</string>
@ -50,6 +51,17 @@
<string name="statistics_counting_range">Played between %1$s and %2$s</string>
<string name="statistics_counting_total">Played in total</string>
<!-- Home fragment -->
<string name="home_surprise_title">Get surprised</string>
<string name="home_classics_title">Check your classics</string>
<string name="home_continue_title">Continue listening</string>
<string name="home_new_title">Review the new</string>
<string name="home_downloads_title">Manage downloads</string>
<string name="home_welcome_title">Welcome to AntennaPod!</string>
<string name="home_welcome_text">You are not subscribed to any podcasts yet. Open the side menu to add a podcast.</string>
<string name="navigate_arrows">%s »</string>
<string name="configure_home">Configure Home Screen</string>
<!-- Download Statistics fragment -->
<string name="total_size_downloaded_podcasts">Total size of episodes on the device</string>