Merge pull request #4634 from AntennaPod/folders

Add subscriptions to tags/folders
This commit is contained in:
ByteHamster 2021-03-05 17:18:51 +01:00 committed by GitHub
commit dd0502c0d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 758 additions and 361 deletions

View File

@ -30,7 +30,6 @@ import java.util.List;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent;
@ -123,7 +122,7 @@ public class NavigationDrawerTest {
for (int i = 0; i < uiTestUtils.hostedFeeds.size(); i++) {
Feed f = uiTestUtils.hostedFeeds.get(i);
openNavDrawer();
onDrawerItem(withText(f.getTitle())).perform(scrollTo(), click());
onDrawerItem(withText(f.getTitle())).perform(click());
onView(isRoot()).perform(waitForView(allOf(isDescendantOfA(withId(R.id.appBar)),
withText(f.getTitle()), isDisplayed()), 1000));
}

View File

@ -18,9 +18,10 @@ import java.io.IOException;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static de.test.antennapod.EspressoTestUtils.onDrawerItem;
import static de.test.antennapod.EspressoTestUtils.openNavDrawer;
@ -61,7 +62,8 @@ public class TextOnlyFeedsTest {
uiTestUtils.addLocalFeedData(false);
final Feed feed = uiTestUtils.hostedFeeds.get(0);
openNavDrawer();
onDrawerItem(withText(feed.getTitle())).perform(scrollTo(), click());
onView(withId(R.id.nav_list)).perform(swipeUp());
onDrawerItem(withText(feed.getTitle())).perform(click());
onView(withText(feed.getItemAtIndex(0).getTitle())).perform(click());
onView(isRoot()).perform(waitForView(withText(R.string.mark_read_no_media_label), 3000));
onView(withText(R.string.mark_read_no_media_label)).perform(click());

View File

@ -21,6 +21,7 @@ import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
public class CoverLoader {
private int resource = 0;
private String uri;
private String fallbackUri;
private TextView txtvPlaceholder;
@ -37,6 +38,11 @@ public class CoverLoader {
return this;
}
public CoverLoader withResource(int resource) {
this.resource = resource;
return this;
}
public CoverLoader withFallbackUri(String uri) {
fallbackUri = uri;
return this;
@ -66,6 +72,12 @@ public class CoverLoader {
}
public void load() {
if (resource != 0) {
imgvCover.setImageResource(resource);
CoverTarget.setPlaceholderVisibility(txtvPlaceholder, textAndImageCombined);
return;
}
RequestOptions options = new RequestOptions()
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
.fitCenter()
@ -106,15 +118,7 @@ public class CoverLoader {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
TextView txtvPlaceholder = placeholder.get();
if (txtvPlaceholder != null) {
if (textAndImageCombined) {
int bgColor = txtvPlaceholder.getContext().getResources().getColor(R.color.feed_text_bg);
txtvPlaceholder.setBackgroundColor(bgColor);
} else {
txtvPlaceholder.setVisibility(View.INVISIBLE);
}
}
setPlaceholderVisibility(placeholder.get(), textAndImageCombined);
ImageView ivCover = cover.get();
ivCover.setImageDrawable(resource);
}
@ -124,5 +128,16 @@ public class CoverLoader {
ImageView ivCover = cover.get();
ivCover.setImageDrawable(placeholder);
}
static void setPlaceholderVisibility(TextView placeholder, boolean textAndImageCombined) {
if (placeholder != null) {
if (textAndImageCombined) {
int bgColor = placeholder.getContext().getResources().getColor(R.color.feed_text_bg);
placeholder.setBackgroundColor(bgColor);
} else {
placeholder.setVisibility(View.INVISIBLE);
}
}
}
}
}

View File

@ -1,20 +1,21 @@
package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.joanzapata.iconify.Iconify;
@ -23,6 +24,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.AddFeedFragment;
import de.danoeh.antennapod.fragment.DownloadsFragment;
import de.danoeh.antennapod.fragment.EpisodesFragment;
@ -30,6 +32,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.common.ThemeUtils;
import org.apache.commons.lang3.ArrayUtils;
import java.lang.ref.WeakReference;
@ -42,10 +45,9 @@ import java.util.List;
/**
* BaseAdapter for the navigation drawer
*/
public class NavListAdapter extends BaseAdapter
public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final int VIEW_TYPE_COUNT = 3;
public static final int VIEW_TYPE_NAV = 0;
public static final int VIEW_TYPE_SECTION_DIVIDER = 1;
private static final int VIEW_TYPE_SUBSCRIPTION = 2;
@ -56,7 +58,7 @@ public class NavListAdapter extends BaseAdapter
*/
public static final String SUBSCRIPTION_LIST_TAG = "SubscriptionList";
private static List<String> tags;
private static List<String> fragmentTags;
private static String[] titles;
private final ItemAccess itemAccess;
@ -96,7 +98,7 @@ public class NavListAdapter extends BaseAdapter
showSubscriptionList = false;
}
tags = newTags;
fragmentTags = newTags;
notifyDataSetChanged();
}
@ -133,19 +135,18 @@ public class NavListAdapter extends BaseAdapter
default:
return null;
}
TypedArray ta = context.obtainStyledAttributes(new int[] { icon } );
TypedArray ta = context.obtainStyledAttributes(new int[] { icon });
Drawable result = ta.getDrawable(0);
ta.recycle();
return result;
}
public List<String> getTags() {
return Collections.unmodifiableList(tags);
public List<String> getFragmentTags() {
return Collections.unmodifiableList(fragmentTags);
}
@Override
public int getCount() {
public int getItemCount() {
int baseCount = getSubscriptionOffset();
if (showSubscriptionList) {
baseCount += itemAccess.getCount();
@ -154,25 +155,18 @@ public class NavListAdapter extends BaseAdapter
}
@Override
public Object getItem(int position) {
public long getItemId(int position) {
int viewType = getItemViewType(position);
if (viewType == VIEW_TYPE_NAV) {
return getLabel(tags.get(position));
} else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
return "";
if (viewType == VIEW_TYPE_SUBSCRIPTION) {
return itemAccess.getItem(position - getSubscriptionOffset()).id;
} else {
return itemAccess.getItem(position);
return -position - 1; // IDs are >0
}
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
if (0 <= position && position < tags.size()) {
if (0 <= position && position < fragmentTags.size()) {
return VIEW_TYPE_NAV;
} else if (position < getSubscriptionOffset()) {
return VIEW_TYPE_SECTION_DIVIDER;
@ -181,69 +175,67 @@ public class NavListAdapter extends BaseAdapter
}
}
@Override
public int getViewTypeCount() {
return VIEW_TYPE_COUNT;
}
public int getSubscriptionOffset() {
return tags.size() > 0 ? tags.size() + 1 : 0;
return fragmentTags.size() > 0 ? fragmentTags.size() + 1 : 0;
}
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(activity.get());
if (viewType == VIEW_TYPE_NAV) {
return new NavHolder(inflater.inflate(R.layout.nav_listitem, parent, false));
} else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
return new DividerHolder(inflater.inflate(R.layout.nav_section_item, parent, false));
} else {
return new FeedHolder(inflater.inflate(R.layout.nav_listitem, parent, false));
}
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
public void onBindViewHolder(@NonNull Holder holder, int position) {
int viewType = getItemViewType(position);
View v;
if (viewType == VIEW_TYPE_NAV) {
v = getNavView((String) getItem(position), position, convertView, parent);
} else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
v = getSectionDividerView(convertView, parent);
} else {
v = getFeedView(position, convertView, parent);
}
if (v != null && viewType != VIEW_TYPE_SECTION_DIVIDER) {
TypedValue typedValue = new TypedValue();
if (position == itemAccess.getSelectedItemIndex()) {
v.getContext().getTheme().resolveAttribute(R.attr.drawer_activated_color, typedValue, true);
v.setBackgroundResource(typedValue.resourceId);
holder.itemView.setOnCreateContextMenuListener(null);
if (viewType == VIEW_TYPE_NAV) {
bindNavView(getLabel(fragmentTags.get(position)), position, (NavHolder) holder);
} else if (viewType == VIEW_TYPE_SECTION_DIVIDER) {
bindSectionDivider((DividerHolder) holder);
} else {
int itemPos = position - getSubscriptionOffset();
NavDrawerData.DrawerItem item = itemAccess.getItem(itemPos);
bindListItem(item, (FeedHolder) holder);
if (item.type == NavDrawerData.DrawerItem.Type.FEED) {
bindFeedView((NavDrawerData.FeedDrawerItem) item, (FeedHolder) holder);
holder.itemView.setOnCreateContextMenuListener(itemAccess);
} else {
v.getContext().getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true);
v.setBackgroundResource(typedValue.resourceId);
bindFolderView((NavDrawerData.FolderDrawerItem) item, (FeedHolder) holder);
}
}
return v;
if (viewType != VIEW_TYPE_SECTION_DIVIDER) {
TypedValue typedValue = new TypedValue();
activity.get().getTheme().resolveAttribute(itemAccess.isSelected(position)
? R.attr.drawer_activated_color : android.R.attr.windowBackground, typedValue, true);
holder.itemView.setBackgroundResource(typedValue.resourceId);
holder.itemView.setOnClickListener(v -> itemAccess.onItemClick(position));
holder.itemView.setOnLongClickListener(v -> itemAccess.onItemLongClick(position));
}
}
private View getNavView(String title, int position, View convertView, ViewGroup parent) {
private void bindNavView(String title, int position, NavHolder holder) {
Activity context = activity.get();
if(context == null) {
return null;
if (context == null) {
return;
}
NavHolder holder;
if (convertView == null) {
holder = new NavHolder();
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.nav_listitem, parent, false);
holder.image = convertView.findViewById(R.id.imgvCover);
holder.title = convertView.findViewById(R.id.txtvTitle);
holder.count = convertView.findViewById(R.id.txtvCount);
convertView.setTag(holder);
} else {
holder = (NavHolder) convertView.getTag();
}
holder.title.setText(title);
// reset for re-use
holder.count.setVisibility(View.GONE);
holder.count.setOnClickListener(null);
String tag = tags.get(position);
String tag = fragmentTags.get(position);
if (tag.equals(QueueFragment.TAG)) {
int queueSize = itemAccess.getQueueSize();
if (queueSize > 0) {
@ -262,78 +254,64 @@ public class NavListAdapter extends BaseAdapter
holder.count.setText(NumberFormat.getInstance().format(sum));
holder.count.setVisibility(View.VISIBLE);
}
} else if(tag.equals(DownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) {
} else if (tag.equals(DownloadsFragment.TAG) && UserPreferences.isEnableAutodownload()) {
int epCacheSize = UserPreferences.getEpisodeCacheSize();
// don't count episodes that can be reclaimed
int spaceUsed = itemAccess.getNumberOfDownloadedItems() -
itemAccess.getReclaimableItems();
int spaceUsed = itemAccess.getNumberOfDownloadedItems()
- itemAccess.getReclaimableItems();
if (epCacheSize > 0 && spaceUsed >= epCacheSize) {
holder.count.setText("{md-disc-full 150%}");
Iconify.addIcons(holder.count);
holder.count.setVisibility(View.VISIBLE);
holder.count.setOnClickListener(v ->
new AlertDialog.Builder(context)
new AlertDialog.Builder(context)
.setTitle(R.string.episode_cache_full_title)
.setMessage(R.string.episode_cache_full_message)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {})
.setPositiveButton(android.R.string.ok, (dialog, which) -> { })
.show()
);
}
}
holder.image.setImageDrawable(getDrawable(tags.get(position)));
return convertView;
holder.image.setImageDrawable(getDrawable(fragmentTags.get(position)));
}
private View getSectionDividerView(View convertView, ViewGroup parent) {
private void bindSectionDivider(DividerHolder holder) {
Activity context = activity.get();
if(context == null) {
return null;
if (context == null) {
return;
}
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.nav_section_item, parent, false);
TextView feedsFilteredMsg = convertView.findViewById(R.id.nav_feeds_filtered_message);
if (UserPreferences.getSubscriptionsFilter().isEnabled() && showSubscriptionList) {
convertView.setEnabled(true);
feedsFilteredMsg.setText("{md-info-outline} " + context.getString(R.string.subscriptions_are_filtered));
Iconify.addIcons(feedsFilteredMsg);
feedsFilteredMsg.setVisibility(View.VISIBLE);
holder.itemView.setEnabled(true);
holder.feedsFilteredMsg.setText("{md-info-outline} "
+ context.getString(R.string.subscriptions_are_filtered));
Iconify.addIcons(holder.feedsFilteredMsg);
holder.feedsFilteredMsg.setVisibility(View.VISIBLE);
} else {
convertView.setEnabled(false);
feedsFilteredMsg.setVisibility(View.GONE);
holder.itemView.setEnabled(false);
holder.feedsFilteredMsg.setVisibility(View.GONE);
}
return convertView;
}
private View getFeedView(int position, View convertView, ViewGroup parent) {
Activity context = activity.get();
if(context == null) {
return null;
}
int feedPos = position - getSubscriptionOffset();
Feed feed = itemAccess.getItem(feedPos);
FeedHolder holder;
if (convertView == null) {
holder = new FeedHolder();
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.nav_listitem, parent, false);
holder.image = convertView.findViewById(R.id.imgvCover);
holder.title = convertView.findViewById(R.id.txtvTitle);
holder.failure = convertView.findViewById(R.id.itxtvFailure);
holder.count = convertView.findViewById(R.id.txtvCount);
convertView.setTag(holder);
private void bindListItem(NavDrawerData.DrawerItem item, FeedHolder holder) {
if (item.getCounter() > 0) {
holder.count.setVisibility(View.VISIBLE);
holder.count.setText(NumberFormat.getInstance().format(item.getCounter()));
} else {
holder = (FeedHolder) convertView.getTag();
holder.count.setVisibility(View.GONE);
}
holder.title.setText(item.getTitle());
int padding = (int) (activity.get().getResources().getDimension(R.dimen.thumbnail_length_navlist) / 2);
holder.itemView.setPadding(item.getLayer() * padding, 0, 0, 0);
}
private void bindFeedView(NavDrawerData.FeedDrawerItem drawerItem, FeedHolder holder) {
Feed feed = drawerItem.feed;
Activity context = activity.get();
if (context == null) {
return;
}
Glide.with(context)
@ -346,9 +324,7 @@ public class NavListAdapter extends BaseAdapter
.dontAnimate())
.into(holder.image);
holder.title.setText(feed.getTitle());
if(feed.hasLastUpdateFailed()) {
if (feed.hasLastUpdateFailed()) {
RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) holder.title.getLayoutParams();
p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure);
holder.failure.setVisibility(View.VISIBLE);
@ -357,39 +333,87 @@ public class NavListAdapter extends BaseAdapter
p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount);
holder.failure.setVisibility(View.GONE);
}
int counter = itemAccess.getFeedCounter(feed.getId());
if(counter > 0) {
holder.count.setVisibility(View.VISIBLE);
holder.count.setText(NumberFormat.getInstance().format(counter));
} else {
}
private void bindFolderView(NavDrawerData.FolderDrawerItem folder, FeedHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
}
if (folder.isOpen) {
holder.count.setVisibility(View.GONE);
}
return convertView;
Glide.with(context).clear(holder.image);
holder.image.setImageResource(ThemeUtils.getDrawableFromAttr(context, R.attr.ic_folder));
holder.failure.setVisibility(View.GONE);
}
static class NavHolder {
ImageView image;
TextView title;
TextView count;
static class Holder extends RecyclerView.ViewHolder {
public Holder(@NonNull View itemView) {
super(itemView);
}
}
static class FeedHolder {
ImageView image;
TextView title;
IconTextView failure;
TextView count;
static class DividerHolder extends Holder {
final TextView feedsFilteredMsg;
public DividerHolder(@NonNull View itemView) {
super(itemView);
feedsFilteredMsg = itemView.findViewById(R.id.nav_feeds_filtered_message);
}
}
public interface ItemAccess {
static class NavHolder extends Holder {
final ImageView image;
final TextView title;
final TextView count;
public NavHolder(@NonNull View itemView) {
super(itemView);
image = itemView.findViewById(R.id.imgvCover);
title = itemView.findViewById(R.id.txtvTitle);
count = itemView.findViewById(R.id.txtvCount);
}
}
static class FeedHolder extends Holder {
final ImageView image;
final TextView title;
final IconTextView failure;
final TextView count;
public FeedHolder(@NonNull View itemView) {
super(itemView);
image = itemView.findViewById(R.id.imgvCover);
title = itemView.findViewById(R.id.txtvTitle);
failure = itemView.findViewById(R.id.itxtvFailure);
count = itemView.findViewById(R.id.txtvCount);
}
}
public interface ItemAccess extends View.OnCreateContextMenuListener {
int getCount();
Feed getItem(int position);
int getSelectedItemIndex();
NavDrawerData.DrawerItem getItem(int position);
boolean isSelected(int position);
int getQueueSize();
int getNumberOfNewItems();
int getNumberOfDownloadedItems();
int getReclaimableItems();
int getFeedCounter(long feedId);
int getFeedCounterSum();
void onItemClick(int position);
boolean onItemLongClick(int position);
@Override
void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo);
}
}

View File

@ -21,17 +21,16 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import jp.shts.android.library.TriangleLabelView;
/**
* Adapter for subscriptions
*/
public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnItemClickListener {
/** placeholder object that indicates item should be added */
public static final Object ADD_ITEM_OBJ = new Object();
/** the position in the view that holds the add item; 0 is the first, -1 is the last position */
private static final String TAG = "SubscriptionsAdapter";
@ -60,7 +59,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
@Override
public long getItemId(int position) {
return itemAccess.getItem(position).getId();
return ((NavDrawerData.DrawerItem) getItem(position)).id;
}
@Override
@ -83,11 +82,13 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
holder = (Holder) convertView.getTag();
}
final Feed feed = (Feed) getItem(position);
if (feed == null) return null;
final NavDrawerData.DrawerItem drawerItem = (NavDrawerData.DrawerItem) getItem(position);
if (drawerItem == null) {
return null;
}
holder.feedTitle.setText(feed.getTitle());
holder.imageView.setContentDescription(feed.getTitle());
holder.feedTitle.setText(drawerItem.getTitle());
holder.imageView.setContentDescription(drawerItem.getTitle());
holder.feedTitle.setVisibility(View.VISIBLE);
// Fix TriangleLabelView corner for RTL
@ -96,30 +97,46 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
holder.count.setCorner(TriangleLabelView.Corner.TOP_LEFT);
}
int count = itemAccess.getFeedCounter(feed.getId());
if(count > 0) {
holder.count.setPrimaryText(
NumberFormat.getInstance().format(itemAccess.getFeedCounter(feed.getId())));
if (drawerItem.getCounter() > 0) {
holder.count.setPrimaryText(NumberFormat.getInstance().format(drawerItem.getCounter()));
holder.count.setVisibility(View.VISIBLE);
} else {
holder.count.setVisibility(View.GONE);
}
boolean textAndImageCombined = feed.isLocalFeed()
&& LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl());
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(holder.feedTitle, textAndImageCombined)
.withCoverView(holder.imageView)
.load();
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
boolean textAndImageCombined = feed.isLocalFeed()
&& LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl());
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(holder.feedTitle, textAndImageCombined)
.withCoverView(holder.imageView)
.load();
} else {
new CoverLoader(mainActivityRef.get())
.withResource(ThemeUtils.getDrawableFromAttr(mainActivityRef.get(), R.attr.ic_folder))
.withPlaceholderView(holder.feedTitle, true)
.withCoverView(holder.imageView)
.load();
}
return convertView;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Fragment fragment = FeedItemlistFragment.newInstance(getItemId(position));
mainActivityRef.get().loadChildFragment(fragment);
final NavDrawerData.DrawerItem drawerItem = (NavDrawerData.DrawerItem) getItem(position);
if (drawerItem == null) {
return;
}
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
Fragment fragment = FeedItemlistFragment.newInstance(feed.getId());
mainActivityRef.get().loadChildFragment(fragment);
} else if (drawerItem.type == NavDrawerData.DrawerItem.Type.FOLDER) {
Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle());
mainActivityRef.get().loadChildFragment(fragment);
}
}
static class Holder {
@ -130,7 +147,7 @@ public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnI
public interface ItemAccess {
int getCount();
Feed getItem(int position);
int getFeedCounter(long feedId);
NavDrawerData.DrawerItem getItem(int position);
}
}

