Tags above subscriptions screen (#7954)

This commit is contained in:
Hans-Peter Lehmann 2025-08-31 11:35:04 +02:00 committed by GitHub
parent acc2638f3b
commit ad94f2f647
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 477 additions and 360 deletions

View File

@ -27,10 +27,8 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.common.ThemeSwitcher;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.databinding.SubscriptionSelectionActivityBinding;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
@ -74,20 +72,6 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
}
public List<Feed> getFeedItems(List<NavDrawerData.DrawerItem> items, List<Feed> result) {
for (NavDrawerData.DrawerItem item : items) {
if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
getFeedItems(((NavDrawerData.TagDrawerItem) item).getChildren(), result);
} else {
Feed feed = ((NavDrawerData.FeedDrawerItem) item).feed;
if (!result.contains(feed)) {
result.add(feed);
}
}
}
return result;
}
private void addShortcut(Feed feed, Bitmap bitmap) {
Intent intent = new Intent(this, MainActivity.class);
intent.setAction(Intent.ACTION_MAIN);
@ -140,12 +124,7 @@ public class SelectSubscriptionActivity extends AppCompatActivity {
if (disposable != null) {
disposable.dispose();
}
disposable = Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting());
return getFeedItems(data.items, new ArrayList<>());
})
disposable = Observable.fromCallable(DBReader::getFeedList)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(

View File

@ -0,0 +1,58 @@
package de.danoeh.antennapod.ui.screen.drawer;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.database.NavDrawerData;
public class DrawerItem {
private final Feed feed;
private final NavDrawerData.TagItem tag;
private int counter;
private final int layer;
public DrawerItem(@NonNull Feed feed, int counter, int layer) {
this.tag = null;
this.feed = feed;
this.counter = counter;
this.layer = layer;
}
public DrawerItem(@NonNull NavDrawerData.TagItem tag) {
this.feed = null;
this.tag = tag;
this.counter = 0;
this.layer = 0;
}
public boolean isFeed() {
return feed != null;
}
public Feed asFeed() {
return feed;
}
public NavDrawerData.TagItem asTag() {
return tag;
}
public void setCounter(int counter) {
this.counter = counter;
}
public int getCounter() {
return counter;
}
public int getLayer() {
return layer;
}
public String getTitle() {
return isFeed() ? feed.getTitle() : tag.getTitle();
}
public long getId() {
return isFeed() ? feed.getId() : tag.getId();
}
}

View File

@ -46,6 +46,7 @@ import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import de.danoeh.antennapod.R;
@ -80,8 +81,8 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
private NavDrawerData navDrawerData;
private int reclaimableSpace = 0;
private List<NavDrawerData.DrawerItem> flatItemList;
private NavDrawerData.DrawerItem contextPressedItem = null;
private List<DrawerItem> flatItemList;
private DrawerItem contextPressedItem = null;
private NavListAdapter navAdapter;
private Disposable disposable;
private ProgressBar progressBar;
@ -162,11 +163,12 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuInflater inflater = getActivity().getMenuInflater();
menu.setHeaderTitle(contextPressedItem.getTitle());
if (contextPressedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
if (contextPressedItem.isFeed()) {
menu.setHeaderTitle(contextPressedItem.asFeed().getTitle());
inflater.inflate(R.menu.nav_feed_context, menu);
// episodes are not loaded, so we cannot check if the podcast has new or unplayed ones!
} else {
menu.setHeaderTitle(contextPressedItem.asTag().getTitle());
inflater.inflate(R.menu.nav_folder_context, menu);
}
MenuItemUtils.setOnClickListeners(menu, this::onContextItemSelected);
@ -174,15 +176,15 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
NavDrawerData.DrawerItem pressedItem = contextPressedItem;
DrawerItem pressedItem = contextPressedItem;
contextPressedItem = null;
if (pressedItem == null) {
return false;
}
if (pressedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
return onFeedContextMenuClicked(((NavDrawerData.FeedDrawerItem) pressedItem).feed, item);
if (pressedItem.isFeed()) {
return onFeedContextMenuClicked(pressedItem.asFeed(), item);
} else {
return onTagContextMenuClicked(pressedItem, item);
return onTagContextMenuClicked(pressedItem.asTag(), item);
}
}
@ -204,7 +206,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
return super.onContextItemSelected(item);
}
private boolean onTagContextMenuClicked(NavDrawerData.DrawerItem drawerItem, MenuItem item) {
private boolean onTagContextMenuClicked(NavDrawerData.TagItem drawerItem, MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.rename_folder_item) {
new RenameFeedDialog(getActivity(), drawerItem).show();
@ -216,10 +218,8 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
List<NavDrawerData.DrawerItem> feeds = ((NavDrawerData.TagDrawerItem) drawerItem).getChildren();
for (NavDrawerData.DrawerItem feed : feeds) {
FeedPreferences preferences = ((NavDrawerData.FeedDrawerItem) feed).feed.getPreferences();
for (Feed feed : drawerItem.getFeeds()) {
FeedPreferences preferences = feed.getPreferences();
preferences.getTags().remove(drawerItem.getTitle());
DBWriter.setFeedPreferences(preferences);
}
@ -272,7 +272,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
}
@Override
public NavDrawerData.DrawerItem getItem(int position) {
public DrawerItem getItem(int position) {
if (flatItemList != null && 0 <= position && position < flatItemList.size()) {
return flatItemList.get(position);
} else {
@ -288,11 +288,11 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
} 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(
DrawerItem itemToCheck = flatItemList.get(
position - navAdapter.getSubscriptionOffset());
if (itemToCheck.type == NavDrawerData.DrawerItem.Type.FEED) {
if (itemToCheck.isFeed()) {
// When the same feed is displayed multiple times, it should be highlighted multiple times.
return ((NavDrawerData.FeedDrawerItem) itemToCheck).feed.getId() == feedId;
return itemToCheck.asFeed().getId() == feedId;
}
}
}
@ -341,15 +341,15 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
((MainActivity) getActivity()).getBottomSheet().setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
int pos = position - navAdapter.getSubscriptionOffset();
NavDrawerData.DrawerItem clickedItem = flatItemList.get(pos);
DrawerItem clickedItem = flatItemList.get(pos);
if (clickedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
long feedId = ((NavDrawerData.FeedDrawerItem) clickedItem).feed.getId();
if (clickedItem.isFeed()) {
long feedId = clickedItem.asFeed().getId();
((MainActivity) getActivity()).loadFeedFragmentById(feedId, null);
((MainActivity) getActivity()).getBottomSheet()
.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) clickedItem);
NavDrawerData.TagItem folder = clickedItem.asTag();
if (openFolders.contains(folder.getTitle())) {
openFolders.remove(folder.getTitle());
} else {
@ -361,7 +361,8 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
.putStringSet(PREF_OPEN_FOLDERS, openFolders)
.apply();
disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.items, 0))
disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.feeds,
navDrawerData.tags, navDrawerData.feedCounters))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
@ -409,7 +410,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting());
reclaimableSpace = EpisodeCleanupAlgorithmFactory.build().getReclaimableItems();
return new Pair<>(data, makeFlatDrawerData(data.items, 0));
return new Pair<>(data, makeFlatDrawerData(data.feeds, data.tags, data.feedCounters));
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -425,23 +426,38 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
});
}
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.TAG) {
NavDrawerData.TagDrawerItem folder = ((NavDrawerData.TagDrawerItem) item);
folder.setOpen(openFolders.contains(folder.getTitle()));
if (folder.isOpen()) {
flatItems.addAll(makeFlatDrawerData(
((NavDrawerData.TagDrawerItem) item).getChildren(), layer + 1));
private List<DrawerItem> makeFlatDrawerData(List<Feed> feeds, List<NavDrawerData.TagItem> tags,
@Nullable java.util.Map<Long, Integer> feedCounters) {
List<DrawerItem> flatItems = new ArrayList<>();
for (Feed feed : feeds) {
flatItems.add(new DrawerItem(feed, feedCounter(feed, feedCounters), 0));
}
for (NavDrawerData.TagItem tag : tags) {
if (FeedPreferences.TAG_ROOT.equals(tag.getTitle())) {
continue;
}
DrawerItem tagItem = new DrawerItem(tag);
flatItems.add(tagItem);
int counter = 0;
tag.setOpen(openFolders.contains(tag.getTitle()));
for (Feed feed : tag.getFeeds()) {
counter += feedCounter(feed, feedCounters);
if (tag.isOpen()) {
flatItems.add(new DrawerItem(feed, feedCounter(feed, feedCounters), 1));
}
}
tagItem.setCounter(counter);
}
return flatItems;
}
private int feedCounter(Feed feed, Map<Long, Integer> feedCounters) {
if (navDrawerData == null || feedCounters == null) {
return 0;
}
return feedCounters.containsKey(feed.getId()) ? feedCounters.get(feed.getId()) : 0;
}
public static void saveLastNavFragment(Context context, String tag) {
Log.d(TAG, "saveLastNavFragment(tag: " + tag + ")");
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);

View File

@ -107,7 +107,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
public long getItemId(int position) {
int viewType = getItemViewType(position);
if (viewType == VIEW_TYPE_SUBSCRIPTION) {
return itemAccess.getItem(position - getSubscriptionOffset()).id;
return itemAccess.getItem(position - getSubscriptionOffset()).getId();
} else if (viewType == VIEW_TYPE_NAV) {
return -Math.abs((long) fragmentTags.get(position).hashCode()) - 1; // Folder IDs are >0
} else {
@ -154,12 +154,12 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
bindSectionDivider((DividerHolder) holder);
} else {
int itemPos = position - getSubscriptionOffset();
NavDrawerData.DrawerItem item = itemAccess.getItem(itemPos);
DrawerItem item = itemAccess.getItem(itemPos);
bindListItem(item, (FeedHolder) holder);
if (item.type == NavDrawerData.DrawerItem.Type.FEED) {
bindFeedView((NavDrawerData.FeedDrawerItem) item, (FeedHolder) holder);
if (item.isFeed()) {
bindFeedView(item.asFeed(), (FeedHolder) holder);
} else {
bindTagView((NavDrawerData.TagDrawerItem) item, (FeedHolder) holder);
bindTagView(item.asTag(), (FeedHolder) holder);
}
holder.itemView.setOnCreateContextMenuListener(itemAccess);
}
@ -231,7 +231,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
}
}
private void bindListItem(NavDrawerData.DrawerItem item, FeedHolder holder) {
private void bindListItem(DrawerItem item, FeedHolder holder) {
if (item.getCounter() > 0) {
holder.count.setVisibility(View.VISIBLE);
holder.count.setText(NumberFormat.getInstance().format(item.getCounter()));
@ -243,8 +243,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
holder.itemView.setPadding(item.getLayer() * padding, 0, 0, 0);
}
private void bindFeedView(NavDrawerData.FeedDrawerItem drawerItem, FeedHolder holder) {
Feed feed = drawerItem.feed;
private void bindFeedView(Feed feed, FeedHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
@ -264,7 +263,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
holder.failure.setVisibility(feed.hasLastUpdateFailed() ? View.VISIBLE : View.GONE);
}
private void bindTagView(NavDrawerData.TagDrawerItem tag, FeedHolder holder) {
private void bindTagView(NavDrawerData.TagItem tag, FeedHolder holder) {
Activity context = activity.get();
if (context == null) {
return;
@ -323,7 +322,7 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
public interface ItemAccess extends View.OnCreateContextMenuListener {
int getCount();
NavDrawerData.DrawerItem getItem(int position);
DrawerItem getItem(int position);
boolean isSelected(int position);

View File

@ -3,8 +3,6 @@ package de.danoeh.antennapod.ui.screen.feed;
import android.app.Activity;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import android.view.LayoutInflater;
import androidx.appcompat.app.AlertDialog;
@ -20,16 +18,16 @@ public class RenameFeedDialog {
private final WeakReference<Activity> activityRef;
private Feed feed = null;
private NavDrawerData.DrawerItem drawerItem = null;
private NavDrawerData.TagItem tag = null;
public RenameFeedDialog(Activity activity, Feed feed) {
this.activityRef = new WeakReference<>(activity);
this.feed = feed;
}
public RenameFeedDialog(Activity activity, NavDrawerData.DrawerItem drawerItem) {
public RenameFeedDialog(Activity activity, NavDrawerData.TagItem drawerItem) {
this.activityRef = new WeakReference<>(activity);
this.drawerItem = drawerItem;
this.tag = drawerItem;
}
public void show() {
@ -39,7 +37,7 @@ public class RenameFeedDialog {
}
final EditTextDialogBinding binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity));
String title = feed != null ? feed.getTitle() : drawerItem.getTitle();
String title = feed != null ? feed.getTitle() : tag.getTitle();
binding.textInput.setText(title);
AlertDialog dialog = new MaterialAlertDialogBuilder(activity)
@ -64,17 +62,11 @@ public class RenameFeedDialog {
}
private void renameTag(String title) {
if (NavDrawerData.DrawerItem.Type.TAG == drawerItem.type) {
List<FeedPreferences> feedPreferences = new ArrayList<>();
for (NavDrawerData.DrawerItem item : ((NavDrawerData.TagDrawerItem) drawerItem).getChildren()) {
feedPreferences.add(((NavDrawerData.FeedDrawerItem) item).feed.getPreferences());
}
for (FeedPreferences preferences : feedPreferences) {
preferences.getTags().remove(drawerItem.getTitle());
preferences.getTags().add(title);
DBWriter.setFeedPreferences(preferences);
}
for (Feed feed : tag.getFeeds()) {
FeedPreferences preferences = feed.getPreferences();
preferences.getTags().remove(tag.getTitle());
preferences.getTags().add(title);
DBWriter.setFeedPreferences(preferences);
}
}

View File

@ -7,33 +7,30 @@ import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.GridLayoutManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.databinding.EditTagsDialogBinding;
import de.danoeh.antennapod.model.feed.FeedCounter;
import de.danoeh.antennapod.model.feed.FeedOrder;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.ui.SimpleChipAdapter;
import de.danoeh.antennapod.ui.view.ItemOffsetDecoration;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.SimpleChipAdapter;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.databinding.EditTagsDialogBinding;
import de.danoeh.antennapod.model.feed.FeedCounter;
import de.danoeh.antennapod.model.feed.FeedOrder;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.ui.view.ItemOffsetDecoration;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class TagSettingsDialog extends DialogFragment {
public static final String TAG = "TagSettingsDialog";
private static final String ARG_FEED_PREFERENCES = "feed_preferences";
@ -113,10 +110,9 @@ public class TagSettingsDialog extends DialogFragment {
Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData(null, FeedOrder.ALPHABETICAL, FeedCounter.SHOW_NONE);
List<NavDrawerData.DrawerItem> items = data.items;
List<String> folders = new ArrayList<String>();
for (NavDrawerData.DrawerItem item : items) {
if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
List<String> folders = new ArrayList<>();
for (NavDrawerData.TagItem item : data.tags) {
if (!FeedPreferences.TAG_ROOT.equals(item.getTitle())) {
folders.add(item.getTitle());
}
}

View File

@ -11,7 +11,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.core.util.Pair;
import androidx.fragment.app.Fragment;
@ -19,43 +18,40 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.ui.common.ConfirmationDialog;
import de.danoeh.antennapod.ui.screen.AddFeedFragment;
import de.danoeh.antennapod.ui.screen.SearchFragment;
import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager;
import de.danoeh.antennapod.ui.view.FloatingSelectMenu;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.ui.MenuItemUtils;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.ui.screen.feed.RenameFeedDialog;
import de.danoeh.antennapod.event.FeedListUpdateEvent;
import de.danoeh.antennapod.event.FeedUpdateRunningEvent;
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager;
import de.danoeh.antennapod.storage.database.DBReader;
import de.danoeh.antennapod.storage.database.DBWriter;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.MenuItemUtils;
import de.danoeh.antennapod.ui.common.ConfirmationDialog;
import de.danoeh.antennapod.ui.screen.AddFeedFragment;
import de.danoeh.antennapod.ui.screen.SearchFragment;
import de.danoeh.antennapod.ui.screen.feed.RenameFeedDialog;
import de.danoeh.antennapod.ui.statistics.StatisticsFragment;
import de.danoeh.antennapod.ui.view.EmptyViewHandler;
import de.danoeh.antennapod.ui.view.FloatingSelectMenu;
import de.danoeh.antennapod.ui.view.ItemOffsetDecoration;
import de.danoeh.antennapod.ui.view.LiftOnScrollListener;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import java.util.Locale;
/**
* Fragment for displaying feed subscriptions
@ -66,6 +62,7 @@ public class SubscriptionFragment extends Fragment
public static final String TAG = "SubscriptionFragment";
private static final String PREFS = "SubscriptionFragment";
private static final String PREF_NUM_COLUMNS = "columns";
private static final String PREF_LAST_TAG = "last_tag";
private static final String KEY_UP_ARROW = "up_arrow";
private static final String ARGUMENT_FOLDER = "folder";
@ -79,12 +76,13 @@ public class SubscriptionFragment extends Fragment
private RecyclerView subscriptionRecycler;
private SubscriptionsRecyclerAdapter subscriptionAdapter;
private RecyclerView tagsRecycler;
private SubscriptionTagAdapter tagAdapter;
private EmptyViewHandler emptyView;
private View feedsFilteredMsg;
private MaterialToolbar toolbar;
private SwipeRefreshLayout swipeRefreshLayout;
private ProgressBar progressBar;
private String displayedFolder = null;
private boolean displayUpArrow;
private Disposable disposable;
@ -94,7 +92,7 @@ public class SubscriptionFragment extends Fragment
private FloatingActionButton subscriptionAddButton;
private FloatingSelectMenu floatingSelectMenu;
private RecyclerView.ItemDecoration itemDecoration;
private List<NavDrawerData.DrawerItem> listItems;
private List<Feed> feeds;
public static SubscriptionFragment newInstance(String folderTitle) {
SubscriptionFragment fragment = new SubscriptionFragment();
@ -134,13 +132,6 @@ public class SubscriptionFragment extends Fragment
}
refreshToolbarState();
if (getArguments() != null) {
displayedFolder = getArguments().getString(ARGUMENT_FOLDER, null);
if (displayedFolder != null) {
toolbar.setTitle(displayedFolder);
}
}
subscriptionRecycler = root.findViewById(R.id.subscriptions_grid);
registerForContextMenu(subscriptionRecycler);
subscriptionRecycler.addOnScrollListener(new LiftOnScrollListener(root.findViewById(R.id.appbar)));
@ -186,6 +177,28 @@ public class SubscriptionFragment extends Fragment
return true;
});
tagsRecycler = root.findViewById(R.id.tags_recycler);
tagsRecycler.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
tagsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4, 0));
registerForContextMenu(tagsRecycler);
tagAdapter = new SubscriptionTagAdapter(getActivity()) {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuItemUtils.setOnClickListeners(menu, SubscriptionFragment.this::onTagContextItemSelected);
}
@Override
protected void onTagClick(NavDrawerData.TagItem tag) {
tagAdapter.setSelectedTag(tag.getTitle());
loadSubscriptionsAndTags();
}
};
tagAdapter.setSelectedTag(prefs.getString(PREF_LAST_TAG, FeedPreferences.TAG_ROOT));
tagsRecycler.setAdapter(tagAdapter);
if (getArguments() != null) {
tagAdapter.setSelectedTag(getArguments().getString(ARGUMENT_FOLDER, null));
}
return root;
}
@ -282,13 +295,14 @@ public class SubscriptionFragment extends Fragment
public void onStart() {
super.onStart();
EventBus.getDefault().register(this);
loadSubscriptions();
loadSubscriptionsAndTags();
}
@Override
public void onPause() {
super.onPause();
scrollPosition = getScrollPosition();
prefs.edit().putString(PREF_LAST_TAG, tagAdapter.getSelectedTag()).apply();
}
@Override
@ -303,44 +317,45 @@ public class SubscriptionFragment extends Fragment
}
}
private void loadSubscriptions() {
private void loadSubscriptionsAndTags() {
if (disposable != null) {
disposable.dispose();
}
emptyView.hide();
disposable = Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting());
List<NavDrawerData.DrawerItem> items = data.items;
for (NavDrawerData.DrawerItem item : items) {
if (item.type == NavDrawerData.DrawerItem.Type.TAG
&& item.getTitle().equals(displayedFolder)) {
return ((NavDrawerData.TagDrawerItem) item).getChildren();
}
}
return items;
})
() -> DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> {
final boolean firstLoaded = listItems == null || listItems.isEmpty();
if (listItems != null && listItems.size() > result.size()) {
List<Feed> openedFolderFeeds = result.feeds;
for (NavDrawerData.TagItem tag : result.tags) {
if (tag.getTitle().equals(tagAdapter.getSelectedTag())) {
openedFolderFeeds = tag.getFeeds();
break;
}
}
final boolean firstLoaded = feeds == null || feeds.isEmpty();
if (feeds != null && feeds.size() > openedFolderFeeds.size()) {
// We have fewer items. This can result in items being selected that are no longer visible.
subscriptionAdapter.endSelectMode();
}
listItems = result;
feeds = openedFolderFeeds;
progressBar.setVisibility(View.GONE);
subscriptionAdapter.setItems(result);
subscriptionAdapter.setItems(feeds, result.feedCounters);
if (firstLoaded) {
restoreScrollPosition(scrollPosition);
}
emptyView.updateVisibility();
if (tagAdapter != null) {
tagAdapter.setTags(result.tags);
tagsRecycler.setVisibility(result.tags.size() > 1 ? View.VISIBLE : View.GONE);
}
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
});
updateFilterVisibility();
}
@ -358,75 +373,71 @@ public class SubscriptionFragment extends Fragment
return getResources().getInteger(R.integer.subscriptions_default_num_of_columns);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
NavDrawerData.DrawerItem drawerItem = subscriptionAdapter.getSelectedItem();
if (drawerItem == null) {
private boolean onTagContextItemSelected(MenuItem item) {
NavDrawerData.TagItem selectedTag = tagAdapter.getLongPressedItem();
if (selectedTag == null) {
return false;
}
int itemId = item.getItemId();
if (drawerItem.type == NavDrawerData.DrawerItem.Type.TAG) {
if (itemId == R.id.rename_folder_item) {
new RenameFeedDialog(getActivity(), drawerItem).show();
return true;
} else if (itemId == R.id.delete_folder_item) {
ConfirmationDialog dialog = new ConfirmationDialog(
getContext(), R.string.delete_tag_label,
getString(R.string.delete_tag_confirmation, drawerItem.getTitle())) {
if (itemId == R.id.rename_folder_item) {
new RenameFeedDialog(getActivity(), selectedTag).show();
return true;
} else if (itemId == R.id.delete_folder_item) {
ConfirmationDialog dialog = new ConfirmationDialog(getContext(), R.string.delete_tag_label,
getString(R.string.delete_tag_confirmation, selectedTag.getTitle())) {
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
List<NavDrawerData.DrawerItem> feeds = ((NavDrawerData.TagDrawerItem) drawerItem).getChildren();
for (NavDrawerData.DrawerItem feed : feeds) {
FeedPreferences preferences = ((NavDrawerData.FeedDrawerItem) feed).feed.getPreferences();
preferences.getTags().remove(drawerItem.getTitle());
DBWriter.setFeedPreferences(preferences);
}
@Override
public void onConfirmButtonPressed(DialogInterface dialog) {
tagAdapter.setSelectedTag(FeedPreferences.TAG_ROOT);
for (Feed feed : selectedTag.getFeeds()) {
FeedPreferences preferences = feed.getPreferences();
preferences.getTags().remove(selectedTag.getTitle());
DBWriter.setFeedPreferences(preferences);
}
};
dialog.createNewDialog().show();
}
};
dialog.createNewDialog().show();
return true;
}
return true;
}
return false;
}
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
if (itemId == R.id.multi_select) {
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
Feed selectedFeed = subscriptionAdapter.getSelectedItem();
if (selectedFeed == null) {
return false;
}
if (item.getItemId() == R.id.multi_select) {
return subscriptionAdapter.onContextItemSelected(item);
}
return FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), feed, this::loadSubscriptions);
return FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedFeed, this::loadSubscriptionsAndTags);
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onFeedListChanged(FeedListUpdateEvent event) {
loadSubscriptions();
loadSubscriptionsAndTags();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onUnreadItemsChanged(UnreadItemsUpdateEvent event) {
loadSubscriptions();
loadSubscriptionsAndTags();
}
@Override
public void onEndSelectMode() {
floatingSelectMenu.setVisibility(View.GONE);
subscriptionAddButton.setVisibility(View.VISIBLE);
subscriptionAdapter.setItems(listItems);
tagsRecycler.setVisibility(tagAdapter.getItemCount() > 1 ? View.VISIBLE : View.GONE);
updateFilterVisibility();
}
@Override
public void onStartSelectMode() {
List<NavDrawerData.DrawerItem> feedsOnly = new ArrayList<>();
for (NavDrawerData.DrawerItem item : listItems) {
if (item.type == NavDrawerData.DrawerItem.Type.FEED) {
feedsOnly.add(item);
}
}
subscriptionAdapter.setItems(feedsOnly);
floatingSelectMenu.setVisibility(View.VISIBLE);
subscriptionAddButton.setVisibility(View.GONE);
tagsRecycler.setVisibility(tagAdapter.getItemCount() > 1 ? View.INVISIBLE : View.GONE);
updateFilterVisibility();
}

View File

@ -0,0 +1,116 @@
package de.danoeh.antennapod.ui.screen.subscriptions;
import android.app.Activity;
import android.os.Build;
import android.view.ContextMenu;
import android.view.InputDevice;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.chip.Chip;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class SubscriptionTagAdapter extends RecyclerView.Adapter<SubscriptionTagAdapter.TagViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<Activity> activityRef;
private List<NavDrawerData.TagItem> tags = new ArrayList<>();
private String selectedTag = null;
private NavDrawerData.TagItem longPressedItem = null;
public SubscriptionTagAdapter(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
public void setTags(List<NavDrawerData.TagItem> tags) {
this.tags = tags;
notifyDataSetChanged();
}
public void setSelectedTag(String tag) {
this.selectedTag = tag;
notifyDataSetChanged();
}
public String getSelectedTag() {
return selectedTag;
}
@NonNull
@Override
public TagViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_tag_chip, parent, false);
return new TagViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TagViewHolder holder, int position) {
NavDrawerData.TagItem tag = tags.get(position);
if (FeedPreferences.TAG_ROOT.equals(tag.getTitle())) {
holder.chip.setText(R.string.tag_all);
} else {
String title = tag.getTitle();
if (title.length() > 20) {
title = title.substring(0, 19) + "";
}
holder.chip.setText(title);
}
holder.chip.setChecked(tag.getTitle().equals(selectedTag));
holder.chip.setOnClickListener(v -> onTagClick(tag));
holder.chip.setOnTouchListener((v, e) -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
&& e.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
longPressedItem = tag;
}
}
return false;
});
holder.chip.setOnLongClickListener(v -> {
longPressedItem = tag;
return false;
});
holder.chip.setOnCreateContextMenuListener(this);
}
@Override
public int getItemCount() {
return tags != null ? tags.size() : 0;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (longPressedItem == null || FeedPreferences.TAG_ROOT.equals(longPressedItem.getTitle())) {
return;
}
MenuInflater inflater = activityRef.get().getMenuInflater();
inflater.inflate(R.menu.nav_folder_context, menu);
menu.setHeaderTitle(longPressedItem.getTitle());
}
protected void onTagClick(NavDrawerData.TagItem tag) {
}
public NavDrawerData.TagItem getLongPressedItem() {
return longPressedItem;
}
public static class TagViewHolder extends RecyclerView.ViewHolder {
public final Chip chip;
public TagViewHolder(@NonNull View itemView) {
super(itemView);
chip = itemView.findViewById(R.id.tag_chip);
}
}
}

View File

@ -13,7 +13,6 @@ import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.CoverLoader;
import de.danoeh.antennapod.ui.common.ThemeUtils;
@ -45,35 +44,28 @@ public class SubscriptionViewHolder extends RecyclerView.ViewHolder {
this.mainActivityRef = new WeakReference<>(mainActivity);
}
public void bind(NavDrawerData.DrawerItem drawerItem, int columnCount) {
public void bind(Feed feed, int columnCount, int counter) {
if (selectView != null) {
Drawable drawable = AppCompatResources.getDrawable(selectView.getContext(),
R.drawable.ic_checkbox_background);
selectView.setBackground(drawable); // Setting this in XML crashes API <= 21
}
title.setText(drawerItem.getTitle());
fallbackTitle.setText(drawerItem.getTitle());
coverImage.setContentDescription(drawerItem.getTitle());
if (drawerItem.getCounter() > 0) {
count.setText(NumberFormat.getInstance().format(drawerItem.getCounter()));
title.setText(feed.getTitle());
fallbackTitle.setText(feed.getTitle());
coverImage.setContentDescription(feed.getTitle());
if (counter > 0) {
count.setText(NumberFormat.getInstance().format(counter));
count.setVisibility(View.VISIBLE);
} else {
count.setVisibility(View.GONE);
}
CoverLoader coverLoader = new CoverLoader();
boolean textAndImageCombined;
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
textAndImageCombined = feed.isLocalFeed() && feed.getImageUrl() != null
&& feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER);
coverLoader.withUri(feed.getImageUrl());
errorIcon.setVisibility(feed.hasLastUpdateFailed() ? View.VISIBLE : View.GONE);
} else {
textAndImageCombined = true;
coverLoader.withResource(R.drawable.ic_tag);
errorIcon.setVisibility(View.GONE);
}
boolean textAndImageCombined = feed.isLocalFeed() && feed.getImageUrl() != null
&& feed.getImageUrl().startsWith(Feed.PREFIX_GENERATIVE_COVER);
coverLoader.withUri(feed.getImageUrl());
errorIcon.setVisibility(feed.hasLastUpdateFailed() ? View.VISIBLE : View.GONE);
if (UserPreferences.shouldShowSubscriptionTitle() || columnCount == 1) {
// No need for fallback title when already showing title
fallbackTitle.setVisibility(View.GONE);

View File

@ -18,7 +18,6 @@ import androidx.recyclerview.widget.RecyclerView;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.SelectableAdapter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
@ -27,6 +26,7 @@ import de.danoeh.antennapod.ui.screen.feed.FeedItemlistFragment;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Adapter for subscriptions
@ -34,8 +34,9 @@ import java.util.List;
public class SubscriptionsRecyclerAdapter extends SelectableAdapter<SubscriptionViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<MainActivity> mainActivityRef;
private List<NavDrawerData.DrawerItem> listItems;
private NavDrawerData.DrawerItem selectedItem = null;
private List<Feed> listItems;
private Map<Long, Integer> feedCounters;
private Feed selectedItem = null;
int longPressedPosition = 0; // used to init actionMode
private int columnCount = 3;
@ -43,6 +44,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
super(mainActivity);
this.mainActivityRef = new WeakReference<>(mainActivity);
this.listItems = new ArrayList<>();
this.feedCounters = Map.of();
setHasStableIds(true);
}
@ -54,7 +56,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
return listItems.get(position);
}
public NavDrawerData.DrawerItem getSelectedItem() {
public Feed getSelectedItem() {
return selectedItem;
}
@ -75,16 +77,13 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
@Override
public void onBindViewHolder(@NonNull SubscriptionViewHolder holder, int position) {
NavDrawerData.DrawerItem drawerItem = listItems.get(position);
boolean isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED;
holder.bind(drawerItem, columnCount);
Feed feed = listItems.get(position);
holder.bind(feed, columnCount, feedCounters.containsKey(feed.getId()) ? feedCounters.get(feed.getId()) : 0);
holder.itemView.setOnCreateContextMenuListener(this);
if (inActionMode()) {
if (holder.selectView != null) {
if (isFeed) {
holder.selectCheckbox.setVisibility(View.VISIBLE);
}
holder.selectView.setVisibility(isFeed ? View.VISIBLE : View.GONE);
holder.selectCheckbox.setVisibility(View.VISIBLE);
holder.selectView.setVisibility(View.VISIBLE);
holder.selectCheckbox.setChecked((isSelected(position)));
holder.selectCheckbox.setOnCheckedChangeListener((buttonView, isChecked)
-> setSelected(holder.getBindingAdapterPosition(), isChecked));
@ -106,10 +105,8 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
holder.itemView.setOnLongClickListener(v -> {
if (!inActionMode()) {
if (isFeed) {
longPressedPosition = holder.getBindingAdapterPosition();
}
selectedItem = drawerItem;
longPressedPosition = holder.getBindingAdapterPosition();
selectedItem = feed;
}
return false;
});
@ -119,27 +116,19 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
&& e.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
if (!inActionMode()) {
if (isFeed) {
longPressedPosition = holder.getBindingAdapterPosition();
}
selectedItem = drawerItem;
longPressedPosition = holder.getBindingAdapterPosition();
selectedItem = feed;
}
}
}
return false;
});
holder.itemView.setOnClickListener(v -> {
if (isFeed) {
if (inActionMode()) {
setSelected(holder.getBindingAdapterPosition(), !isSelected(holder.getBindingAdapterPosition()));
notifyItemChanged(holder.getBindingAdapterPosition());
} else {
Fragment fragment = FeedItemlistFragment
.newInstance(((NavDrawerData.FeedDrawerItem) drawerItem).feed.getId());
mainActivityRef.get().loadChildFragment(fragment);
}
} else if (!inActionMode()) {
Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle());
if (inActionMode()) {
setSelected(holder.getBindingAdapterPosition(), !isSelected(holder.getBindingAdapterPosition()));
notifyItemChanged(holder.getBindingAdapterPosition());
} else {
Fragment fragment = FeedItemlistFragment.newInstance(feed.getId());
mainActivityRef.get().loadChildFragment(fragment);
}
});
@ -156,7 +145,7 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
if (position >= listItems.size()) {
return RecyclerView.NO_ID; // Dummy views
}
return listItems.get(position).id;
return listItems.get(position).getId();
}
@Override
@ -165,12 +154,8 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
return;
}
MenuInflater inflater = mainActivityRef.get().getMenuInflater();
if (selectedItem.type == NavDrawerData.DrawerItem.Type.FEED) {
inflater.inflate(R.menu.nav_feed_context, menu);
menu.findItem(R.id.multi_select).setVisible(true);
} else {
inflater.inflate(R.menu.nav_folder_context, menu);
}
inflater.inflate(R.menu.nav_feed_context, menu);
menu.findItem(R.id.multi_select).setVisible(true);
menu.setHeaderTitle(selectedItem.getTitle());
}
@ -186,29 +171,18 @@ public class SubscriptionsRecyclerAdapter extends SelectableAdapter<Subscription
List<Feed> items = new ArrayList<>();
for (int i = 0; i < getItemCount(); i++) {
if (isSelected(i)) {
NavDrawerData.DrawerItem drawerItem = listItems.get(i);
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
items.add(feed);
}
items.add(listItems.get(i));
}
}
return items;
}
public void setItems(List<NavDrawerData.DrawerItem> listItems) {
public void setItems(List<Feed> listItems, Map<Long, Integer> feedCounters) {
this.listItems = listItems;
this.feedCounters = feedCounters;
notifyDataSetChanged();
}
@Override
public void setSelected(int pos, boolean selected) {
NavDrawerData.DrawerItem drawerItem = listItems.get(pos);
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
super.setSelected(pos, selected);
}
}
@Override
public int getItemViewType(int position) {
if (columnCount == 1) {

View File

@ -6,19 +6,24 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
/**
* Source: https://stackoverflow.com/a/30794046
*/
public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
private final int itemOffset;
private final int itemOffsetHorizontal;
private final int itemOffsetVertical;
public ItemOffsetDecoration(@NonNull Context context, int itemOffsetDp) {
itemOffset = (int) (itemOffsetDp * context.getResources().getDisplayMetrics().density);
itemOffsetHorizontal = (int) (itemOffsetDp * context.getResources().getDisplayMetrics().density);
itemOffsetVertical = (int) (itemOffsetDp * context.getResources().getDisplayMetrics().density);
}
public ItemOffsetDecoration(@NonNull Context context, int itemOffsetHorizontalDp, int itemOffsetVerticalDp) {
itemOffsetHorizontal = (int) (itemOffsetHorizontalDp * context.getResources().getDisplayMetrics().density);
itemOffsetVertical = (int) (itemOffsetVerticalDp * context.getResources().getDisplayMetrics().density);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(itemOffset, itemOffset, itemOffset, itemOffset);
outRect.set(itemOffsetHorizontal, itemOffsetVertical, itemOffsetHorizontal, itemOffsetVertical);
}
}

View File

@ -20,15 +20,32 @@
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/feeds_filtered_message"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="4dp"
android:layout_marginTop="-16dp"
android:background="?android:attr/selectableItemBackground"
android:text="@string/filtered_label" />
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tags_recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:clipToPadding="false"
android:visibility="visible"
tools:orientation="horizontal"
tools:listitem="@layout/item_tag_chip" />
<TextView
android:id="@+id/feeds_filtered_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="4dp"
android:background="?android:attr/selectableItemBackground"
android:text="@string/filtered_label" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.Chip
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tag_chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="true"
android:longClickable="true"
style="@style/Widget.Material3.Chip.Filter" />

View File

@ -343,7 +343,7 @@ public class DbReaderTest {
DbTestUtils.saveFeedlist(numFeeds, numItems, true);
NavDrawerData navDrawerData = DBReader.getNavDrawerData(
UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW);
assertEquals(numFeeds, navDrawerData.items.size());
assertEquals(numFeeds, navDrawerData.feeds.size());
assertEquals(0, navDrawerData.numNewItems);
assertEquals(0, navDrawerData.queueSize);
}
@ -373,7 +373,7 @@ public class DbReaderTest {
NavDrawerData navDrawerData = DBReader.getNavDrawerData(
UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW);
assertEquals(numFeeds, navDrawerData.items.size());
assertEquals(numFeeds, navDrawerData.feeds.size());
assertEquals(numNew, navDrawerData.numNewItems);
assertEquals(numQueue, navDrawerData.queueSize);
}

View File

@ -21,7 +21,6 @@ import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedOrder;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.model.feed.SubscriptionsFilter;
import de.danoeh.antennapod.model.download.DownloadResult;
@ -737,32 +736,21 @@ public final class DBReader {
final int numNewItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW));
final int numDownloadedItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED));
List<NavDrawerData.DrawerItem> items = new ArrayList<>();
Map<String, NavDrawerData.TagDrawerItem> folders = new HashMap<>();
Map<String, NavDrawerData.TagItem> tags = new HashMap<>();
for (Feed feed : feeds) {
for (String tag : feed.getPreferences().getTags()) {
if (!tags.containsKey(tag)) {
tags.put(tag, new NavDrawerData.TagItem(tag));
}
int counter = feedCounters.containsKey(feed.getId()) ? feedCounters.get(feed.getId()) : 0;
NavDrawerData.FeedDrawerItem drawerItem = new NavDrawerData.FeedDrawerItem(feed, feed.getId(), counter);
if (FeedPreferences.TAG_ROOT.equals(tag)) {
items.add(drawerItem);
continue;
}
NavDrawerData.TagDrawerItem folder;
if (folders.containsKey(tag)) {
folder = folders.get(tag);
} else {
folder = new NavDrawerData.TagDrawerItem(tag);
folders.put(tag, folder);
}
drawerItem.id |= folder.id;
folder.getChildren().add(drawerItem);
tags.get(tag).addFeed(feed, counter);
}
}
List<NavDrawerData.TagDrawerItem> foldersSorted = new ArrayList<>(folders.values());
Collections.sort(foldersSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
items.addAll(foldersSorted);
List<NavDrawerData.TagItem> tagsSorted = new ArrayList<>(tags.values());
Collections.sort(tagsSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
NavDrawerData result = new NavDrawerData(items, queueSize, numNewItems, numDownloadedItems, feedCounters);
NavDrawerData result = new NavDrawerData(feeds, tagsSorted,
queueSize, numNewItems, numDownloadedItems, feedCounters);
adapter.close();
return result;
}

View File

@ -7,60 +7,38 @@ import java.util.List;
import java.util.Map;
public class NavDrawerData {
public final List<DrawerItem> items;
public final List<Feed> feeds;
public final List<TagItem> tags;
public final int queueSize;
public final int numNewItems;
public final int numDownloadedItems;
public final Map<Long, Integer> feedCounters;
public NavDrawerData(List<DrawerItem> feeds,
public NavDrawerData(List<Feed> feeds,
List<TagItem> tags,
int queueSize,
int numNewItems,
int numDownloadedItems,
Map<Long, Integer> feedIndicatorValues) {
this.items = feeds;
this.feeds = feeds;
this.tags = tags;
this.queueSize = queueSize;
this.numNewItems = numNewItems;
this.numDownloadedItems = numDownloadedItems;
this.feedCounters = feedIndicatorValues;
}
public abstract static class DrawerItem {
public enum Type {
TAG, 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 TagDrawerItem extends DrawerItem {
private final List<DrawerItem> children = new ArrayList<>();
public static class TagItem {
private final String name;
private boolean isOpen;
private List<Feed> feeds = new ArrayList<>();
private int counter = 0;
private boolean isOpen = false;
private long id;
public TagDrawerItem(String name) {
// Keep IDs >0 but make room for many feeds
super(DrawerItem.Type.TAG, Math.abs((long) name.hashCode()) << 20);
public TagItem(String name) {
this.name = name;
// Keep IDs >0 but make room for many feeds
this.id = (Math.abs((long) name.hashCode()) << 20);
}
public String getTitle() {
@ -75,35 +53,21 @@ public class NavDrawerData {
isOpen = open;
}
public List<DrawerItem> getChildren() {
return children;
}
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 List<Feed> getFeeds() {
return feeds;
}
public int getCounter() {
return counter;
}
public void addFeed(Feed feed, int feedCounter) {
counter += feedCounter;
feeds.add(feed);
}
public long getId() {
return id;
}
}
}

View File

@ -761,7 +761,8 @@
<string name="authentication_descr">Change your username and password for this podcast and its episodes</string>
<string name="feed_tags_label">Tags</string>
<string name="feed_tags_summary">Change the tags of this podcast to help organize your subscriptions</string>
<string name="feed_folders_include_root">Show this podcast in main list</string>
<string name="feed_folders_include_root">Show this podcast in \"all\" tag</string>
<string name="tag_all">All</string>
<string name="multi_feed_common_tags_info">Only common tags from all selected subscriptions are shown. Other tags stay unaffected.</string>
<string name="auto_download_inbox_category">Automatically download episodes from the inbox</string>
<string name="episode_filters_label">Episode filter</string>