Add more explicit feature to archive feeds (#8022)

This commit is contained in:
Hans-Peter Lehmann 2025-10-11 13:48:08 +02:00 committed by GitHub
parent 6510bea11d
commit 23d862185d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 553 additions and 183 deletions

View File

@ -130,7 +130,7 @@ public class FeedItemMenuHandler {
setItemVisibility(menu, R.id.download_item, canDownload);
setItemVisibility(menu, R.id.transcript_item, canShowTranscript);
if (selectedItems.size() == 1 && selectedItems.get(0).getFeed().getState() != Feed.STATE_SUBSCRIBED) {
if (selectedItems.size() == 1 && selectedItems.get(0).getFeed().getState() == Feed.STATE_NOT_SUBSCRIBED) {
setItemVisibility(menu, R.id.mark_read_item, false);
}
@ -193,7 +193,7 @@ public class FeedItemMenuHandler {
} else if (menuItemId == R.id.mark_read_item) {
selectedItem.setPlayed(true);
DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true);
if (!selectedItem.getFeed().isLocalFeed() && selectedItem.getFeed().getState() == Feed.STATE_SUBSCRIBED
if (!selectedItem.getFeed().isLocalFeed() && selectedItem.getFeed().getState() != Feed.STATE_NOT_SUBSCRIBED
&& SynchronizationSettings.isProviderConnected()) {
FeedMedia media = selectedItem.getMedia();
// not all items have media, Gpodder only cares about those that do
@ -211,7 +211,7 @@ public class FeedItemMenuHandler {
selectedItem.setPlayed(false);
DBWriter.markItemPlayed(selectedItem, FeedItem.UNPLAYED, false);
if (!selectedItem.getFeed().isLocalFeed() && selectedItem.getMedia() != null
&& selectedItem.getFeed().getState() == Feed.STATE_SUBSCRIBED) {
&& selectedItem.getFeed().getState() != Feed.STATE_NOT_SUBSCRIBED) {
SynchronizationQueue.getInstance().enqueueEpisodeAction(
new EpisodeAction.Builder(selectedItem, EpisodeAction.NEW)
.currentTimestamp()

View File

@ -69,6 +69,7 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
private static final String ARG_QUERY = "query";
private static final String ARG_FEED = "feed";
private static final String ARG_FEED_NAME = "feedName";
private static final String ARG_ARCHIVED = "archived";
private static final int SEARCH_DEBOUNCE_INTERVAL = 1500;
private EpisodeItemListAdapter adapter;
@ -117,6 +118,12 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
return fragment;
}
public static SearchFragment newInstanceArchive() {
SearchFragment fragment = newInstance();
fragment.getArguments().putBoolean(ARG_ARCHIVED, true);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -198,10 +205,10 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
chip = layout.findViewById(R.id.feed_title_chip);
chip.setOnCloseIconClickListener(v -> {
getArguments().putLong(ARG_FEED, 0);
getArguments().putBoolean(ARG_ARCHIVED, false);
searchWithProgressBar();
});
chip.setVisibility((getArguments().getLong(ARG_FEED, 0) == 0) ? View.GONE : View.VISIBLE);
chip.setText(getArguments().getString(ARG_FEED_NAME, ""));
updateChipVisibility();
if (getArguments().getString(ARG_QUERY, null) != null) {
search();
}
@ -377,6 +384,17 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
search();
}
private void updateChipVisibility() {
chip.setVisibility(View.GONE);
if (getArguments().getBoolean(ARG_ARCHIVED, false)) {
chip.setVisibility(View.VISIBLE);
chip.setText(R.string.archive_feed_label_noun);
} else if (getArguments().getLong(ARG_FEED, 0) != 0) {
chip.setVisibility(View.VISIBLE);
chip.setText(getArguments().getString(ARG_FEED_NAME, ""));
}
}
private void search() {
if (disposableFeeds != null) {
disposableFeeds.dispose();
@ -386,7 +404,7 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
}
long feed = getArguments().getLong(ARG_FEED, 0);
boolean isSearchingFeed = feed != 0;
chip.setVisibility(isSearchingFeed ? View.VISIBLE : View.GONE);
updateChipVisibility();
adapterFeeds.setEndButton(R.string.search_online, isSearchingFeed ? null : this::searchOnline);
String query = searchView.getQuery().toString();
@ -394,11 +412,12 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
emptyViewHandler.setTitle(R.string.type_to_search);
return;
}
final int state = getArguments().getBoolean(ARG_ARCHIVED, false) ? Feed.STATE_ARCHIVED : Feed.STATE_SUBSCRIBED;
if (feed != 0) {
// Search within a feed
adapterFeeds.updateData(Collections.emptyList());
} else {
disposableFeeds = Observable.fromCallable(() -> DBReader.searchFeeds(query))
disposableFeeds = Observable.fromCallable(() -> DBReader.searchFeeds(query, state))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(results -> {
@ -407,7 +426,7 @@ public class SearchFragment extends Fragment implements EpisodeItemListAdapter.O
emptyViewHandler.setTitle(getString(R.string.no_results_for_query, query));
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
disposableEpisodes = Observable.fromCallable(() -> DBReader.searchFeedItems(feed, query))
disposableEpisodes = Observable.fromCallable(() -> DBReader.searchFeedItems(feed, query, state))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(results -> {

View File

@ -44,6 +44,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -167,6 +168,8 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
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 if (FeedPreferences.TAG_UNTAGGED.equals(contextPressedItem.asTag().getTitle())) {
return;
} else {
menu.setHeaderTitle(contextPressedItem.asTag().getTitle());
inflater.inflate(R.menu.nav_folder_context, menu);
@ -190,14 +193,8 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
private boolean onFeedContextMenuClicked(Feed feed, MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.remove_feed) {
RemoveFeedDialog.show(getContext(), feed, () -> {
if (String.valueOf(feed.getId()).equals(getLastNavFragment(getContext()))) {
((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null);
// Make sure fragment is hidden before actually starting to delete
getActivity().getSupportFragmentManager().executePendingTransactions();
}
});
if (itemId == R.id.remove_archive_feed) {
new RemoveFeedDialogClose(Collections.singletonList(feed)).show(getParentFragmentManager(), null);
return true;
}
if (FeedMenuHandler.onMenuItemClicked(this, itemId, feed, null)) {
@ -206,6 +203,26 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
return super.onContextItemSelected(item);
}
public static class RemoveFeedDialogClose extends RemoveFeedDialog {
public RemoveFeedDialogClose(@NonNull List<Feed> feeds) {
super(feeds);
}
public RemoveFeedDialogClose() {
super();
}
@Override
protected void onRemoveButtonPressed() {
if (String.valueOf(feeds.get(0).getId()).equals(getLastNavFragment(getContext()))) {
// Make sure fragment is hidden before actually starting to delete
((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null);
getActivity().getSupportFragmentManager().executePendingTransactions();
}
super.onRemoveButtonPressed();
}
}
private boolean onTagContextMenuClicked(NavDrawerData.TagItem drawerItem, MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.rename_folder_item) {
@ -361,7 +378,7 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
.putStringSet(PREF_OPEN_FOLDERS, openFolders)
.apply();
disposable = Observable.fromCallable(() -> makeFlatDrawerData(navDrawerData.feeds,
disposable = Observable.fromCallable(() -> makeFlatDrawerData(
navDrawerData.tags, navDrawerData.feedCounters))
.subscribeOn(Schedulers.computation())
.observeOn(AndroidSchedulers.mainThread())
@ -408,9 +425,10 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
disposable = Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData(UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting());
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting(),
Feed.STATE_SUBSCRIBED);
reclaimableSpace = EpisodeCleanupAlgorithmFactory.build().getReclaimableItems();
return new Pair<>(data, makeFlatDrawerData(data.feeds, data.tags, data.feedCounters));
return new Pair<>(data, makeFlatDrawerData(data.tags, data.feedCounters));
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -426,14 +444,20 @@ public class NavDrawerFragment extends Fragment implements SharedPreferences.OnS
});
}
private List<DrawerItem> makeFlatDrawerData(List<Feed> feeds, List<NavDrawerData.TagItem> tags,
private List<DrawerItem> makeFlatDrawerData(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())) {
for (Feed feed : tag.getFeeds()) {
flatItems.add(new DrawerItem(feed, feedCounter(feed, feedCounters), 0));
}
break;
}
}
for (NavDrawerData.TagItem tag : tags) {
if (FeedPreferences.TAG_ROOT.equals(tag.getTitle())
|| FeedPreferences.TAG_UNTAGGED.equals(tag.getTitle())) {
continue;
}
DrawerItem tagItem = new DrawerItem(tag);

View File

@ -22,6 +22,7 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
import com.bumptech.glide.request.RequestOptions;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.storage.database.NavDrawerData;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.common.ImagePlaceholder;
@ -271,6 +272,9 @@ public class NavListAdapter extends RecyclerView.Adapter<NavListAdapter.Holder>
if (tag.isOpen()) {
holder.count.setVisibility(View.GONE);
}
if (FeedPreferences.TAG_UNTAGGED.equals(tag.getTitle())) {
holder.title.setText(R.string.tag_untagged);
}
Glide.with(context).clear(holder.image);
holder.image.setImageResource(R.drawable.ic_tag);
holder.failure.setVisibility(View.GONE);

View File

@ -268,7 +268,7 @@ public class ItemFragment extends Fragment {
viewBinding.txtvPublished.setText(pubDateStr);
viewBinding.txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate()));
}
if (item.getFeed().getState() != Feed.STATE_SUBSCRIBED) {
if (item.getFeed().getState() == Feed.STATE_NOT_SUBSCRIBED) {
viewBinding.nonSubscribedWarningLabel.setVisibility(View.VISIBLE);
viewBinding.nonSubscribedWarningLabel.setOnClickListener(v -> openPodcast());
}
@ -349,12 +349,12 @@ public class ItemFragment extends Fragment {
if (item == null) {
return;
}
if (item.getFeed().getState() == Feed.STATE_SUBSCRIBED) {
Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId());
((MainActivity) getActivity()).loadChildFragment(fragment);
} else {
if (item.getFeed().getState() == Feed.STATE_NOT_SUBSCRIBED) {
startActivity(new OnlineFeedviewActivityStarter(getContext(), item.getFeed().getDownloadUrl())
.getIntent());
} else {
Fragment fragment = FeedItemlistFragment.newInstance(item.getFeedId());
((MainActivity) getActivity()).loadChildFragment(fragment);
}
}

View File

@ -181,10 +181,10 @@ public class ItemPagerFragment extends Fragment implements MaterialToolbar.OnMen
if (item == null) {
return;
}
if (item.getFeed().getState() == Feed.STATE_SUBSCRIBED) {
new MainActivityStarter(getContext()).withOpenFeed(item.getFeedId()).withClearTop().start();
} else {
if (item.getFeed().getState() == Feed.STATE_NOT_SUBSCRIBED) {
startActivity(new OnlineFeedviewActivityStarter(getContext(), item.getFeed().getDownloadUrl()).getIntent());
} else {
new MainActivityStarter(getContext()).withOpenFeed(item.getFeedId()).withClearTop().start();
}
}

View File

@ -244,17 +244,7 @@ public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenu
viewBinding.supportUrl.setText(str.toString());
}
if (feed.getState() == Feed.STATE_SUBSCRIBED) {
long feedId = getArguments().getLong(EXTRA_FEED_ID);
getParentFragmentManager().beginTransaction().replace(R.id.statisticsFragmentContainer,
FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment")
.commitAllowingStateLoss();
viewBinding.statisticsButton.setOnClickListener(view -> {
StatisticsFragment fragment = new StatisticsFragment();
((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE);
});
} else {
if (feed.getState() == Feed.STATE_NOT_SUBSCRIBED) {
viewBinding.statisticsHeading.setVisibility(View.GONE);
viewBinding.statisticsFragmentContainer.setVisibility(View.GONE);
viewBinding.supportHeadingLabel.setVisibility(View.GONE);
@ -268,6 +258,16 @@ public class FeedInfoFragment extends Fragment implements MaterialToolbar.OnMenu
getActivity().finish();
startActivity(mainActivityStarter.getIntent());
});
} else {
long feedId = getArguments().getLong(EXTRA_FEED_ID);
getParentFragmentManager().beginTransaction().replace(R.id.statisticsFragmentContainer,
FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment")
.commitAllowingStateLoss();
viewBinding.statisticsButton.setOnClickListener(view -> {
StatisticsFragment fragment = new StatisticsFragment();
((MainActivity) getActivity()).loadChildFragment(fragment, TransitionEffect.SLIDE);
});
}
refreshToolbarState();

View File

@ -288,9 +288,11 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
viewBinding.toolbar.getMenu().findItem(R.id.sort_items).setVisible(false);
viewBinding.toolbar.getMenu().findItem(R.id.refresh_item).setVisible(false);
viewBinding.toolbar.getMenu().findItem(R.id.rename_item).setVisible(false);
viewBinding.toolbar.getMenu().findItem(R.id.remove_feed).setVisible(false);
viewBinding.toolbar.getMenu().findItem(R.id.remove_archive_feed).setVisible(false);
viewBinding.toolbar.getMenu().findItem(R.id.remove_all_inbox_item).setVisible(false);
viewBinding.toolbar.getMenu().findItem(R.id.action_search).setVisible(false);
} else if (feed.getState() == Feed.STATE_ARCHIVED) {
viewBinding.toolbar.getMenu().findItem(R.id.sort_items).setVisible(false);
}
}
@ -330,12 +332,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
} else if (item.getItemId() == R.id.sort_items) {
SingleFeedSortDialog.newInstance(feed).show(getChildFragmentManager(), "SortDialog");
return true;
} else if (item.getItemId() == R.id.remove_feed) {
RemoveFeedDialog.show(getContext(), feed, () -> {
((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null);
// Make sure fragment is hidden before actually starting to delete
getActivity().getSupportFragmentManager().executePendingTransactions();
});
} else if (item.getItemId() == R.id.remove_archive_feed) {
new RemoveFeedDialogClose(Collections.singletonList(feed)).show(getParentFragmentManager(), null);
return true;
} else if (item.getItemId() == R.id.action_search) {
((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance(feed.getId(), feed.getTitle()));
@ -347,6 +345,24 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
return FeedMenuHandler.onMenuItemClicked(this, item.getItemId(), feed, showRemovedAllSnackbar);
}
public static class RemoveFeedDialogClose extends RemoveFeedDialog {
public RemoveFeedDialogClose(@NonNull List<Feed> feeds) {
super(feeds);
}
public RemoveFeedDialogClose() {
super();
}
@Override
protected void onRemoveButtonPressed() {
// Make sure fragment is hidden before actually starting to delete
((MainActivity) getActivity()).loadFragment(UserPreferences.getDefaultPage(), null);
getActivity().getSupportFragmentManager().executePendingTransactions();
super.onRemoveButtonPressed();
}
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
FeedItem selectedItem = adapter.getLongPressedItem();
@ -497,7 +513,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
} else {
viewBinding.header.txtvFailure.setVisibility(View.GONE);
}
if (!feed.getPreferences().getKeepUpdated() && feed.getState() == Feed.STATE_SUBSCRIBED) {
if ((!feed.getPreferences().getKeepUpdated() && feed.getState() != Feed.STATE_NOT_SUBSCRIBED)
|| feed.getState() == Feed.STATE_ARCHIVED) {
viewBinding.header.txtvUpdatesDisabled.setText(R.string.updates_disabled_label);
viewBinding.header.txtvUpdatesDisabled.setVisibility(View.VISIBLE);
} else {
@ -506,7 +523,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
viewBinding.header.txtvTitle.setText(feed.getTitle());
viewBinding.header.txtvAuthor.setText(feed.getAuthor());
viewBinding.header.descriptionContainer.setVisibility(View.GONE);
if (feed.getState() != Feed.STATE_SUBSCRIBED) {
if (feed.getState() == Feed.STATE_NOT_SUBSCRIBED) {
viewBinding.header.descriptionContainer.setVisibility(View.VISIBLE);
viewBinding.header.headerDescriptionLabel.setText(HtmlToPlainText.getPlainText(feed.getDescription()));
viewBinding.header.subscribeNagLabel.setVisibility(
@ -524,13 +541,16 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
} else {
viewBinding.header.txtvInformation.setVisibility(View.GONE);
}
boolean isSubscribed = feed.getState() == Feed.STATE_SUBSCRIBED;
viewBinding.header.butShowInfo.setVisibility(isSubscribed ? View.VISIBLE : View.GONE);
viewBinding.header.butFilter.setVisibility(isSubscribed ? View.VISIBLE : View.GONE);
viewBinding.header.butShowSettings.setVisibility(isSubscribed ? View.VISIBLE : View.GONE);
viewBinding.header.butSubscribe.setVisibility(isSubscribed ? View.GONE : View.VISIBLE);
boolean isNotSubscribed = feed.getState() == Feed.STATE_NOT_SUBSCRIBED;
boolean isArchived = feed.getState() == Feed.STATE_ARCHIVED;
boolean showSettingsButtons = !isNotSubscribed && !isArchived;
viewBinding.header.butShowInfo.setVisibility(!isNotSubscribed ? View.VISIBLE : View.GONE);
viewBinding.header.butFilter.setVisibility(showSettingsButtons ? View.VISIBLE : View.GONE);
viewBinding.header.butShowSettings.setVisibility(showSettingsButtons ? View.VISIBLE : View.GONE);
viewBinding.header.butSubscribe.setVisibility(isNotSubscribed ? View.VISIBLE : View.GONE);
viewBinding.header.butRestore.setVisibility(isArchived ? View.VISIBLE : View.GONE);
if (!isSubscribed && feed.getLastRefreshAttempt() < System.currentTimeMillis() - 1000L * 3600 * 24) {
if (isNotSubscribed && feed.getLastRefreshAttempt() < System.currentTimeMillis() - 1000L * 3600 * 24) {
FeedUpdateManager.getInstance().runOnce(getContext(), feed, true);
}
}
@ -551,6 +571,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
getActivity().finish();
startActivity(mainActivityStarter.getIntent());
});
viewBinding.header.butRestore.setOnClickListener(v -> {
if (feed == null) {
return;
}
DBWriter.setFeedState(getContext(), feed, Feed.STATE_SUBSCRIBED);
});
viewBinding.header.butShowSettings.setOnClickListener(v -> {
if (feed == null) {
return;
@ -749,7 +775,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
if (!inActionMode() && feed.getState() == Feed.STATE_SUBSCRIBED) {
if (!inActionMode() && feed.getState() != Feed.STATE_NOT_SUBSCRIBED) {
menu.findItem(R.id.multi_select).setVisible(true);
}
MenuItemUtils.setOnClickListeners(menu, FeedItemlistFragment.this::onContextItemSelected);

View File

@ -1,84 +1,167 @@
package de.danoeh.antennapod.ui.screen.feed;
import android.app.ProgressDialog;
import android.animation.ValueAnimator;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.common.ConfirmationDialog;
import de.danoeh.antennapod.databinding.RemoveFeedDialogBinding;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.storage.database.DBWriter;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class RemoveFeedDialog {
import java.util.ArrayList;
import java.util.List;
public class RemoveFeedDialog extends BottomSheetDialogFragment {
private static final String TAG = "RemoveFeedDialog";
private static final String ARGUMENT_FEEDS = "feeds";
public static void show(Context context, Feed feed, @Nullable Runnable callback) {
List<Feed> feeds = Collections.singletonList(feed);
String message = getMessageId(context, feeds);
showDialog(context, feeds, message, callback);
protected List<Feed> feeds;
private RemoveFeedDialogBinding binding;
private Disposable disposable;
public RemoveFeedDialog() {
// Required empty public constructor
}
public static void show(Context context, List<Feed> feeds) {
String message = getMessageId(context, feeds);
showDialog(context, feeds, message, null);
public RemoveFeedDialog(List<Feed> feeds) {
Bundle args = new Bundle();
args.putSerializable(ARGUMENT_FEEDS, new ArrayList<>(feeds));
setArguments(args);
}
private static void showDialog(Context context, List<Feed> feeds, String message, @Nullable Runnable callback) {
ConfirmationDialog dialog = new ConfirmationDialog(context, R.string.remove_feed_label, message) {
@Override
public void onConfirmButtonPressed(DialogInterface clickedDialog) {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = RemoveFeedDialogBinding.inflate(inflater, container, false);
if (getArguments() == null || !getArguments().containsKey(ARGUMENT_FEEDS)) {
Log.e(TAG, "No feeds specified");
dismiss();
return binding.getRoot();
}
feeds = (List<Feed>) getArguments().getSerializable(ARGUMENT_FEEDS);
if (feeds.size() == 1) {
binding.selectionText.setText(feeds.get(0).getTitle());
} else {
binding.selectionText.setText(getResources()
.getQuantityString(R.plurals.num_subscriptions, feeds.size(), feeds.size()));
}
boolean allArchived = true;
for (Feed feed : feeds) {
if (feed.getState() != Feed.STATE_ARCHIVED) {
allArchived = false;
break;
}
}
if (allArchived) {
binding.archiveButton.setVisibility(View.GONE);
binding.explanationArchiveText.setVisibility(View.GONE);
}
binding.explanationArchiveText.setText(Html.fromHtml(getString(R.string.feed_delete_explanation_archive)));
binding.explanationDeleteText.setText(Html.fromHtml(getString(R.string.feed_delete_explanation_delete)));
binding.cancelButton.setOnClickListener(v -> dismiss());
binding.removeButton.setOnClickListener(v -> showRemoveConfirm());
binding.removeConfirmButton.setOnClickListener(v -> onRemoveButtonPressed());
binding.archiveButton.setOnClickListener(v -> onArchiveButtonPressed());
return binding.getRoot();
}
if (callback != null) {
callback.run();
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (disposable != null) {
disposable.dispose();
disposable = null;
}
binding = null;
}
clickedDialog.dismiss();
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setOnShowListener(dialogInterface -> {
BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface;
setupFullHeight(bottomSheetDialog);
});
return dialog;
}
ProgressDialog progressDialog = new ProgressDialog(context);
progressDialog.setMessage(context.getString(R.string.feed_remover_msg));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
progressDialog.show();
private void setupFullHeight(BottomSheetDialog bottomSheetDialog) {
FrameLayout bottomSheet = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
if (bottomSheet != null) {
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(bottomSheet);
ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams();
bottomSheet.setLayoutParams(layoutParams);
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
Completable.fromAction(() -> {
protected void onRemoveButtonPressed() {
Context context = getContext();
if (context == null) {
return;
}
binding.progressBar.setVisibility(View.VISIBLE);
binding.removeConfirmButton.setVisibility(View.GONE);
binding.archiveButton.setVisibility(View.GONE);
binding.cancelButton.setVisibility(View.GONE);
disposable = Completable.fromAction(
() -> {
for (Feed feed : feeds) {
DBWriter.deleteFeed(context, feed.getId()).get();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
Log.d(TAG, "Feed(s) deleted");
progressDialog.dismiss();
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
progressDialog.dismiss();
});
}
};
dialog.createNewDialog().show();
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
Log.d(TAG, "Feed(s) deleted");
dismiss();
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));
dismiss();
});
}
private static String getMessageId(Context context, List<Feed> feeds) {
if (feeds.size() == 1) {
if (feeds.get(0).isLocalFeed()) {
return context.getString(R.string.feed_delete_confirmation_local_msg, feeds.get(0).getTitle());
} else {
return context.getString(R.string.feed_delete_confirmation_msg, feeds.get(0).getTitle());
}
} else {
return context.getString(R.string.feed_delete_confirmation_msg_batch);
private void onArchiveButtonPressed() {
dismiss();
for (Feed feed : feeds) {
DBWriter.setFeedState(getContext(), feed, Feed.STATE_ARCHIVED);
}
}
private void showRemoveConfirm() {
binding.removeButton.setVisibility(View.GONE);
binding.removeConfirmButton.setVisibility(View.VISIBLE);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) binding.removeConfirmButton.getLayoutParams();
ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 2.0f);
animator.addUpdateListener(animation -> {
params.weight = (float) animation.getAnimatedValue();
binding.removeConfirmButton.setLayoutParams(params);
});
animator.setDuration(400);
animator.setInterpolator(new OvershootInterpolator(3.0f));
animator.start();
}
}

View File

@ -14,12 +14,14 @@ 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.Feed;
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.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.SimpleChipAdapter;
import de.danoeh.antennapod.ui.view.ItemOffsetDecoration;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@ -76,6 +78,8 @@ public class TagSettingsDialog extends DialogFragment {
};
viewBinding.tagsRecycler.setAdapter(adapter);
viewBinding.rootFolderCheckbox.setChecked(commonTags.contains(FeedPreferences.TAG_ROOT));
viewBinding.rootFolderCheckbox.setVisibility(UserPreferences.isBottomNavigationEnabled()
? View.GONE : View.VISIBLE);
viewBinding.newTagTextInput.setEndIconOnClickListener(v ->
addTag(viewBinding.newTagEditText.getText().toString().trim()));
@ -109,10 +113,12 @@ public class TagSettingsDialog extends DialogFragment {
private void loadTags() {
Observable.fromCallable(
() -> {
NavDrawerData data = DBReader.getNavDrawerData(null, FeedOrder.ALPHABETICAL, FeedCounter.SHOW_NONE);
NavDrawerData data = DBReader.getNavDrawerData(null, FeedOrder.ALPHABETICAL, FeedCounter.SHOW_NONE,
Feed.STATE_SUBSCRIBED);
List<String> folders = new ArrayList<>();
for (NavDrawerData.TagItem item : data.tags) {
if (!FeedPreferences.TAG_ROOT.equals(item.getTitle())) {
if (!FeedPreferences.TAG_ROOT.equals(item.getTitle())
&& !FeedPreferences.TAG_UNTAGGED.equals(item.getTitle())) {
folders.add(item.getTitle());
}
}
@ -131,7 +137,7 @@ public class TagSettingsDialog extends DialogFragment {
}
private void addTag(String name) {
if (TextUtils.isEmpty(name) || displayedTags.contains(name)) {
if (TextUtils.isEmpty(name) || displayedTags.contains(name) || FeedPreferences.TAG_UNTAGGED.equals(name)) {
return;
}
displayedTags.add(name);

View File

@ -103,7 +103,10 @@ public class SubscriptionsSection extends HomeSection {
Collections.sort(statisticsData, (item1, item2) ->
Long.compare(item2.timePlayed, item1.timePlayed));
List<Feed> feeds = new ArrayList<>();
for (int i = 0; i < statisticsData.size() && i < NUM_FEEDS; i++) {
for (int i = 0; i < statisticsData.size() && feeds.size() < NUM_FEEDS; i++) {
if (statisticsData.get(i).feed.getState() != Feed.STATE_SUBSCRIBED) {
continue;
}
feeds.add(statisticsData.get(i).feed);
}
listAdapter.setDummyViews(0);

View File

@ -235,10 +235,10 @@ public class OnlineFeedViewActivity extends AppCompatActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(subscribedFeed -> {
if (subscribedFeed.getState() == Feed.STATE_SUBSCRIBED) {
openFeed(subscribedFeed.getId());
} else {
if (subscribedFeed.getState() == Feed.STATE_NOT_SUBSCRIBED) {
showFeedFragment(subscribedFeed.getId());
} else {
openFeed(subscribedFeed.getId());
}
}, error -> Log.e(TAG, Log.getStackTraceString(error)), () -> startFeedDownload(url));
return null;

View File

@ -514,10 +514,10 @@ public class AudioPlayerFragment extends Fragment implements
if (feed == null) {
return;
}
if (feed.getState() == Feed.STATE_SUBSCRIBED) {
new MainActivityStarter(getContext()).withOpenFeed(feed.getId()).withClearTop().start();
} else {
if (feed.getState() == Feed.STATE_NOT_SUBSCRIBED) {
startActivity(new OnlineFeedviewActivityStarter(getContext(), feed.getDownloadUrl()).getIntent());
} else {
new MainActivityStarter(getContext()).withOpenFeed(feed.getId()).withClearTop().start();
}
}

View File

@ -167,10 +167,10 @@ public class CoverFragment extends Fragment {
if (feed == null) {
return;
}
if (feed.getState() == Feed.STATE_SUBSCRIBED) {
new MainActivityStarter(getContext()).withOpenFeed(feed.getId()).withClearTop().start();
} else {
if (feed.getState() == Feed.STATE_NOT_SUBSCRIBED) {
startActivity(new OnlineFeedviewActivityStarter(getContext(), feed.getDownloadUrl()).getIntent());
} else {
new MainActivityStarter(getContext()).withOpenFeed(feed.getId()).withClearTop().start();
}
}

View File

@ -58,8 +58,9 @@ public abstract class FeedMenuHandler {
.show(fragment.getChildFragmentManager(), TagSettingsDialog.TAG);
} else if (menuItemId == R.id.rename_item) {
new RenameFeedDialog(fragment.getActivity(), selectedFeed).show();
} else if (menuItemId == R.id.remove_feed) {
RemoveFeedDialog.show(context, selectedFeed, null);
} else if (menuItemId == R.id.remove_archive_feed) {
new RemoveFeedDialog(Collections.singletonList(selectedFeed))
.show(fragment.getChildFragmentManager(), null);
} else if (menuItemId == R.id.share_feed) {
ShareUtils.shareFeedLink(context, selectedFeed);
} else {

View File

@ -34,8 +34,8 @@ public class FeedMultiSelectActionHandler {
}
public void handleAction(int id) {
if (id == R.id.remove_feed) {
RemoveFeedDialog.show(activity, selectedItems);
if (id == R.id.remove_archive_feed) {
new RemoveFeedDialog(selectedItems).show(activity.getSupportFragmentManager(), null);
} else if (id == R.id.notify_new_episodes) {
notifyNewEpisodesPrefHandler();
} else if (id == R.id.keep_updated) {

View File

@ -48,6 +48,7 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
@ -62,7 +63,7 @@ public class SubscriptionFragment extends Fragment
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";
private static final String ARGUMENT_STATE = "state";
private static final int MIN_NUM_COLUMNS = 1;
private static final int[] COLUMN_CHECKBOX_IDS = {
@ -92,11 +93,12 @@ public class SubscriptionFragment extends Fragment
private FloatingSelectMenu floatingSelectMenu;
private RecyclerView.ItemDecoration itemDecoration;
private List<Feed> feeds;
private int stateToShow = Feed.STATE_SUBSCRIBED;
public static SubscriptionFragment newInstance(String folderTitle) {
public static SubscriptionFragment newInstance(int state) {
SubscriptionFragment fragment = new SubscriptionFragment();
Bundle args = new Bundle();
args.putString(ARGUMENT_FOLDER, folderTitle);
args.putInt(ARGUMENT_STATE, state);
fragment.setArguments(args);
return fragment;
}
@ -105,6 +107,9 @@ public class SubscriptionFragment extends Fragment
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
prefs = requireActivity().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
if (getArguments() != null) {
stateToShow = getArguments().getInt(ARGUMENT_STATE, Feed.STATE_SUBSCRIBED);
}
}
@Override
@ -172,6 +177,14 @@ public class SubscriptionFragment extends Fragment
floatingSelectMenu = root.findViewById(R.id.floatingSelectMenu);
floatingSelectMenu.inflate(R.menu.nav_feed_action_speeddial);
if (stateToShow == Feed.STATE_ARCHIVED) {
toolbar.setTitle(R.string.archive_feed_label_noun);
floatingSelectMenu.getMenu().removeItem(R.id.keep_updated);
floatingSelectMenu.getMenu().removeItem(R.id.notify_new_episodes);
floatingSelectMenu.getMenu().removeItem(R.id.autodownload);
floatingSelectMenu.getMenu().removeItem(R.id.autoDeleteDownload);
floatingSelectMenu.getMenu().removeItem(R.id.playback_speed);
}
floatingSelectMenu.setOnMenuItemClickListener(menuItem -> {
new FeedMultiSelectActionHandler(getActivity(), subscriptionAdapter.getSelectedItems())
.handleAction(menuItem.getItemId());
@ -195,7 +208,11 @@ public class SubscriptionFragment extends Fragment
loadSubscriptionsAndTags();
}
};
tagAdapter.setSelectedTag(prefs.getString(PREF_LAST_TAG, FeedPreferences.TAG_ROOT));
if (stateToShow == Feed.STATE_SUBSCRIBED) {
tagAdapter.setSelectedTag(prefs.getString(PREF_LAST_TAG, FeedPreferences.TAG_ROOT));
} else {
tagAdapter.setSelectedTag(FeedPreferences.TAG_ROOT);
}
tagsRecycler.setAdapter(tagAdapter);
return root;
}
@ -250,7 +267,11 @@ public class SubscriptionFragment extends Fragment
setColumnNumber(5);
return true;
} else if (itemId == R.id.action_search) {
((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance());
if (stateToShow == Feed.STATE_ARCHIVED) {
((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstanceArchive());
} else {
((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance());
}
return true;
} else if (itemId == R.id.action_statistics) {
((MainActivity) getActivity()).loadChildFragment(new StatisticsFragment());
@ -259,6 +280,10 @@ public class SubscriptionFragment extends Fragment
item.setChecked(!item.isChecked());
UserPreferences.setShouldShowSubscriptionTitle(item.isChecked());
subscriptionAdapter.notifyDataSetChanged();
} else if (itemId == R.id.show_archive) {
Fragment fragment = SubscriptionFragment.newInstance(Feed.STATE_ARCHIVED);
((MainActivity) getActivity()).loadChildFragment(fragment);
return true;
}
return false;
}
@ -303,7 +328,9 @@ public class SubscriptionFragment extends Fragment
public void onPause() {
super.onPause();
scrollPosition = getScrollPosition();
prefs.edit().putString(PREF_LAST_TAG, tagAdapter.getSelectedTag()).apply();
if (stateToShow == Feed.STATE_SUBSCRIBED) {
prefs.edit().putString(PREF_LAST_TAG, tagAdapter.getSelectedTag()).apply();
}
}
@Override
@ -327,16 +354,19 @@ public class SubscriptionFragment extends Fragment
() -> {
NavDrawerData navDrawerData = DBReader.getNavDrawerData(
UserPreferences.getSubscriptionsFilter(),
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting());
List<NavDrawerData.TagItem> tags = DBReader.getAllTags();
UserPreferences.getFeedOrder(), UserPreferences.getFeedCounterSetting(),
stateToShow);
List<NavDrawerData.TagItem> tags = DBReader.getAllTags(stateToShow);
return new Pair<>(navDrawerData, tags);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
result -> {
List<Feed> openedFolderFeeds = result.first.feeds;
if (!FeedPreferences.TAG_ROOT.equals(tagAdapter.getSelectedTag())) {
List<Feed> openedFolderFeeds = Collections.emptyList();
if (FeedPreferences.TAG_ROOT.equals(tagAdapter.getSelectedTag())) {
openedFolderFeeds = result.first.feeds;
} else {
for (NavDrawerData.TagItem tag : result.first.tags) { // Filtered list
if (tag.getTitle().equals(tagAdapter.getSelectedTag())) {
openedFolderFeeds = tag.getFeeds();
@ -359,7 +389,15 @@ public class SubscriptionFragment extends Fragment
emptyView.updateVisibility();
if (tagAdapter != null) {
tagAdapter.setTags(result.second);
tagsRecycler.setVisibility(result.second.size() > 1 ? View.VISIBLE : View.GONE);
boolean shouldShowTags = false;
for (NavDrawerData.TagItem tag : result.second) {
if (!FeedPreferences.TAG_ROOT.equals(tag.getTitle())
&& !FeedPreferences.TAG_UNTAGGED.equals(tag.getTitle())) {
shouldShowTags = true;
break;
}
}
tagsRecycler.setVisibility(shouldShowTags ? View.VISIBLE : View.GONE);
}
}, error -> {
Log.e(TAG, Log.getStackTraceString(error));

View File

@ -58,6 +58,9 @@ public class SubscriptionTagAdapter extends RecyclerView.Adapter<SubscriptionTag
NavDrawerData.TagItem tag = tags.get(position);
if (FeedPreferences.TAG_ROOT.equals(tag.getTitle())) {
holder.chip.setText(R.string.tag_all);
} else if (FeedPreferences
.TAG_UNTAGGED.equals(tag.getTitle())) {
holder.chip.setText(R.string.tag_untagged);
} else {
String title = tag.getTitle();
if (title.length() > 20) {
@ -91,7 +94,9 @@ public class SubscriptionTagAdapter extends RecyclerView.Adapter<SubscriptionTag
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (longPressedItem == null || FeedPreferences.TAG_ROOT.equals(longPressedItem.getTitle())) {
if (longPressedItem == null
|| FeedPreferences.TAG_ROOT.equals(longPressedItem.getTitle())
|| FeedPreferences.TAG_UNTAGGED.equals(longPressedItem.getTitle())) {
return;
}
MenuInflater inflater = activityRef.get().getMenuInflater();

View File

@ -37,6 +37,15 @@
android:layout_marginVertical="4dp"
tools:visibility="visible" />
<Button
android:id="@+id/butRestore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/restore_archive_label"
android:visibility="gone"
android:layout_marginVertical="4dp"
tools:visibility="visible" />
<ImageButton
android:id="@+id/butShowInfo"
android:layout_width="48dp"

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingTop="24dp"
android:paddingRight="24dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/remove_archive_feed_label"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/selectionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/explanationDeleteText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/feed_delete_explanation_delete"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:layout_marginBottom="8dp" />
<TextView
android:id="@+id/explanationArchiveText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/feed_delete_explanation_archive"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:layout_marginBottom="16dp" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:indeterminate="true"
style="?android:attr/progressBarStyle" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancelButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:paddingHorizontal="8dp"
android:text="@string/cancel_label"
style="@style/Widget.Material3.Button.OutlinedButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/removeButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:paddingHorizontal="8dp"
android:text="@string/remove_feed_label"
style="@style/Widget.Material3.Button.OutlinedButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/removeConfirmButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:paddingHorizontal="8dp"
android:text="@string/remove_feed_label"
android:backgroundTint="?attr/colorError"
android:textColor="?attr/colorOnError"
android:visibility="gone"
style="@style/Widget.Material3.Button.OutlinedButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/archiveButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingHorizontal="8dp"
android:text="@string/archive_feed_label_verb"
style="@style/Widget.Material3.Button" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -56,10 +56,10 @@
custom:showAsAction="never" />
<item
android:id="@+id/remove_feed"
android:id="@+id/remove_archive_feed"
android:icon="@drawable/ic_delete"
android:menuCategory="container"
android:title="@string/remove_feed_label"
android:title="@string/remove_archive_feed_label"
android:visible="true"
custom:showAsAction="collapseActionView">
</item>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/remove_feed"
android:id="@+id/remove_archive_feed"
android:menuCategory="container"
android:title="@string/remove_feed_label"
android:title="@string/remove_archive_feed_label"
android:icon="@drawable/ic_delete"/>
<item
android:id="@+id/keep_updated"

View File

@ -17,9 +17,9 @@
android:title="@string/rename_feed_label" />
<item
android:id="@+id/remove_feed"
android:id="@+id/remove_archive_feed"
android:menuCategory="container"
android:title="@string/remove_feed_label" />
android:title="@string/remove_archive_feed_label" />
<item
android:id="@+id/share_feed"

View File

@ -59,4 +59,9 @@
android:title="@string/pref_show_subscription_title"
android:checkable="true"
custom:showAsAction="never" />
<item
android:id="@+id/show_archive"
android:title="@string/archive_feed_label_noun"
custom:showAsAction="never" />
</menu>

View File

@ -19,6 +19,7 @@ public class Feed {
public static final int FEEDFILETYPE_FEED = 0;
public static final int STATE_SUBSCRIBED = 0;
public static final int STATE_NOT_SUBSCRIBED = 1;
public static final int STATE_ARCHIVED = 2;
public static final String TYPE_RSS2 = "rss";
public static final String TYPE_ATOM1 = "atom";
public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:";

View File

@ -120,7 +120,7 @@ public class FeedItemFilter implements Serializable {
&& item.getMedia().getLastPlayedTimeHistory().getTime() == 0) {
return false;
} else if (!includeNotSubscribed && item.getFeed() != null
&& item.getFeed().getState() != Feed.STATE_SUBSCRIBED) {
&& item.getFeed().getState() == Feed.STATE_NOT_SUBSCRIBED) {
return false;
}
return true;

View File

@ -14,6 +14,7 @@ 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_UNTAGGED = "#untagged";
public static final String TAG_SEPARATOR = "\u001e";
public enum AutoDeleteAction {

View File

@ -59,7 +59,7 @@ public class ItunesTopListLoader {
Set<String> subscribedPodcastsSet = new HashSet<>();
for (Feed subscribedFeed : subscribedFeeds) {
if (subscribedFeed.getTitle() != null && subscribedFeed.getAuthor() != null
&& subscribedFeed.getState() == Feed.STATE_SUBSCRIBED) {
&& subscribedFeed.getState() != Feed.STATE_NOT_SUBSCRIBED) {
subscribedPodcastsSet.add(subscribedFeed.getTitle().trim());
}
}

View File

@ -114,7 +114,7 @@ public class MediaDownloadedHandler implements Runnable {
FeedMedia.FEEDFILETYPE_FEEDMEDIA, false, DownloadError.ERROR_DB_ACCESS_ERROR, e.getMessage());
}
if (item != null && item.getFeed().getState() == Feed.STATE_SUBSCRIBED) {
if (item != null && item.getFeed().getState() != Feed.STATE_NOT_SUBSCRIBED) {
SynchronizationQueue.getInstance().enqueueEpisodeAction(
new EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD)
.currentTimestamp()

View File

@ -342,7 +342,8 @@ public class DbReaderTest {
final int numItems = 10;
DbTestUtils.saveFeedlist(numFeeds, numItems, true);
NavDrawerData navDrawerData = DBReader.getNavDrawerData(
UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW);
UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW,
Feed.STATE_SUBSCRIBED);
assertEquals(numFeeds, navDrawerData.feeds.size());
assertEquals(0, navDrawerData.numNewItems);
assertEquals(0, navDrawerData.queueSize);
@ -372,7 +373,8 @@ public class DbReaderTest {
adapter.close();
NavDrawerData navDrawerData = DBReader.getNavDrawerData(
UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW);
UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW,
Feed.STATE_SUBSCRIBED);
assertEquals(numFeeds, navDrawerData.feeds.size());
assertEquals(numNew, navDrawerData.numNewItems);
assertEquals(numQueue, navDrawerData.queueSize);

View File

@ -113,7 +113,7 @@ public class SynchronizationQueueImpl extends SynchronizationQueue {
return;
}
if (media.getItem() == null || media.getItem().getFeed().isLocalFeed()
|| media.getItem().getFeed().getState() != Feed.STATE_SUBSCRIBED) {
|| media.getItem().getFeed().getState() == Feed.STATE_NOT_SUBSCRIBED) {
return;
}
if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) {

View File

@ -1885,7 +1885,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
return;
}
List<FeedItem> results = DBReader.searchFeedItems(0, query);
List<FeedItem> results = DBReader.searchFeedItems(0, query, Feed.STATE_SUBSCRIBED);
if (results.size() > 0 && results.get(0).getMedia() != null) {
FeedMedia media = results.get(0).getMedia();
startPlaying(media, false);

View File

@ -666,17 +666,22 @@ public final class DBReader {
*/
@NonNull
public static NavDrawerData getNavDrawerData(@Nullable SubscriptionsFilter subscriptionsFilter,
FeedOrder feedOrder, FeedCounter feedCounter) {
FeedOrder feedOrder, FeedCounter feedCounter, int feedState) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
final Map<Long, Integer> feedCounters = adapter.getFeedCounters(feedCounter);
List<Feed> feeds = getFeedList();
List<Feed> allFeeds = getFeedList();
List<Feed> typeFilteredFeeds = new ArrayList<>();
for (Feed feed : allFeeds) {
if (feed.getState() == feedState) {
typeFilteredFeeds.add(feed);
}
}
if (subscriptionsFilter == null) {
subscriptionsFilter = new SubscriptionsFilter("");
}
feeds = SubscriptionsFilterExecutor.filter(feeds, feedCounters, subscriptionsFilter);
List<Feed> feeds = SubscriptionsFilterExecutor.filter(typeFilteredFeeds, feedCounters, subscriptionsFilter);
Comparator<Feed> comparator;
switch (feedOrder) {
@ -737,8 +742,13 @@ public final class DBReader {
final int numNewItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW));
final int numDownloadedItems = getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED));
NavDrawerData.TagItem untaggedTag = new NavDrawerData.TagItem(FeedPreferences.TAG_UNTAGGED);
Map<String, NavDrawerData.TagItem> tags = new HashMap<>();
for (Feed feed : feeds) {
if (feed.getPreferences().getTags().isEmpty() || (feed.getPreferences().getTags().size()) == 1
&& feed.getPreferences().getTags().contains(FeedPreferences.TAG_ROOT)) {
untaggedTag.addFeed(feed, 0);
}
for (String tag : feed.getPreferences().getTags()) {
if (!tags.containsKey(tag)) {
tags.put(tag, new NavDrawerData.TagItem(tag));
@ -750,16 +760,31 @@ public final class DBReader {
List<NavDrawerData.TagItem> tagsSorted = new ArrayList<>(tags.values());
Collections.sort(tagsSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
if (!untaggedTag.getFeeds().isEmpty()) {
tagsSorted.add(0, untaggedTag);
}
NavDrawerData result = new NavDrawerData(feeds, tagsSorted,
queueSize, numNewItems, numDownloadedItems, feedCounters);
adapter.close();
return result;
}
public static List<NavDrawerData.TagItem> getAllTags() {
public static List<NavDrawerData.TagItem> getAllTags(int feedState) {
Map<String, NavDrawerData.TagItem> tags = new HashMap<>();
List<Feed> feeds = getFeedList();
List<Feed> allFeeds = getFeedList();
List<Feed> feeds = new ArrayList<>();
for (Feed feed : allFeeds) {
if (feed.getState() == feedState) {
feeds.add(feed);
}
}
NavDrawerData.TagItem untaggedTag = new NavDrawerData.TagItem(FeedPreferences.TAG_UNTAGGED);
for (Feed feed : feeds) {
if (feed.getPreferences().getTags().isEmpty() || (feed.getPreferences().getTags().size()) == 1
&& feed.getPreferences().getTags().contains(FeedPreferences.TAG_ROOT)) {
untaggedTag.addFeed(feed, 0);
}
for (String tag : feed.getPreferences().getTags()) {
if (FeedPreferences.TAG_ROOT.equals(tag)) {
continue;
@ -772,6 +797,9 @@ public final class DBReader {
}
List<NavDrawerData.TagItem> tagsSorted = new ArrayList<>(tags.values());
Collections.sort(tagsSorted, (o1, o2) -> o1.getTitle().compareToIgnoreCase(o2.getTitle()));
if (!untaggedTag.getFeeds().isEmpty()) {
tagsSorted.add(0, untaggedTag);
}
// Root tag here means "all feeds", this is different from the nav drawer.
NavDrawerData.TagItem rootTag = new NavDrawerData.TagItem(FeedPreferences.TAG_ROOT);
for (Feed feed : feeds) {
@ -781,10 +809,10 @@ public final class DBReader {
return tagsSorted;
}
public static List<FeedItem> searchFeedItems(final long feedId, final String query) {
public static List<FeedItem> searchFeedItems(final long feedId, final String query, int state) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try (FeedItemCursor searchResult = new FeedItemCursor(adapter.searchItems(feedId, query))) {
try (FeedItemCursor searchResult = new FeedItemCursor(adapter.searchItems(feedId, query, state))) {
List<FeedItem> items = extractItemlistFromCursor(searchResult);
loadAdditionalFeedItemListData(items);
return items;
@ -793,10 +821,10 @@ public final class DBReader {
}
}
public static List<Feed> searchFeeds(final String query) {
public static List<Feed> searchFeeds(final String query, int state) {
PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
try (FeedCursor cursor = new FeedCursor(adapter.searchFeeds(query))) {
try (FeedCursor cursor = new FeedCursor(adapter.searchFeeds(query, state))) {
List<Feed> items = new ArrayList<>();
while (cursor.moveToNext()) {
items.add(cursor.getFeed());

View File

@ -151,7 +151,7 @@ public class DBWriter {
// Do full update of this feed to get rid of the item
FeedUpdateManager.getInstance().runOnce(context, media.getItem().getFeed());
} else {
if (media.getItem().getFeed().getState() == Feed.STATE_SUBSCRIBED) {
if (media.getItem().getFeed().getState() != Feed.STATE_NOT_SUBSCRIBED) {
SynchronizationQueue.getInstance().enqueueEpisodeAction(
new EpisodeAction.Builder(media.getItem(), EpisodeAction.DELETE)
.currentTimestamp()
@ -184,7 +184,7 @@ public class DBWriter {
adapter.removeFeed(feed);
adapter.close();
if (!feed.isLocalFeed() && feed.getState() == Feed.STATE_SUBSCRIBED) {
if (!feed.isLocalFeed() && feed.getState() != Feed.STATE_NOT_SUBSCRIBED) {
SynchronizationQueue.getInstance().enqueueFeedRemoved(feed.getDownloadUrl());
}
EventBus.getDefault().post(new FeedListUpdateEvent(feed));
@ -726,7 +726,7 @@ public class DBWriter {
adapter.close();
for (Feed feed : feeds) {
if (!feed.isLocalFeed() && feed.getState() == Feed.STATE_SUBSCRIBED) {
if (!feed.isLocalFeed() && feed.getState() != Feed.STATE_NOT_SUBSCRIBED) {
SynchronizationQueue.getInstance().enqueueFeedAdded(feed.getDownloadUrl());
}
}

View File

@ -128,7 +128,7 @@ public abstract class FeedDatabaseWriter {
oldItem.setItemIdentifier(item.getItemIdentifier());
if (oldItem.isPlayed() && oldItem.getMedia() != null
&& savedFeed.getState() == Feed.STATE_SUBSCRIBED) {
&& savedFeed.getState() != Feed.STATE_NOT_SUBSCRIBED) {
EpisodeAction action = new EpisodeAction.Builder(oldItem, EpisodeAction.PLAY)
.currentTimestamp()
.started(oldItem.getMedia().getDuration() / 1000)

View File

@ -1263,7 +1263,7 @@ public class PodDBAdapter {
+ JOIN_FEED_ITEM_AND_MEDIA
+ " INNER JOIN " + TABLE_NAME_FEEDS
+ " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID
+ " WHERE " + TABLE_NAME_FEEDS + "." + KEY_STATE + "=" + Feed.STATE_SUBSCRIBED
+ " WHERE " + TABLE_NAME_FEEDS + "." + KEY_STATE + "!=" + Feed.STATE_NOT_SUBSCRIBED
+ " GROUP BY " + TABLE_NAME_FEEDS + "." + KEY_ID;
return db.rawQuery(query, null);
}
@ -1404,8 +1404,8 @@ public class PodDBAdapter {
*
* @return A cursor with all search results in SEL_FI_EXTRA selection.
*/
public Cursor searchItems(long feedID, String searchQuery) {
String[] queryWords = prepareSearchQuery(searchQuery);
public Cursor searchItems(long feedID, String searchQuery, int state) {
final String[] queryWords = prepareSearchQuery(searchQuery);
String queryFeedId;
if (feedID != 0) {
@ -1416,8 +1416,11 @@ public class PodDBAdapter {
queryFeedId = "1 = 1";
}
String queryStart = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION
+ " WHERE " + queryFeedId + " AND " + SELECT_WHERE_FEED_IS_SUBSCRIBED + " AND (";
String queryStart = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION + " WHERE " + queryFeedId;
if (state == Feed.STATE_SUBSCRIBED && feedID == 0) {
queryStart += " AND " + SELECT_WHERE_FEED_IS_SUBSCRIBED;
}
queryStart += " AND (";
StringBuilder sb = new StringBuilder(queryStart);
for (int i = 0; i < queryWords.length; i++) {
@ -1443,11 +1446,10 @@ public class PodDBAdapter {
*
* @return A cursor with all search results in SEL_FI_EXTRA selection.
*/
public Cursor searchFeeds(String searchQuery) {
String[] queryWords = prepareSearchQuery(searchQuery);
public Cursor searchFeeds(String searchQuery, int state) {
final String[] queryWords = prepareSearchQuery(searchQuery);
String queryStart = "SELECT " + KEYS_FEED + " FROM " + TABLE_NAME_FEEDS
+ " WHERE " + KEY_STATE + " = " + Feed.STATE_SUBSCRIBED;
+ " WHERE " + KEY_STATE + " = " + state;
StringBuilder sb = new StringBuilder(queryStart);
for (int i = 0; i < queryWords.length; i++) {

View File

@ -37,7 +37,7 @@ public abstract class SubscriptionsFilterExecutor {
continue;
}
if (filter.hideNonSubscribedFeeds && item.getState() != Feed.STATE_SUBSCRIBED) {
if (filter.hideNonSubscribedFeeds && item.getState() == Feed.STATE_NOT_SUBSCRIBED) {
continue;
}

View File

@ -175,6 +175,10 @@
<item quantity="one">%d episode</item>
<item quantity="other">%d episodes</item>
</plurals>
<plurals name="num_subscriptions">
<item quantity="one">%d subscription</item>
<item quantity="other">%d subscriptions</item>
</plurals>
<string name="episode_notification">Episode notifications</string>
<string name="episode_notification_summary">Show a notification when a new episode is released</string>
<plurals name="new_episode_notification_message">
@ -196,14 +200,16 @@
<string name="show_info_label">Show information</string>
<string name="show_feed_settings_label">Show podcast settings</string>
<string name="feed_settings_label">Podcast settings</string>
<string name="rename_feed_label">Rename podcast</string>
<string name="remove_feed_label">Remove podcast</string>
<string name="rename_feed_label">Rename</string>
<string name="archive_feed_label_verb">Archive</string>
<string name="archive_feed_label_noun">Archive</string>
<string name="restore_archive_label">Restore</string>
<string name="remove_archive_feed_label">Delete / archive</string>
<string name="remove_feed_label">Delete</string>
<string name="share_label">Share</string>
<string name="share_file_label">Share file</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\", ALL its episodes (including downloaded episodes), playback history, and its statistics.</string>
<string name="feed_delete_confirmation_msg_batch">Please confirm that you want to remove the selected podcasts, ALL their episodes (including downloaded episodes), playback history, and its statistics.</string>
<string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\", its playback history, and its statistics. The files in the local source folder will not be deleted.</string>
<string name="feed_remover_msg">Removing podcast</string>
<string name="feed_delete_explanation_delete"><![CDATA[<b>Deleting</b> will remove ALL its episodes including downloads, playback history, and statistics.]]></string>
<string name="feed_delete_explanation_archive"><![CDATA[<b>Archiving</b> will hide it from the subscription list and avoid it getting updates. Downloads, statistics and playback state are kept.]]></string>
<string name="load_complete_feed">Refresh complete podcast</string>
<string name="multi_select">Multi select</string>
<string name="select_all_above">Select all above</string>
@ -757,6 +763,7 @@
<string name="feed_tags_summary">Change the tags of this podcast to help organize your subscriptions</string>
<string name="feed_folders_include_root">Show above tags (side navigation only)</string>
<string name="tag_all">All</string>
<string name="tag_untagged">Untagged</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>