View File

@ -0,0 +1,116 @@
package de.danoeh.antennapod.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.chip.Chip;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.feed.FeedPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.databinding.EditTagsDialogBinding;
import de.danoeh.antennapod.view.ItemOffsetDecoration;
import java.util.ArrayList;
import java.util.List;
public class TagSettingsDialog extends DialogFragment {
public static final String TAG = "TagSettingsDialog";
private static final String ARG_FEED_PREFERENCES = "feed_preferences";
private List<String> displayedTags;
public static TagSettingsDialog newInstance(FeedPreferences preferences) {
TagSettingsDialog fragment = new TagSettingsDialog();
Bundle args = new Bundle();
args.putSerializable(ARG_FEED_PREFERENCES, preferences);
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
FeedPreferences preferences = (FeedPreferences) getArguments().getSerializable(ARG_FEED_PREFERENCES);
displayedTags = new ArrayList<>(preferences.getTags());
displayedTags.remove(FeedPreferences.TAG_ROOT);
EditTagsDialogBinding viewBinding = EditTagsDialogBinding.inflate(getLayoutInflater());
viewBinding.tagsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2));
viewBinding.tagsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4));
TagSelectionAdapter adapter = new TagSelectionAdapter();
adapter.setHasStableIds(true);
viewBinding.tagsRecycler.setAdapter(adapter);
viewBinding.rootFolderCheckbox.setChecked(preferences.getTags().contains(FeedPreferences.TAG_ROOT));
viewBinding.newTagButton.setOnClickListener(v -> {
String name = viewBinding.newTagEditText.getText().toString().trim();
if (TextUtils.isEmpty(name) || displayedTags.contains(name)) {
return;
}
displayedTags.add(name);
viewBinding.newTagEditText.setText("");
adapter.notifyDataSetChanged();
});
AlertDialog.Builder dialog = new AlertDialog.Builder(getContext());
dialog.setView(viewBinding.getRoot());
dialog.setTitle(R.string.feed_folders_label);
dialog.setPositiveButton(android.R.string.ok, (d, input) -> {
preferences.getTags().clear();
preferences.getTags().addAll(displayedTags);
if (viewBinding.rootFolderCheckbox.isChecked()) {
preferences.getTags().add(FeedPreferences.TAG_ROOT);
}
DBWriter.setFeedPreferences(preferences);
});
dialog.setNegativeButton(R.string.cancel_label, null);
return dialog.create();
}
public class TagSelectionAdapter extends RecyclerView.Adapter<TagSelectionAdapter.ViewHolder> {
@Override
@NonNull
public TagSelectionAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Chip chip = new Chip(getContext());
chip.setCloseIconVisible(true);
chip.setCloseIconResource(R.drawable.ic_delete_black);
return new TagSelectionAdapter.ViewHolder(chip);
}
@Override
public void onBindViewHolder(@NonNull TagSelectionAdapter.ViewHolder holder, int position) {
holder.chip.setText(displayedTags.get(position));
holder.chip.setOnCloseIconClickListener(v -> {
displayedTags.remove(position);
notifyDataSetChanged();
});
}
@Override
public int getItemCount() {
return displayedTags.size();
}
@Override
public long getItemId(int position) {
return displayedTags.get(position).hashCode();
}
public class ViewHolder extends RecyclerView.ViewHolder {
Chip chip;
ViewHolder(Chip itemView) {
super(itemView);
chip = itemView;
}
}
}
}

View File

@ -30,6 +30,7 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.dialog.AuthenticationDialog;
import de.danoeh.antennapod.dialog.EpisodeFilterDialog;
import de.danoeh.antennapod.dialog.FeedPreferenceSkipDialog;
import de.danoeh.antennapod.dialog.TagSettingsDialog;
import io.reactivex.Maybe;
import io.reactivex.MaybeOnSubscribe;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -105,6 +106,7 @@ public class FeedSettingsFragment extends Fragment {
private static final CharSequence PREF_CATEGORY_AUTO_DOWNLOAD = "autoDownloadCategory";
private static final String PREF_FEED_PLAYBACK_SPEED = "feedPlaybackSpeed";
private static final String PREF_AUTO_SKIP = "feedAutoSkip";
private static final String PREF_TAGS = "tags";
private static final DecimalFormat SPEED_FORMAT =
new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.US));
@ -160,6 +162,7 @@ public class FeedSettingsFragment extends Fragment {
setupPlaybackSpeedPreference();
setupFeedAutoSkipPreference();
setupEpisodeNotificationPreference();
setupTags();
updateAutoDeleteSummary();
updateVolumeReductionValue();
@ -395,6 +398,13 @@ public class FeedSettingsFragment extends Fragment {
}
}
private void setupTags() {
findPreference(PREF_TAGS).setOnPreferenceClickListener(preference -> {
TagSettingsDialog.newInstance(feedPreferences).show(getChildFragmentManager(), TagSettingsDialog.TAG);
return true;
});
}
private void setupEpisodeNotificationPreference() {
SwitchPreferenceCompat pref = findPreference("episodeNotification");

View File

@ -12,15 +12,16 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Pair;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
@ -34,6 +35,7 @@ import de.danoeh.antennapod.core.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.RenameFeedDialog;
@ -46,12 +48,15 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class NavDrawerFragment extends Fragment implements AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener, SharedPreferences.OnSharedPreferenceChangeListener {
public class NavDrawerFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener {
@VisibleForTesting
public static final String PREF_LAST_FRAGMENT_TAG = "prefLastFragmentTag";
private static final String PREF_OPEN_FOLDERS = "prefOpenFolders";
@VisibleForTesting
public static final String PREF_NAME = "NavDrawerPrefs";
public static final String TAG = "NavDrawerFragment";
@ -66,12 +71,13 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
NavListAdapter.SUBSCRIPTION_LIST_TAG
};
private DBReader.NavDrawerData navDrawerData;
private int selectedNavListIndex = -1;
private int position = -1;
private NavDrawerData navDrawerData;
private List<NavDrawerData.DrawerItem> flatItemList;
private NavDrawerData.DrawerItem contextPressedItem = null;
private NavListAdapter navAdapter;
private Disposable disposable;
private ProgressBar progressBar;
private Set<String> openFolders = new HashSet<>();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@ -79,40 +85,21 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
super.onCreateView(inflater, container, savedInstanceState);
View root = inflater.inflate(R.layout.nav_list, container, false);
SharedPreferences preferences = getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
openFolders = new HashSet<>(preferences.getStringSet(PREF_OPEN_FOLDERS, new HashSet<>())); // Must not modify
progressBar = root.findViewById(R.id.progressBar);
ListView navList = root.findViewById(R.id.nav_list);
RecyclerView navList = root.findViewById(R.id.nav_list);
navAdapter = new NavListAdapter(itemAccess, getActivity());
navAdapter.setHasStableIds(true);
navList.setAdapter(navAdapter);
navList.setOnItemClickListener(this);
navList.setOnItemLongClickListener(this);
registerForContextMenu(navList);
updateSelection();
navList.setLayoutManager(new LinearLayoutManager(getContext()));
root.findViewById(R.id.nav_settings).setOnClickListener(v ->
startActivity(new Intent(getActivity(), PreferenceActivity.class)));
getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(this);
return root;
}
private void updateSelection() {
String lastNavFragment = getLastNavFragment(getContext());
int tagIndex = navAdapter.getTags().indexOf(lastNavFragment);
if (tagIndex >= 0) {
selectedNavListIndex = tagIndex;
} else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed
long feedId = Long.parseLong(lastNavFragment);
if (navDrawerData != null) {
List<Feed> feeds = navDrawerData.feeds;
for (int i = 0; i < feeds.size(); i++) {
if (feeds.get(i).getId() == feedId) {
selectedNavListIndex = navAdapter.getSubscriptionOffset() + i;
break;
}
}
}
}
navAdapter.notifyDataSetChanged();
preferences.registerOnSharedPreferenceChangeListener(this);
return root;
}
@Override
@ -135,29 +122,25 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
@Override
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
if (v.getId() != R.id.nav_list) {
return;
}
AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
int position = adapterInfo.position;
if (position < navAdapter.getSubscriptionOffset()) {
return;
if (contextPressedItem.type != NavDrawerData.DrawerItem.Type.FEED) {
return; // Should actually never happen because the context menu is not set up for other items
}
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.nav_feed_context, menu);
Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
menu.setHeaderTitle(feed.getTitle());
menu.setHeaderTitle(((NavDrawerData.FeedDrawerItem) contextPressedItem).feed.getTitle());
// episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
final int position = this.position;
this.position = -1; // reset
if (position < 0) {
return false;
if (contextPressedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
return onFeedContextMenuClicked(((NavDrawerData.FeedDrawerItem) contextPressedItem).feed, item);
}
Feed feed = navDrawerData.feeds.get(position - navAdapter.getSubscriptionOffset());
return false;
}
private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) {
switch (item.getItemId()) {
case R.id.remove_all_new_flags_item:
ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getContext(),
@ -189,13 +172,7 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
return true;
case R.id.remove_item:
RemoveFeedDialog.show(getContext(), feed, () -> {
if (selectedNavListIndex == position) {
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null);
} else {
showMainActivity(EpisodesFragment.TAG);
}
}
((MainActivity) getActivity()).loadFragment(EpisodesFragment.TAG, null);
});
return true;
default:
@ -203,12 +180,6 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
}
}
private void showMainActivity(String tag) {
Intent intent = new Intent(getActivity(), MainActivity.class);
intent.putExtra(MainActivity.EXTRA_FRAGMENT_TAG, tag);
startActivity(intent);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) {
loadData();
@ -261,7 +232,7 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
});
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
UserPreferences.setHiddenDrawerItems(hiddenDrawerItems);
updateSelection();
navAdapter.notifyDataSetChanged(); // Update selection
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.create().show();
@ -270,25 +241,39 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
private final NavListAdapter.ItemAccess itemAccess = new NavListAdapter.ItemAccess() {
@Override
public int getCount() {
if (navDrawerData != null) {
return navDrawerData.feeds.size();
if (flatItemList != null) {
return flatItemList.size();
} else {
return 0;
}
}
@Override
public Feed getItem(int position) {
if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) {
return navDrawerData.feeds.get(position);
public NavDrawerData.DrawerItem getItem(int position) {
if (flatItemList != null && 0 <= position && position < flatItemList.size()) {
return flatItemList.get(position);
} else {
return null;
}
}
@Override
public int getSelectedItemIndex() {
return selectedNavListIndex;
public boolean isSelected(int position) {
String lastNavFragment = getLastNavFragment(getContext());
if (position < navAdapter.getSubscriptionOffset()) {
return navAdapter.getFragmentTags().get(position).equals(lastNavFragment);
} else if (StringUtils.isNumeric(lastNavFragment)) { // last fragment was not a list, but a feed
long feedId = Long.parseLong(lastNavFragment);
if (navDrawerData != null) {
NavDrawerData.DrawerItem itemToCheck = flatItemList.get(
position - navAdapter.getSubscriptionOffset());
if (itemToCheck.type == NavDrawerData.DrawerItem.Type.FEED) {
// When the same feed is displayed multiple times, it should be highlighted multiple times.
return ((NavDrawerData.FeedDrawerItem) itemToCheck).feed.getId() == feedId;
}
}
}
return false;
}
@Override
@ -311,11 +296,6 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
return (navDrawerData != null) ? navDrawerData.reclaimableSpace : 0;
}
@Override
public int getFeedCounter(long feedId) {
return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0;
}
@Override
public int getFeedCounterSum() {
if (navDrawerData == null) {
@ -328,16 +308,82 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
return sum;
}
@Override
public void onItemClick(int position) {
int viewType = navAdapter.getItemViewType(position);
if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) {
if (position < navAdapter.getSubscriptionOffset()) {
String tag = navAdapter.getFragmentTags().get(position);
((MainActivity) getActivity()).loadFragment(tag, null);
((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
int pos = position - navAdapter.getSubscriptionOffset();
NavDrawerData.DrawerItem clickedItem = flatItemList.get(pos);
if (clickedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
long feedId = ((NavDrawerData.FeedDrawerItem) clickedItem).feed.getId();
((MainActivity) getActivity()).loadFeedFragmentById(feedId, null);
((MainActivity) getActivity()).getBottomSheet()
.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
NavDrawerData.FolderDrawerItem folder = ((NavDrawerData.FolderDrawerItem) clickedItem);
if (openFolders.contains(folder.name)) {
openFolders.remove(folder.name);
} else {
openFolders.add(folder.name);
}
getContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putStringSet(PREF_OPEN_FOLDERS, openFolders)
.apply();
disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.items, 0))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> {
flatItemList = result;
navAdapter.notifyDataSetChanged();
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
}
} else if (UserPreferences.getSubscriptionsFilter().isEnabled()
&& navAdapter.showSubscriptionList) {
SubscriptionsFilterDialog.showDialog(requireContext());
}
}
@Override
public boolean onItemLongClick(int position) {
if (position < navAdapter.getFragmentTags().size()) {
showDrawerPreferencesDialog();
return true;
} else {
contextPressedItem = flatItemList.get(position - navAdapter.getSubscriptionOffset());
return false;
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
NavDrawerFragment.this.onCreateContextMenu(menu, v, menuInfo);
}
};
private void loadData() {
disposable = Observable.fromCallable(DBReader::getNavDrawerData)
progressBar.setVisibility(View.VISIBLE);
disposable = Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData();
return new Pair<>(data, makeFlatDrawerData(data.items, 0));
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> {
navDrawerData = result;
updateSelection(); // Selected item might be a feed
navDrawerData = result.first;
flatItemList = result.second;
navAdapter.notifyDataSetChanged();
progressBar.setVisibility(View.GONE); // Stays hidden once there is something in the list
}, error -> {
@ -346,45 +392,20 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
});
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int viewType = parent.getAdapter().getItemViewType(position);
if (viewType != NavListAdapter.VIEW_TYPE_SECTION_DIVIDER) {
if (position < navAdapter.getSubscriptionOffset()) {
String tag = navAdapter.getTags().get(position);
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).loadFragment(tag, null);
((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
showMainActivity(tag);
}
} else {
int pos = position - navAdapter.getSubscriptionOffset();
long feedId = navDrawerData.feeds.get(pos).getId();
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).loadFeedFragmentById(feedId, null);
((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
Intent intent = new Intent(getActivity(), MainActivity.class);
intent.putExtra(MainActivity.EXTRA_FEED_ID, feedId);
startActivity(intent);
private List<NavDrawerData.DrawerItem> makeFlatDrawerData(List<NavDrawerData.DrawerItem> items, int layer) {
List<NavDrawerData.DrawerItem> flatItems = new ArrayList<>();
for (NavDrawerData.DrawerItem item : items) {
item.setLayer(layer);
flatItems.add(item);
if (item.type == NavDrawerData.DrawerItem.Type.FOLDER) {
NavDrawerData.FolderDrawerItem folder = ((NavDrawerData.FolderDrawerItem) item);
folder.isOpen = openFolders.contains(folder.name);
if (folder.isOpen) {
flatItems.addAll(makeFlatDrawerData(((NavDrawerData.FolderDrawerItem) item).children, layer + 1));
}
}
} else if (UserPreferences.getSubscriptionsFilter().isEnabled()
&& navAdapter.showSubscriptionList) {
SubscriptionsFilterDialog.showDialog(requireContext());
}
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
if (position < navAdapter.getTags().size()) {
showDrawerPreferencesDialog();
return true;
} else {
this.position = position;
return false;
}
return flatItems;
}
public static void saveLastNavFragment(Context context, String tag) {
@ -409,8 +430,7 @@ public class NavDrawerFragment extends Fragment implements AdapterView.OnItemCli
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (PREF_LAST_FRAGMENT_TAG.equals(key)) {
updateSelection();
navAdapter.notifyDataSetChanged();
navAdapter.notifyDataSetChanged(); // Update selection
}
}
}

View File

@ -28,6 +28,7 @@ import android.widget.TextView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.joanzapata.iconify.Iconify;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
@ -44,6 +45,7 @@ import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
@ -68,6 +70,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
private static final String PREFS = "SubscriptionFragment";
private static final String PREF_NUM_COLUMNS = "columns";
private static final String KEY_UP_ARROW = "up_arrow";
private static final String ARGUMENT_FOLDER = "folder";
private static final int MIN_NUM_COLUMNS = 2;
private static final int[] COLUMN_CHECKBOX_IDS = {
@ -77,21 +80,30 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
R.id.subscription_num_columns_5};
private GridView subscriptionGridLayout;
private DBReader.NavDrawerData navDrawerData;
private List<NavDrawerData.DrawerItem> listItems;
private SubscriptionsAdapter subscriptionAdapter;
private FloatingActionButton subscriptionAddButton;
private ProgressBar progressBar;
private EmptyViewHandler emptyView;
private TextView feedsFilteredMsg;
private Toolbar toolbar;
private String displayedFolder = null;
private int mPosition = -1;
private Feed selectedFeed = null;
private boolean isUpdatingFeeds = false;
private boolean displayUpArrow;
private Disposable disposable;
private SharedPreferences prefs;
public static SubscriptionFragment newInstance(String folderTitle) {
SubscriptionFragment fragment = new SubscriptionFragment();
Bundle args = new Bundle();
args.putString(ARGUMENT_FOLDER, folderTitle);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -119,6 +131,13 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
}
refreshToolbarState();
if (getArguments() != null) {
displayedFolder = getArguments().getString(ARGUMENT_FOLDER, null);
if (displayedFolder != null) {
toolbar.setTitle(displayedFolder);
}
}
subscriptionGridLayout = root.findViewById(R.id.subscriptions_grid);
subscriptionGridLayout.setNumColumns(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()));
registerForContextMenu(subscriptionGridLayout);
@ -231,12 +250,23 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
disposable.dispose();
}
emptyView.hide();
disposable = Observable.fromCallable(DBReader::getNavDrawerData)
disposable = Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData();
List<NavDrawerData.DrawerItem> items = data.items;
for (NavDrawerData.DrawerItem item : items) {
if (item.type == NavDrawerData.DrawerItem.Type.FOLDER
&& item.getTitle().equals(displayedFolder)) {
return ((NavDrawerData.FolderDrawerItem) item).children;
}
}
return items;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> {
navDrawerData = result;
listItems = result;
subscriptionAdapter.notifyDataSetChanged();
emptyView.updateVisibility();
progressBar.setVisibility(View.GONE); // Keep hidden to avoid flickering while refreshing
@ -264,37 +294,24 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
int position = adapterInfo.position;
Object selectedObject = subscriptionAdapter.getItem(position);
if (selectedObject.equals(SubscriptionsAdapter.ADD_ITEM_OBJ)) {
mPosition = position;
return;
NavDrawerData.DrawerItem selectedObject = (NavDrawerData.DrawerItem) subscriptionAdapter.getItem(position);
if (selectedObject.type == NavDrawerData.DrawerItem.Type.FEED) {
MenuInflater inflater = requireActivity().getMenuInflater();
inflater.inflate(R.menu.nav_feed_context, menu);
selectedFeed = ((NavDrawerData.FeedDrawerItem) selectedObject).feed;
}
Feed feed = (Feed) selectedObject;
MenuInflater inflater = requireActivity().getMenuInflater();
inflater.inflate(R.menu.nav_feed_context, menu);
menu.setHeaderTitle(feed.getTitle());
mPosition = position;
menu.setHeaderTitle(selectedObject.getTitle());
}
@Override
public boolean onContextItemSelected(MenuItem item) {
final int position = mPosition;
mPosition = -1; // reset
if (position < 0) {
if (selectedFeed == null) {
return false;
}
Object selectedObject = subscriptionAdapter.getItem(position);
if (selectedObject.equals(SubscriptionsAdapter.ADD_ITEM_OBJ)) {
// this is the add object, do nothing
return false;
}
Feed feed = (Feed) selectedObject;
Feed feed = selectedFeed;
selectedFeed = null;
switch (item.getItemId()) {
case R.id.remove_all_new_flags_item:
displayConfirmationDialog(
@ -359,25 +376,20 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
private final SubscriptionsAdapter.ItemAccess itemAccess = new SubscriptionsAdapter.ItemAccess() {
@Override
public int getCount() {
if (navDrawerData != null) {
return navDrawerData.feeds.size();
if (listItems != null) {
return listItems.size();
} else {
return 0;
}
}
@Override
public Feed getItem(int position) {
if (navDrawerData != null && 0 <= position && position < navDrawerData.feeds.size()) {
return navDrawerData.feeds.get(position);
public NavDrawerData.DrawerItem getItem(int position) {
if (listItems != null && 0 <= position && position < listItems.size()) {
return listItems.get(position);
} else {
return null;
}
}
@Override
public int getFeedCounter(long feedId) {
return navDrawerData != null ? navDrawerData.feedCounters.get(feedId) : 0;
}
};
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tagsRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<CheckBox
android:id="@+id/rootFolderCheckbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/feed_folders_include_root" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="text"
android:ems="10"
android:id="@+id/newTagEditText"/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="?attr/content_new"
android:contentDescription="@string/new_label"
android:id="@+id/newTagButton"/>
</LinearLayout>
</LinearLayout>

View File

@ -12,6 +12,7 @@
android:theme="?attr/actionBarTheme"
android:layout_alignParentTop="true"
app:title="@string/subscriptions_label"
app:navigationIcon="?homeAsUpIndicator"
android:id="@+id/toolbar"/>
<TextView

View File

@ -57,7 +57,7 @@
android:background="?android:attr/listDivider"
tools:background="@android:color/holo_red_dark" />
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nav_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -23,6 +23,12 @@
android:title="@string/authentication_label"
android:summary="@string/authentication_descr"/>
<Preference
android:key="tags"
android:icon="?attr/ic_folder"
android:title="@string/feed_folders_label"
android:summary="@string/feed_folders_summary"/>
<ListPreference
android:key="feedPlaybackSpeed"
android:icon="?attr/ic_settings_speed"

View File

@ -1,11 +1,12 @@
package de.danoeh.antennapod.core.feed;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FeedFilter {
public class FeedFilter implements Serializable {
private static final String TAG = "FeedFilter";

View File

@ -7,12 +7,19 @@ import android.text.TextUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Contains preferences for a single feed.
*/
public class FeedPreferences {
public class FeedPreferences implements Serializable {
public static final float SPEED_USE_GLOBAL = -1;
public static final String TAG_ROOT = "#root";
public static final String TAG_SEPARATOR = "\u001e";
public enum AutoDeleteAction {
GLOBAL,
@ -33,17 +40,19 @@ public class FeedPreferences {
private int feedSkipIntro;
private int feedSkipEnding;
private boolean showEpisodeNotification;
private final Set<String> tags = new HashSet<>();
public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction autoDeleteAction,
VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) {
this(feedID, autoDownload, true, autoDeleteAction, volumeAdaptionSetting,
username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false);
username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0, false, new HashSet<>());
}
private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated,
AutoDeleteAction autoDeleteAction, VolumeAdaptionSetting volumeAdaptionSetting,
String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed,
int feedSkipIntro, int feedSkipEnding, boolean showEpisodeNotification) {
int feedSkipIntro, int feedSkipEnding, boolean showEpisodeNotification,
Set<String> tags) {
this.feedID = feedID;
this.autoDownload = autoDownload;
this.keepUpdated = keepUpdated;
@ -56,6 +65,7 @@ public class FeedPreferences {
this.feedSkipIntro = feedSkipIntro;
this.feedSkipEnding = feedSkipEnding;
this.showEpisodeNotification = showEpisodeNotification;
this.tags.addAll(tags);
}
public static FeedPreferences fromCursor(Cursor cursor) {
@ -72,6 +82,7 @@ public class FeedPreferences {
int indexAutoSkipIntro = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_INTRO);
int indexAutoSkipEnding = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_SKIP_ENDING);
int indexEpisodeNotification = cursor.getColumnIndex(PodDBAdapter.KEY_EPISODE_NOTIFICATION);
int indexTags = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_TAGS);
long feedId = cursor.getLong(indexId);
boolean autoDownload = cursor.getInt(indexAutoDownload) > 0;
@ -88,6 +99,10 @@ public class FeedPreferences {
int feedAutoSkipIntro = cursor.getInt(indexAutoSkipIntro);
int feedAutoSkipEnding = cursor.getInt(indexAutoSkipEnding);
boolean showNotification = cursor.getInt(indexEpisodeNotification) > 0;
String tagsString = cursor.getString(indexTags);
if (TextUtils.isEmpty(tagsString)) {
tagsString = TAG_ROOT;
}
return new FeedPreferences(feedId,
autoDownload,
autoRefresh,
@ -99,8 +114,8 @@ public class FeedPreferences {
feedPlaybackSpeed,
feedAutoSkipIntro,
feedAutoSkipEnding,
showNotification
);
showNotification,
new HashSet<>(Arrays.asList(tagsString.split(TAG_SEPARATOR))));
}
/**
@ -240,6 +255,14 @@ public class FeedPreferences {
return feedSkipEnding;
}
public Set<String> getTags() {
return tags;
}
public String getTagsAsString() {
return TextUtils.join(TAG_SEPARATOR, tags);
}
/**
* getter for preference if notifications should be display for new episodes.
* @return true for displaying notifications

View File

@ -11,6 +11,7 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -874,32 +875,34 @@ public final class DBReader {
int numNewItems = adapter.getNumberOfNewItems();
int numDownloadedItems = adapter.getNumberOfDownloadedEpisodes();
NavDrawerData result = new NavDrawerData(feeds, queueSize, numNewItems, numDownloadedItems,
List<NavDrawerData.DrawerItem> items = new ArrayList<>();
Map<String, NavDrawerData.FolderDrawerItem> folders = new HashMap<>();
for (Feed feed : feeds) {
for (String tag : feed.getPreferences().getTags()) {
NavDrawerData.FeedDrawerItem drawerItem = new NavDrawerData.FeedDrawerItem(feed, feed.getId(),
feedCounters.get(feed.getId()));
if (FeedPreferences.TAG_ROOT.equals(tag)) {
items.add(drawerItem);
continue;
}
NavDrawerData.FolderDrawerItem folder;
if (folders.containsKey(tag)) {
folder = folders.get(tag);
} else {
folder = new NavDrawerData.FolderDrawerItem(tag);
folders.put(tag, folder);
}
drawerItem.id |= folder.id;
folder.children.add(drawerItem);
}
}
List<NavDrawerData.FolderDrawerItem> foldersSorted = new ArrayList<>(folders.values());
Collections.sort(foldersSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
items.addAll(foldersSorted);
NavDrawerData result = new NavDrawerData(items, queueSize, numNewItems, numDownloadedItems,
feedCounters, UserPreferences.getEpisodeCleanupAlgorithm().getReclaimableItems());
adapter.close();
return result;
}
public static class NavDrawerData {
public final List<Feed> feeds;
public final int queueSize;
public final int numNewItems;
public final int numDownloadedItems;
public final LongIntMap feedCounters;
public final int reclaimableSpace;
public NavDrawerData(List<Feed> feeds,
int queueSize,
int numNewItems,
int numDownloadedItems,
LongIntMap feedIndicatorValues,
int reclaimableSpace) {
this.feeds = feeds;
this.queueSize = queueSize;
this.numNewItems = numNewItems;
this.numDownloadedItems = numDownloadedItems;
this.feedCounters = feedIndicatorValues;
this.reclaimableSpace = reclaimableSpace;
}
}
}

View File

@ -319,6 +319,8 @@ class DBUpgrader {
+ " SET " + PodDBAdapter.KEY_DESCRIPTION + " = content_encoded, content_encoded = NULL "
+ "WHERE length(" + PodDBAdapter.KEY_DESCRIPTION + ") < length(content_encoded)");
db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + " SET content_encoded = NULL");
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
+ " ADD COLUMN " + PodDBAdapter.KEY_FEED_TAGS + " TEXT;");
}
}

View File

@ -0,0 +1,99 @@
package de.danoeh.antennapod.core.storage;
import de.danoeh.antennapod.core.feed.Feed;
import de.danoeh.antennapod.core.util.LongIntMap;
import java.util.ArrayList;
import java.util.List;
public class NavDrawerData {
public final List<DrawerItem> items;
public final int queueSize;
public final int numNewItems;
public final int numDownloadedItems;
public final LongIntMap feedCounters;
public final int reclaimableSpace;
public NavDrawerData(List<DrawerItem> feeds,
int queueSize,
int numNewItems,
int numDownloadedItems,
LongIntMap feedIndicatorValues,
int reclaimableSpace) {
this.items = feeds;
this.queueSize = queueSize;
this.numNewItems = numNewItems;
this.numDownloadedItems = numDownloadedItems;
this.feedCounters = feedIndicatorValues;
this.reclaimableSpace = reclaimableSpace;
}
public abstract static class DrawerItem {
public enum Type {
FOLDER, FEED
}
public final Type type;
private int layer;
public long id;
public DrawerItem(Type type, long id) {
this.type = type;
this.id = id;
}
public int getLayer() {
return layer;
}
public void setLayer(int layer) {
this.layer = layer;
}
public abstract String getTitle();
public abstract int getCounter();
}
public static class FolderDrawerItem extends DrawerItem {
public final List<DrawerItem> children = new ArrayList<>();
public final String name;
public boolean isOpen;
public FolderDrawerItem(String name) {
super(DrawerItem.Type.FOLDER, (long) name.hashCode() << 20); // Keep IDs >0 but make room for many feeds
this.name = name;
}
public String getTitle() {
return name;
}
public int getCounter() {
int sum = 0;
for (DrawerItem item : children) {
sum += item.getCounter();
}
return sum;
}
}
public static class FeedDrawerItem extends DrawerItem {
public final Feed feed;
public final int counter;
public FeedDrawerItem(Feed feed, long id, int counter) {
super(DrawerItem.Type.FEED, id);
this.feed = feed;
this.counter = counter;
}
public String getTitle() {
return feed.getTitle();
}
public int getCounter() {
return counter;
}
}
}

View File

@ -115,6 +115,7 @@ public class PodDBAdapter {
public static final String KEY_FEED_PLAYBACK_SPEED = "feed_playback_speed";
public static final String KEY_FEED_SKIP_INTRO = "feed_skip_intro";
public static final String KEY_FEED_SKIP_ENDING = "feed_skip_ending";
public static final String KEY_FEED_TAGS = "tags";
public static final String KEY_EPISODE_NOTIFICATION = "episode_notification";
// Table names
@ -152,6 +153,7 @@ public class PodDBAdapter {
+ KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0,"
+ KEY_FEED_PLAYBACK_SPEED + " REAL DEFAULT " + SPEED_USE_GLOBAL + ","
+ KEY_FEED_VOLUME_ADAPTION + " INTEGER DEFAULT 0,"
+ KEY_FEED_TAGS + " TEXT,"
+ KEY_FEED_SKIP_INTRO + " INTEGER DEFAULT 0,"
+ KEY_FEED_SKIP_ENDING + " INTEGER DEFAULT 0,"
+ KEY_EPISODE_NOTIFICATION + " INTEGER DEFAULT 0)";
@ -255,6 +257,7 @@ public class PodDBAdapter {
TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER,
TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER,
TABLE_NAME_FEEDS + "." + KEY_FEED_PLAYBACK_SPEED,
TABLE_NAME_FEEDS + "." + KEY_FEED_TAGS,
TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_INTRO,
TABLE_NAME_FEEDS + "." + KEY_FEED_SKIP_ENDING,
TABLE_NAME_FEEDS + "." + KEY_EPISODE_NOTIFICATION
@ -447,6 +450,7 @@ public class PodDBAdapter {
values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter());
values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter());
values.put(KEY_FEED_PLAYBACK_SPEED, prefs.getFeedPlaybackSpeed());
values.put(KEY_FEED_TAGS, prefs.getTagsAsString());
values.put(KEY_FEED_SKIP_INTRO, prefs.getFeedSkipIntro());
values.put(KEY_FEED_SKIP_ENDING, prefs.getFeedSkipEnding());
values.put(KEY_EPISODE_NOTIFICATION, prefs.getShowEpisodeNotification());

View File

@ -731,9 +731,12 @@
<string name="apply_action">Apply action</string>
<string name="play_chapter">Play chapter</string>
<!-- Feed information screen -->
<!-- Feed settings/information screen -->
<string name="authentication_label">Authentication</string>
<string name="authentication_descr">Change your username and password for this podcast and its episodes.</string>
<string name="feed_folders_label">Folders</string>
<string name="feed_folders_summary">Change the folders in which this podcast is displayed.</string>
<string name="feed_folders_include_root">Show in main list</string>
<string name="auto_download_settings_label">Auto Download Settings</string>
<string name="episode_filters_label">Episode Filter</string>
<string name="episode_filters_description">List of terms used to decide if an episode should be included or excluded when auto downloading</string>

View File

@ -322,8 +322,8 @@ public class DbReaderTest {
final int numFeeds = 10;
final int numItems = 10;
DbTestUtils.saveFeedlist(numFeeds, numItems, true);
DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData();
assertEquals(numFeeds, navDrawerData.feeds.size());
NavDrawerData navDrawerData = DBReader.getNavDrawerData();
assertEquals(numFeeds, navDrawerData.items.size());
assertEquals(0, navDrawerData.numNewItems);
assertEquals(0, navDrawerData.queueSize);
}
@ -351,8 +351,8 @@ public class DbReaderTest {
adapter.close();
DBReader.NavDrawerData navDrawerData = DBReader.getNavDrawerData();
assertEquals(numFeeds, navDrawerData.feeds.size());
NavDrawerData navDrawerData = DBReader.getNavDrawerData();
assertEquals(numFeeds, navDrawerData.items.size());
assertEquals(numNew, navDrawerData.numNewItems);
assertEquals(numQueue, navDrawerData.queueSize);
}