mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-08-21 05:45:13 +00:00
New preference to load next feed on mark read (#15)
New preference to load next feed on mark read. When the option is enabled, a session data source is created to calculate the next feed or folder to load.
This commit is contained in:
parent
b88906404c
commit
48dad79bab
23 changed files with 557 additions and 78 deletions
|
@ -44,6 +44,8 @@ dependencies {
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.0'
|
implementation 'androidx.core:core-splashscreen:1.0.0'
|
||||||
implementation "com.google.dagger:hilt-android:2.43.2"
|
implementation "com.google.dagger:hilt-android:2.43.2"
|
||||||
kapt "com.google.dagger:hilt-compiler:2.43.2"
|
kapt "com.google.dagger:hilt-compiler:2.43.2"
|
||||||
|
|
||||||
|
testImplementation "junit:junit:4.13.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -71,6 +73,9 @@ android {
|
||||||
res.srcDirs = ['res']
|
res.srcDirs = ['res']
|
||||||
assets.srcDirs = ['assets']
|
assets.srcDirs = ['assets']
|
||||||
}
|
}
|
||||||
|
test {
|
||||||
|
java.srcDirs = ['test']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
|
@ -579,6 +579,8 @@
|
||||||
<item>DOWN_NEXT</item>
|
<item>DOWN_NEXT</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string name="default_volume_key_navigation_value">OFF</string>
|
<string name="default_volume_key_navigation_value">OFF</string>
|
||||||
|
<string name="settings_load_next_on_mark_read">Open next feed/folder after read</string>
|
||||||
|
<string name="settings_load_next_on_mark_read_summary">Load the next feed/folder after marked as read</string>
|
||||||
|
|
||||||
<string name="settings_confirm_mark_all_read">Confirm mark all read on…</string>
|
<string name="settings_confirm_mark_all_read">Confirm mark all read on…</string>
|
||||||
<string name="none">Neither</string>
|
<string name="none">Neither</string>
|
||||||
|
|
|
@ -148,6 +148,11 @@
|
||||||
android:entries="@array/volume_key_navigation_entries"
|
android:entries="@array/volume_key_navigation_entries"
|
||||||
android:entryValues="@array/volume_key_navigation_values"
|
android:entryValues="@array/volume_key_navigation_values"
|
||||||
android:defaultValue="@string/default_volume_key_navigation_value" />
|
android:defaultValue="@string/default_volume_key_navigation_value" />
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="load_next_on_mark_read"
|
||||||
|
android:title="@string/settings_load_next_on_mark_read"
|
||||||
|
android:summary="@string/settings_load_next_on_mark_read_summary" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
|
|
|
@ -3,6 +3,9 @@ package com.newsblur.activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -12,17 +15,30 @@ import com.google.android.play.core.review.ReviewManager;
|
||||||
import com.google.android.play.core.review.ReviewManagerFactory;
|
import com.google.android.play.core.review.ReviewManagerFactory;
|
||||||
import com.google.android.play.core.tasks.Task;
|
import com.google.android.play.core.tasks.Task;
|
||||||
import com.newsblur.R;
|
import com.newsblur.R;
|
||||||
|
import com.newsblur.di.IconLoader;
|
||||||
import com.newsblur.domain.Feed;
|
import com.newsblur.domain.Feed;
|
||||||
import com.newsblur.fragment.DeleteFeedFragment;
|
import com.newsblur.fragment.DeleteFeedFragment;
|
||||||
import com.newsblur.fragment.FeedIntelTrainerFragment;
|
import com.newsblur.fragment.FeedIntelTrainerFragment;
|
||||||
import com.newsblur.fragment.RenameDialogFragment;
|
import com.newsblur.fragment.RenameDialogFragment;
|
||||||
import com.newsblur.util.FeedExt;
|
import com.newsblur.util.FeedExt;
|
||||||
import com.newsblur.util.FeedSet;
|
import com.newsblur.util.FeedSet;
|
||||||
|
import com.newsblur.util.ImageLoader;
|
||||||
import com.newsblur.util.PrefsUtils;
|
import com.newsblur.util.PrefsUtils;
|
||||||
|
import com.newsblur.util.Session;
|
||||||
|
import com.newsblur.util.SessionDataSource;
|
||||||
import com.newsblur.util.UIUtils;
|
import com.newsblur.util.UIUtils;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
public class FeedItemsList extends ItemsList {
|
public class FeedItemsList extends ItemsList {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IconLoader
|
||||||
|
ImageLoader iconLoader;
|
||||||
|
|
||||||
public static final String EXTRA_FEED = "feed";
|
public static final String EXTRA_FEED = "feed";
|
||||||
public static final String EXTRA_FOLDER_NAME = "folderName";
|
public static final String EXTRA_FOLDER_NAME = "folderName";
|
||||||
private Feed feed;
|
private Feed feed;
|
||||||
|
@ -31,22 +47,21 @@ public class FeedItemsList extends ItemsList {
|
||||||
private ReviewInfo reviewInfo;
|
private ReviewInfo reviewInfo;
|
||||||
|
|
||||||
public static void startActivity(Context context, FeedSet feedSet,
|
public static void startActivity(Context context, FeedSet feedSet,
|
||||||
Feed feed, String folderName) {
|
Feed feed, String folderName,
|
||||||
|
@Nullable SessionDataSource sessionDataSource) {
|
||||||
Intent intent = new Intent(context, FeedItemsList.class);
|
Intent intent = new Intent(context, FeedItemsList.class);
|
||||||
intent.putExtra(ItemsList.EXTRA_FEED_SET, feedSet);
|
|
||||||
intent.putExtra(FeedItemsList.EXTRA_FEED, feed);
|
intent.putExtra(FeedItemsList.EXTRA_FEED, feed);
|
||||||
intent.putExtra(FeedItemsList.EXTRA_FOLDER_NAME, folderName);
|
intent.putExtra(FeedItemsList.EXTRA_FOLDER_NAME, folderName);
|
||||||
|
intent.putExtra(ItemsList.EXTRA_FEED_SET, feedSet);
|
||||||
|
intent.putExtra(ItemsList.EXTRA_SESSION_DATA, sessionDataSource);
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle) {
|
protected void onCreate(Bundle bundle) {
|
||||||
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
|
|
||||||
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
|
|
||||||
|
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
setupFeedItems(getIntent());
|
||||||
UIUtils.setupToolbar(this, feed.faviconUrl, feed.title, iconLoader, false);
|
viewModel.getNextSession().observe(this, this::setupFeedItems);
|
||||||
checkInAppReview();
|
checkInAppReview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +153,28 @@ public class FeedItemsList extends ItemsList {
|
||||||
return "feed:" + feed.feedId;
|
return "feed:" + feed.feedId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupFeedItems(Session session) {
|
||||||
|
Feed feed = session.getFeed();
|
||||||
|
String folderName = session.getFolderName();
|
||||||
|
if (feed != null && folderName != null) {
|
||||||
|
setupFeedItems(feed, folderName);
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupFeedItems(Intent intent) {
|
||||||
|
Feed feed = (Feed) intent.getSerializableExtra(EXTRA_FEED);
|
||||||
|
String folderName = intent.getStringExtra(EXTRA_FOLDER_NAME);
|
||||||
|
setupFeedItems(feed, folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupFeedItems(@NonNull Feed feed, @NonNull String folderName) {
|
||||||
|
this.feed = feed;
|
||||||
|
this.folderName = folderName;
|
||||||
|
UIUtils.setupToolbar(this, feed.faviconUrl, feed.title, iconLoader, false);
|
||||||
|
}
|
||||||
|
|
||||||
private void checkInAppReview() {
|
private void checkInAppReview() {
|
||||||
if (!PrefsUtils.hasInAppReviewed(this)) {
|
if (!PrefsUtils.hasInAppReviewed(this)) {
|
||||||
reviewManager = ReviewManagerFactory.create(this);
|
reviewManager = ReviewManagerFactory.create(this);
|
||||||
|
|
|
@ -12,15 +12,19 @@ public class FolderItemsList extends ItemsList {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle) {
|
protected void onCreate(Bundle bundle) {
|
||||||
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
|
|
||||||
|
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
setupFolder(getIntent().getStringExtra(EXTRA_FOLDER_NAME));
|
||||||
UIUtils.setupToolbar(this, R.drawable.ic_folder_closed, folderName, false);
|
viewModel.getNextSession().observe(this, session ->
|
||||||
|
setupFolder(session.getFolderName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
String getSaveSearchFeedId() {
|
String getSaveSearchFeedId() {
|
||||||
return "river:" + folderName;
|
return "river:" + folderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupFolder(String folderName) {
|
||||||
|
this.folderName = folderName;
|
||||||
|
UIUtils.setupToolbar(this, R.drawable.ic_folder_closed, folderName, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,12 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
|
||||||
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
|
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
|
@ -19,23 +23,25 @@ import com.newsblur.database.BlurDatabaseHelper;
|
||||||
import com.newsblur.databinding.ActivityItemslistBinding;
|
import com.newsblur.databinding.ActivityItemslistBinding;
|
||||||
import com.newsblur.delegate.ItemListContextMenuDelegate;
|
import com.newsblur.delegate.ItemListContextMenuDelegate;
|
||||||
import com.newsblur.delegate.ItemListContextMenuDelegateImpl;
|
import com.newsblur.delegate.ItemListContextMenuDelegateImpl;
|
||||||
import com.newsblur.di.IconLoader;
|
|
||||||
import com.newsblur.fragment.ItemSetFragment;
|
import com.newsblur.fragment.ItemSetFragment;
|
||||||
import com.newsblur.service.NBSyncService;
|
import com.newsblur.service.NBSyncService;
|
||||||
import com.newsblur.util.AppConstants;
|
import com.newsblur.util.AppConstants;
|
||||||
import com.newsblur.util.FeedSet;
|
import com.newsblur.util.FeedSet;
|
||||||
import com.newsblur.util.FeedUtils;
|
import com.newsblur.util.FeedUtils;
|
||||||
import com.newsblur.util.ImageLoader;
|
import com.newsblur.util.ReadingActionListener;
|
||||||
import com.newsblur.util.PrefsUtils;
|
import com.newsblur.util.PrefsUtils;
|
||||||
|
import com.newsblur.util.Session;
|
||||||
|
import com.newsblur.util.SessionDataSource;
|
||||||
import com.newsblur.util.StateFilter;
|
import com.newsblur.util.StateFilter;
|
||||||
import com.newsblur.util.UIUtils;
|
import com.newsblur.util.UIUtils;
|
||||||
|
import com.newsblur.viewModel.ItemListViewModel;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
public abstract class ItemsList extends NbActivity {
|
public abstract class ItemsList extends NbActivity implements ReadingActionListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
BlurDatabaseHelper dbHelper;
|
BlurDatabaseHelper dbHelper;
|
||||||
|
@ -43,22 +49,22 @@ public abstract class ItemsList extends NbActivity {
|
||||||
@Inject
|
@Inject
|
||||||
FeedUtils feedUtils;
|
FeedUtils feedUtils;
|
||||||
|
|
||||||
@Inject
|
|
||||||
@IconLoader
|
|
||||||
ImageLoader iconLoader;
|
|
||||||
|
|
||||||
public static final String EXTRA_FEED_SET = "feed_set";
|
public static final String EXTRA_FEED_SET = "feed_set";
|
||||||
public static final String EXTRA_STORY_HASH = "story_hash";
|
public static final String EXTRA_STORY_HASH = "story_hash";
|
||||||
public static final String EXTRA_WIDGET_STORY = "widget_story";
|
public static final String EXTRA_WIDGET_STORY = "widget_story";
|
||||||
public static final String EXTRA_VISIBLE_SEARCH = "visibleSearch";
|
public static final String EXTRA_VISIBLE_SEARCH = "visibleSearch";
|
||||||
|
public static final String EXTRA_SESSION_DATA = "session_data";
|
||||||
private static final String BUNDLE_ACTIVE_SEARCH_QUERY = "activeSearchQuery";
|
private static final String BUNDLE_ACTIVE_SEARCH_QUERY = "activeSearchQuery";
|
||||||
private ActivityItemslistBinding binding;
|
|
||||||
|
|
||||||
protected ItemListContextMenuDelegate contextMenuDelegate;
|
protected ItemListViewModel viewModel;
|
||||||
protected ItemSetFragment itemSetFragment;
|
|
||||||
protected StateFilter intelState;
|
|
||||||
protected FeedSet fs;
|
protected FeedSet fs;
|
||||||
|
|
||||||
|
private ItemSetFragment itemSetFragment;
|
||||||
|
private ActivityItemslistBinding binding;
|
||||||
|
private ItemListContextMenuDelegate contextMenuDelegate;
|
||||||
|
@Nullable
|
||||||
|
private SessionDataSource sessionDataSource;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle bundle) {
|
protected void onCreate(Bundle bundle) {
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
@ -66,8 +72,9 @@ public abstract class ItemsList extends NbActivity {
|
||||||
overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left);
|
overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left);
|
||||||
|
|
||||||
contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils);
|
contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils);
|
||||||
|
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
|
||||||
fs = (FeedSet) getIntent().getSerializableExtra(EXTRA_FEED_SET);
|
fs = (FeedSet) getIntent().getSerializableExtra(EXTRA_FEED_SET);
|
||||||
intelState = PrefsUtils.getStateFilter(this);
|
sessionDataSource = (SessionDataSource) getIntent().getSerializableExtra(EXTRA_SESSION_DATA);
|
||||||
|
|
||||||
// this is not strictly necessary, since our first refresh with the fs will swap in
|
// this is not strictly necessary, since our first refresh with the fs will swap in
|
||||||
// the correct session, but that can be delayed by sync backup, so we try here to
|
// the correct session, but that can be delayed by sync backup, so we try here to
|
||||||
|
@ -77,6 +84,7 @@ public abstract class ItemsList extends NbActivity {
|
||||||
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
|
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
|
||||||
UIUtils.startReadingActivity(fs, hash, this);
|
UIUtils.startReadingActivity(fs, hash, this);
|
||||||
} else if (PrefsUtils.isAutoOpenFirstUnread(this)) {
|
} else if (PrefsUtils.isAutoOpenFirstUnread(this)) {
|
||||||
|
StateFilter intelState = PrefsUtils.getStateFilter(this);
|
||||||
if (dbHelper.getUnreadCount(fs, intelState) > 0) {
|
if (dbHelper.getUnreadCount(fs, intelState) > 0) {
|
||||||
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this);
|
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this);
|
||||||
}
|
}
|
||||||
|
@ -188,6 +196,27 @@ public abstract class ItemsList extends NbActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReadingActionCompleted() {
|
||||||
|
if (sessionDataSource != null) {
|
||||||
|
Session session = sessionDataSource.getNextSession();
|
||||||
|
if (session != null) {
|
||||||
|
// set the next session on the parent activity
|
||||||
|
fs = session.getFeedSet();
|
||||||
|
feedUtils.prepareReadingSession(fs, false);
|
||||||
|
triggerSync();
|
||||||
|
|
||||||
|
// set the next session on the child activity
|
||||||
|
viewModel.updateSession(session);
|
||||||
|
|
||||||
|
// update item set fragment
|
||||||
|
itemSetFragment.resetEmptyState();
|
||||||
|
itemSetFragment.hasUpdated();
|
||||||
|
itemSetFragment.scrollToTop();
|
||||||
|
} else finish();
|
||||||
|
} else finish();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateStatusIndicators() {
|
private void updateStatusIndicators() {
|
||||||
if (binding.itemlistSyncStatus != null) {
|
if (binding.itemlistSyncStatus != null) {
|
||||||
String syncStatus = NBSyncService.getSyncStatusMessage(this, true);
|
String syncStatus = NBSyncService.getSyncStatusMessage(this, true);
|
||||||
|
@ -266,4 +295,5 @@ public abstract class ItemsList extends NbActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract String getSaveSearchFeedId();
|
abstract String getSaveSearchFeedId();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,22 @@ package com.newsblur.activity;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.newsblur.di.IconLoader;
|
||||||
import com.newsblur.domain.SocialFeed;
|
import com.newsblur.domain.SocialFeed;
|
||||||
|
import com.newsblur.util.ImageLoader;
|
||||||
import com.newsblur.util.UIUtils;
|
import com.newsblur.util.UIUtils;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
public class SocialFeedItemsList extends ItemsList {
|
public class SocialFeedItemsList extends ItemsList {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
@IconLoader
|
||||||
|
ImageLoader iconLoader;
|
||||||
|
|
||||||
public static final String EXTRA_SOCIAL_FEED = "social_feed";
|
public static final String EXTRA_SOCIAL_FEED = "social_feed";
|
||||||
|
|
||||||
private SocialFeed socialFeed;
|
private SocialFeed socialFeed;
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
package com.newsblur.database;
|
package com.newsblur.database;
|
||||||
|
|
||||||
|
import static com.newsblur.util.AppConstants.ALL_SHARED_STORIES_GROUP_KEY;
|
||||||
|
import static com.newsblur.util.AppConstants.ALL_STORIES_GROUP_KEY;
|
||||||
|
import static com.newsblur.util.AppConstants.GLOBAL_SHARED_STORIES_GROUP_KEY;
|
||||||
|
import static com.newsblur.util.AppConstants.INFREQUENT_SITE_STORIES_GROUP_KEY;
|
||||||
|
import static com.newsblur.util.AppConstants.READ_STORIES_GROUP_KEY;
|
||||||
|
import static com.newsblur.util.AppConstants.SAVED_SEARCHES_GROUP_KEY;
|
||||||
|
import static com.newsblur.util.AppConstants.SAVED_STORIES_GROUP_KEY;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -35,8 +43,10 @@ import com.newsblur.domain.Folder;
|
||||||
import com.newsblur.domain.SavedSearch;
|
import com.newsblur.domain.SavedSearch;
|
||||||
import com.newsblur.domain.StarredCount;
|
import com.newsblur.domain.StarredCount;
|
||||||
import com.newsblur.domain.SocialFeed;
|
import com.newsblur.domain.SocialFeed;
|
||||||
|
import com.newsblur.util.Session;
|
||||||
import com.newsblur.util.AppConstants;
|
import com.newsblur.util.AppConstants;
|
||||||
import com.newsblur.util.FeedListOrder;
|
import com.newsblur.util.FeedListOrder;
|
||||||
|
import com.newsblur.util.SessionDataSource;
|
||||||
import com.newsblur.util.SpacingStyle;
|
import com.newsblur.util.SpacingStyle;
|
||||||
import com.newsblur.util.FeedSet;
|
import com.newsblur.util.FeedSet;
|
||||||
import com.newsblur.util.ImageLoader;
|
import com.newsblur.util.ImageLoader;
|
||||||
|
@ -52,21 +62,6 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
||||||
private enum GroupType { GLOBAL_SHARED_STORIES, ALL_SHARED_STORIES, INFREQUENT_STORIES, ALL_STORIES, FOLDER, READ_STORIES, SAVED_SEARCHES, SAVED_STORIES }
|
private enum GroupType { GLOBAL_SHARED_STORIES, ALL_SHARED_STORIES, INFREQUENT_STORIES, ALL_STORIES, FOLDER, READ_STORIES, SAVED_SEARCHES, SAVED_STORIES }
|
||||||
private enum ChildType { SOCIAL_FEED, FEED, SAVED_BY_TAG, SAVED_SEARCH }
|
private enum ChildType { SOCIAL_FEED, FEED, SAVED_BY_TAG, SAVED_SEARCH }
|
||||||
|
|
||||||
// The following keys are used to mark the position of the special meta-folders within
|
|
||||||
// the folders array. Since the ExpandableListView doesn't handle collapsing of views
|
|
||||||
// set to View.GONE, we have to totally remove any hidden groups from the group count
|
|
||||||
// and adjust all folder indicies accordingly. Fake folders are created with these
|
|
||||||
// very unlikely names and layout methods check against them before assuming a row is
|
|
||||||
// a normal folder. All the string comparison is a small price to pay to avoid the
|
|
||||||
// alternative of index-counting in a situation where some rows might be disabled.
|
|
||||||
private static final String GLOBAL_SHARED_STORIES_GROUP_KEY = "GLOBAL_SHARED_STORIES_GROUP_KEY";
|
|
||||||
private static final String ALL_SHARED_STORIES_GROUP_KEY = "ALL_SHARED_STORIES_GROUP_KEY";
|
|
||||||
private static final String ALL_STORIES_GROUP_KEY = "ALL_STORIES_GROUP_KEY";
|
|
||||||
private static final String INFREQUENT_SITE_STORIES_GROUP_KEY = "INFREQUENT_SITE_STORIES_GROUP_KEY";
|
|
||||||
private static final String READ_STORIES_GROUP_KEY = "READ_STORIES_GROUP_KEY";
|
|
||||||
private static final String SAVED_STORIES_GROUP_KEY = "SAVED_STORIES_GROUP_KEY";
|
|
||||||
private static final String SAVED_SEARCHES_GROUP_KEY = "SAVED_SEARCHES_GROUP_KEY";
|
|
||||||
|
|
||||||
private final static float defaultTextSize_childName = 14;
|
private final static float defaultTextSize_childName = 14;
|
||||||
private final static float defaultTextSize_groupName = 13;
|
private final static float defaultTextSize_groupName = 13;
|
||||||
private final static float defaultTextSize_count = 13;
|
private final static float defaultTextSize_count = 13;
|
||||||
|
@ -982,4 +977,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
|
||||||
this.spacingStyle = spacingStyle;
|
this.spacingStyle = spacingStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SessionDataSource buildSessionDataSource(Session activeSession) {
|
||||||
|
return new SessionDataSource(activeSession, activeFolderNames, activeFolderChildren);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -423,11 +423,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case R.id.menu_mark_older_stories_as_read:
|
case R.id.menu_mark_older_stories_as_read:
|
||||||
feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options, false);
|
feedUtils.markRead(context, fs, story.timestamp, null, R.array.mark_older_read_options);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case R.id.menu_mark_newer_stories_as_read:
|
case R.id.menu_mark_newer_stories_as_read:
|
||||||
feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options, false);
|
feedUtils.markRead(context, fs, null, story.timestamp, R.array.mark_newer_read_options);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case R.id.menu_send_story:
|
case R.id.menu_send_story:
|
||||||
|
@ -456,7 +456,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||||
case R.id.menu_go_to_feed:
|
case R.id.menu_go_to_feed:
|
||||||
FeedSet fs = FeedSet.singleFeed(story.feedId);
|
FeedSet fs = FeedSet.singleFeed(story.feedId);
|
||||||
FeedItemsList.startActivity(context, fs,
|
FeedItemsList.startActivity(context, fs,
|
||||||
feedUtils.getFeed(story.feedId), null);
|
feedUtils.getFeed(story.feedId), null, null);
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -27,7 +27,7 @@ interface ItemListContextMenuDelegate {
|
||||||
open class ItemListContextMenuDelegateImpl(
|
open class ItemListContextMenuDelegateImpl(
|
||||||
private val activity: ItemsList,
|
private val activity: ItemsList,
|
||||||
private val feedUtils: FeedUtils,
|
private val feedUtils: FeedUtils,
|
||||||
) : ItemListContextMenuDelegate {
|
) : ItemListContextMenuDelegate, ReadingActionListener by activity {
|
||||||
|
|
||||||
override fun onCreateMenuOptions(menu: Menu, menuInflater: MenuInflater, fs: FeedSet): Boolean {
|
override fun onCreateMenuOptions(menu: Menu, menuInflater: MenuInflater, fs: FeedSet): Boolean {
|
||||||
menuInflater.inflate(R.menu.itemslist, menu)
|
menuInflater.inflate(R.menu.itemslist, menu)
|
||||||
|
@ -171,7 +171,7 @@ open class ItemListContextMenuDelegateImpl(
|
||||||
activity.finish()
|
activity.finish()
|
||||||
return true
|
return true
|
||||||
} else if (item.itemId == R.id.menu_mark_all_as_read) {
|
} else if (item.itemId == R.id.menu_mark_all_as_read) {
|
||||||
feedUtils.markRead(activity, fs, null, null, R.array.mark_all_read_options, true)
|
feedUtils.markRead(activity, fs, null, null, R.array.mark_all_read_options, this)
|
||||||
return true
|
return true
|
||||||
} else if (item.itemId == R.id.menu_story_order_newest) {
|
} else if (item.itemId == R.id.menu_story_order_newest) {
|
||||||
updateStoryOrder(fragment, fs, StoryOrder.NEWEST)
|
updateStoryOrder(fragment, fs, StoryOrder.NEWEST)
|
||||||
|
|
|
@ -10,10 +10,6 @@ annotation class StoryFileCache
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
annotation class IconFileCache
|
annotation class IconFileCache
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@Retention(AnnotationRetention.BINARY)
|
|
||||||
annotation class ThumbnailFileCache
|
|
||||||
|
|
||||||
@Qualifier
|
@Qualifier
|
||||||
@Retention(AnnotationRetention.BINARY)
|
@Retention(AnnotationRetention.BINARY)
|
||||||
annotation class IconLoader
|
annotation class IconLoader
|
||||||
|
|
|
@ -12,6 +12,7 @@ import java.util.List;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.newsblur.database.DatabaseConstants;
|
import com.newsblur.database.DatabaseConstants;
|
||||||
import com.newsblur.util.FeedListOrder;
|
import com.newsblur.util.FeedListOrder;
|
||||||
|
import com.newsblur.util.FeedUtils;
|
||||||
|
|
||||||
public class Feed implements Comparable<Feed>, Serializable {
|
public class Feed implements Comparable<Feed>, Serializable {
|
||||||
|
|
||||||
|
@ -153,7 +154,7 @@ public class Feed implements Comparable<Feed>, Serializable {
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (! (o instanceof Feed)) return false;
|
if (! (o instanceof Feed)) return false;
|
||||||
Feed otherFeed = (Feed) o;
|
Feed otherFeed = (Feed) o;
|
||||||
return (TextUtils.equals(feedId, otherFeed.feedId));
|
return (FeedUtils.textUtilsEquals(feedId, otherFeed.feedId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -51,8 +51,10 @@ import com.newsblur.domain.Feed;
|
||||||
import com.newsblur.domain.Folder;
|
import com.newsblur.domain.Folder;
|
||||||
import com.newsblur.domain.SavedSearch;
|
import com.newsblur.domain.SavedSearch;
|
||||||
import com.newsblur.domain.SocialFeed;
|
import com.newsblur.domain.SocialFeed;
|
||||||
|
import com.newsblur.util.Session;
|
||||||
import com.newsblur.util.AppConstants;
|
import com.newsblur.util.AppConstants;
|
||||||
import com.newsblur.util.FeedExt;
|
import com.newsblur.util.FeedExt;
|
||||||
|
import com.newsblur.util.SessionDataSource;
|
||||||
import com.newsblur.util.SpacingStyle;
|
import com.newsblur.util.SpacingStyle;
|
||||||
import com.newsblur.util.FeedSet;
|
import com.newsblur.util.FeedSet;
|
||||||
import com.newsblur.util.FeedUtils;
|
import com.newsblur.util.FeedUtils;
|
||||||
|
@ -392,7 +394,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markFeedsAsRead(FeedSet fs) {
|
private void markFeedsAsRead(FeedSet fs) {
|
||||||
feedUtils.markRead(((NbActivity) getActivity()), fs, null, null, R.array.mark_all_read_options, false);
|
feedUtils.markRead(((NbActivity) getActivity()), fs, null, null, R.array.mark_all_read_options);
|
||||||
adapter.lastFeedViewedId = fs.getSingleFeed();
|
adapter.lastFeedViewedId = fs.getSingleFeed();
|
||||||
adapter.lastFolderViewed = fs.getFolderName();
|
adapter.lastFolderViewed = fs.getFolderName();
|
||||||
}
|
}
|
||||||
|
@ -437,7 +439,13 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onGroupClick(ExpandableListView list, View group, int groupPosition, long id) {
|
public boolean onGroupClick(ExpandableListView list, View group, int groupPosition, long id) {
|
||||||
Intent i = null;
|
if (adapter.isRowSavedSearches(groupPosition)) {
|
||||||
|
// group not clickable
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedSet fs = adapter.getGroup(groupPosition);
|
||||||
|
Intent i;
|
||||||
if (adapter.isRowAllStories(groupPosition)) {
|
if (adapter.isRowAllStories(groupPosition)) {
|
||||||
if (currentState == StateFilter.SAVED) {
|
if (currentState == StateFilter.SAVED) {
|
||||||
// the existence of this row in saved mode is something of a framework artifact and may
|
// the existence of this row in saved mode is something of a framework artifact and may
|
||||||
|
@ -456,17 +464,15 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
||||||
i = new Intent(getActivity(), ReadStoriesItemsList.class);
|
i = new Intent(getActivity(), ReadStoriesItemsList.class);
|
||||||
} else if (adapter.isRowSavedStories(groupPosition)) {
|
} else if (adapter.isRowSavedStories(groupPosition)) {
|
||||||
i = new Intent(getActivity(), SavedStoriesItemsList.class);
|
i = new Intent(getActivity(), SavedStoriesItemsList.class);
|
||||||
} else if (adapter.isRowSavedSearches(groupPosition)) {
|
|
||||||
// group not clickable
|
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
i = new Intent(getActivity(), FolderItemsList.class);
|
i = new Intent(getActivity(), FolderItemsList.class);
|
||||||
String canonicalFolderName = adapter.getGroupFolderName(groupPosition);
|
String canonicalFolderName = adapter.getGroupFolderName(groupPosition);
|
||||||
|
SessionDataSource sessionDataSource = getSessionData(fs, canonicalFolderName, null);
|
||||||
i.putExtra(FolderItemsList.EXTRA_FOLDER_NAME, canonicalFolderName);
|
i.putExtra(FolderItemsList.EXTRA_FOLDER_NAME, canonicalFolderName);
|
||||||
|
i.putExtra(ItemsList.EXTRA_SESSION_DATA, sessionDataSource);
|
||||||
adapter.lastFeedViewedId = null;
|
adapter.lastFeedViewedId = null;
|
||||||
adapter.lastFolderViewed = canonicalFolderName;
|
adapter.lastFolderViewed = canonicalFolderName;
|
||||||
}
|
}
|
||||||
FeedSet fs = adapter.getGroup(groupPosition);
|
|
||||||
i.putExtra(ItemsList.EXTRA_FEED_SET, fs);
|
i.putExtra(ItemsList.EXTRA_FEED_SET, fs);
|
||||||
startActivity(i);
|
startActivity(i);
|
||||||
|
|
||||||
|
@ -541,7 +547,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
||||||
|
|
||||||
feedUtils.currentFolderName = folderName;
|
feedUtils.currentFolderName = folderName;
|
||||||
}
|
}
|
||||||
FeedItemsList.startActivity(getActivity(), fs, feed, folderName);
|
SessionDataSource sessionDataSource = getSessionData(fs, folderName, feed);
|
||||||
|
FeedItemsList.startActivity(getActivity(), fs, feed, folderName, sessionDataSource);
|
||||||
adapter.lastFeedViewedId = feed.feedId;
|
adapter.lastFeedViewedId = feed.feedId;
|
||||||
adapter.lastFolderViewed = null;
|
adapter.lastFolderViewed = null;
|
||||||
}
|
}
|
||||||
|
@ -619,4 +626,13 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private SessionDataSource getSessionData(FeedSet fs, String folderName, @Nullable Feed feed) {
|
||||||
|
if (PrefsUtils.loadNextOnMarkRead(requireContext())) {
|
||||||
|
Session activeSession = new Session(fs, folderName, feed);
|
||||||
|
return adapter.buildSessionDataSource(activeSession);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
package com.newsblur.fragment;
|
package com.newsblur.fragment;
|
||||||
|
|
||||||
import com.newsblur.util.FeedUtils;
|
|
||||||
import com.newsblur.util.ReadingAction;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
|
import com.newsblur.util.FeedUtils;
|
||||||
|
import com.newsblur.util.ReadingAction;
|
||||||
|
import com.newsblur.util.ReadingActionListener;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
|
@ -24,29 +27,30 @@ public class ReadingActionConfirmationFragment extends DialogFragment {
|
||||||
private static final String DIALOG_TITLE = "dialog_title";
|
private static final String DIALOG_TITLE = "dialog_title";
|
||||||
private static final String DIALOG_MESSAGE = "dialog_message";
|
private static final String DIALOG_MESSAGE = "dialog_message";
|
||||||
private static final String DIALOG_CHOICES_RID = "dialog_choices_rid";
|
private static final String DIALOG_CHOICES_RID = "dialog_choices_rid";
|
||||||
private static final String FINISH_AFTER = "finish_after";
|
private static final String ACTION_CALLBACK = "action_callback";
|
||||||
|
|
||||||
public static ReadingActionConfirmationFragment newInstance(ReadingAction ra, CharSequence title, CharSequence message, int choicesId, boolean finishAfter) {
|
public static ReadingActionConfirmationFragment newInstance(ReadingAction ra, CharSequence title, CharSequence message, int choicesId, @Nullable ReadingActionListener callback) {
|
||||||
ReadingActionConfirmationFragment fragment = new ReadingActionConfirmationFragment();
|
ReadingActionConfirmationFragment fragment = new ReadingActionConfirmationFragment();
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
args.putSerializable(READING_ACTION, ra);
|
args.putSerializable(READING_ACTION, ra);
|
||||||
args.putCharSequence(DIALOG_TITLE, title);
|
args.putCharSequence(DIALOG_TITLE, title);
|
||||||
args.putCharSequence(DIALOG_MESSAGE, message);
|
args.putCharSequence(DIALOG_MESSAGE, message);
|
||||||
args.putInt(DIALOG_CHOICES_RID, choicesId);
|
args.putInt(DIALOG_CHOICES_RID, choicesId);
|
||||||
args.putBoolean(FINISH_AFTER, finishAfter);
|
args.putSerializable(ACTION_CALLBACK, callback);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
||||||
|
|
||||||
final ReadingAction ra = (ReadingAction) getArguments().getSerializable(READING_ACTION);
|
final ReadingAction ra = (ReadingAction) getArguments().getSerializable(READING_ACTION);
|
||||||
CharSequence title = getArguments().getCharSequence(DIALOG_TITLE);
|
CharSequence title = getArguments().getCharSequence(DIALOG_TITLE);
|
||||||
CharSequence message = getArguments().getCharSequence(DIALOG_MESSAGE);
|
CharSequence message = getArguments().getCharSequence(DIALOG_MESSAGE);
|
||||||
int choicesId = getArguments().getInt(DIALOG_CHOICES_RID);
|
int choicesId = getArguments().getInt(DIALOG_CHOICES_RID);
|
||||||
final boolean finishAfter = getArguments().getBoolean(FINISH_AFTER);
|
@Nullable ReadingActionListener callback = (ReadingActionListener) getArguments().getSerializable(ACTION_CALLBACK);
|
||||||
|
|
||||||
builder.setTitle(title);
|
builder.setTitle(title);
|
||||||
// NB: setting a message will override the display of the buttons, making the dialogue a no-op
|
// NB: setting a message will override the display of the buttons, making the dialogue a no-op
|
||||||
|
@ -54,9 +58,9 @@ public class ReadingActionConfirmationFragment extends DialogFragment {
|
||||||
builder.setItems(choicesId, new DialogInterface.OnClickListener() {
|
builder.setItems(choicesId, new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
if (which == 0) {
|
if (which == 0) {
|
||||||
feedUtils.doAction(ra, getActivity());
|
feedUtils.doAction(ra, requireContext());
|
||||||
if (finishAfter) {
|
if (callback != null) {
|
||||||
getActivity().finish();
|
callback.onReadingActionCompleted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -419,7 +419,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.menu_go_to_feed -> {
|
R.id.menu_go_to_feed -> {
|
||||||
FeedItemsList.startActivity(context, fs, dbHelper.getFeed(story!!.feedId), null)
|
FeedItemsList.startActivity(context, fs, dbHelper.getFeed(story!!.feedId), null, null)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
|
|
@ -95,4 +95,19 @@ public class AppConstants {
|
||||||
// Free standard account sites limit
|
// Free standard account sites limit
|
||||||
public final static int FREE_ACCOUNT_SITE_LIMIT = 64;
|
public final static int FREE_ACCOUNT_SITE_LIMIT = 64;
|
||||||
|
|
||||||
|
// The following keys are used to mark the position of the special meta-folders within
|
||||||
|
// the folders array. Since the ExpandableListView doesn't handle collapsing of views
|
||||||
|
// set to View.GONE, we have to totally remove any hidden groups from the group count
|
||||||
|
// and adjust all folder indicies accordingly. Fake folders are created with these
|
||||||
|
// very unlikely names and layout methods check against them before assuming a row is
|
||||||
|
// a normal folder. All the string comparison is a small price to pay to avoid the
|
||||||
|
// alternative of index-counting in a situation where some rows might be disabled.
|
||||||
|
public static final String GLOBAL_SHARED_STORIES_GROUP_KEY = "GLOBAL_SHARED_STORIES_GROUP_KEY";
|
||||||
|
public static final String ALL_SHARED_STORIES_GROUP_KEY = "ALL_SHARED_STORIES_GROUP_KEY";
|
||||||
|
public static final String ALL_STORIES_GROUP_KEY = "ALL_STORIES_GROUP_KEY";
|
||||||
|
public static final String INFREQUENT_SITE_STORIES_GROUP_KEY = "INFREQUENT_SITE_STORIES_GROUP_KEY";
|
||||||
|
public static final String READ_STORIES_GROUP_KEY = "READ_STORIES_GROUP_KEY";
|
||||||
|
public static final String SAVED_STORIES_GROUP_KEY = "SAVED_STORIES_GROUP_KEY";
|
||||||
|
public static final String SAVED_SEARCHES_GROUP_KEY = "SAVED_SEARCHES_GROUP_KEY";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,8 +330,8 @@ public class FeedSet implements Serializable {
|
||||||
if (!( o instanceof FeedSet)) return false;
|
if (!( o instanceof FeedSet)) return false;
|
||||||
FeedSet s = (FeedSet) o;
|
FeedSet s = (FeedSet) o;
|
||||||
|
|
||||||
if ( !TextUtils.equals(searchQuery, s.searchQuery)) return false;
|
if ( !FeedUtils.textUtilsEquals(searchQuery, s.searchQuery)) return false;
|
||||||
if ( !TextUtils.equals(folderName, s.folderName)) return false;
|
if ( !FeedUtils.textUtilsEquals(folderName, s.folderName)) return false;
|
||||||
if ( isFilterSaved != s.isFilterSaved ) return false;
|
if ( isFilterSaved != s.isFilterSaved ) return false;
|
||||||
if ( (feeds != null) && (s.feeds != null) && s.feeds.equals(feeds) ) return true;
|
if ( (feeds != null) && (s.feeds != null) && s.feeds.equals(feeds) ) return true;
|
||||||
if ( (socialFeeds != null) && (s.socialFeeds != null) && s.socialFeeds.equals(socialFeeds) ) return true;
|
if ( (socialFeeds != null) && (s.socialFeeds != null) && s.socialFeeds.equals(socialFeeds) ) return true;
|
||||||
|
|
|
@ -210,10 +210,13 @@ class FeedUtils(
|
||||||
triggerSync(context)
|
triggerSync(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun markRead(activity: NbActivity, fs: FeedSet, olderThan: Long?, newerThan: Long?, choicesRid: Int) =
|
||||||
|
markRead(activity, fs, olderThan, newerThan, choicesRid, null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks some or all of the stories in a FeedSet as read for an activity, handling confirmation dialogues as necessary.
|
* Marks some or all of the stories in a FeedSet as read for an activity, handling confirmation dialogues as necessary.
|
||||||
*/
|
*/
|
||||||
fun markRead(activity: NbActivity, fs: FeedSet, olderThan: Long?, newerThan: Long?, choicesRid: Int, finishAfter: Boolean) {
|
fun markRead(activity: NbActivity, fs: FeedSet, olderThan: Long?, newerThan: Long?, choicesRid: Int, callback: ReadingActionListener?) {
|
||||||
val ra: ReadingAction = if (fs.isAllNormal && (olderThan != null || newerThan != null)) {
|
val ra: ReadingAction = if (fs.isAllNormal && (olderThan != null || newerThan != null)) {
|
||||||
// the mark-all-read API doesn't support range bounding, so we need to pass each and every
|
// the mark-all-read API doesn't support range bounding, so we need to pass each and every
|
||||||
// feed ID to the API instead.
|
// feed ID to the API instead.
|
||||||
|
@ -252,9 +255,7 @@ class FeedUtils(
|
||||||
}
|
}
|
||||||
if (doImmediate) {
|
if (doImmediate) {
|
||||||
doAction(ra, activity)
|
doAction(ra, activity)
|
||||||
if (finishAfter) {
|
callback?.onReadingActionCompleted()
|
||||||
activity.finish()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
val title: String? = when {
|
val title: String? = when {
|
||||||
fs.isAllNormal -> {
|
fs.isAllNormal -> {
|
||||||
|
@ -270,7 +271,7 @@ class FeedUtils(
|
||||||
dbHelper.getFeed(fs.singleFeed)?.title ?: ""
|
dbHelper.getFeed(fs.singleFeed)?.title ?: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val dialog = ReadingActionConfirmationFragment.newInstance(ra, title, optionalOverrideMessage, choicesRid, finishAfter)
|
val dialog = ReadingActionConfirmationFragment.newInstance(ra, title, optionalOverrideMessage, choicesRid, callback)
|
||||||
dialog.show(activity.supportFragmentManager, "dialog")
|
dialog.show(activity.supportFragmentManager, "dialog")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -508,5 +509,23 @@ class FeedUtils(
|
||||||
val parts = TextUtils.split(storyHash, ":")
|
val parts = TextUtils.split(storyHash, ":")
|
||||||
return if (parts.size != 2) null else parts[0]
|
return if (parts.size != 2) null else parts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of TextUtils.equals because of Java for unit tests
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun textUtilsEquals(a: CharSequence?, b: CharSequence?): Boolean {
|
||||||
|
if (a === b) return true
|
||||||
|
return if (a != null && b != null && a.length == b.length) {
|
||||||
|
if (a is String && b is String) {
|
||||||
|
a == b
|
||||||
|
} else {
|
||||||
|
for (i in a.indices) {
|
||||||
|
if (a[i] != b[i]) return false
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,4 +123,5 @@ public class PrefConstants {
|
||||||
public static final String FEED_CHOOSER_FOLDER_VIEW = "feed_chooser_folder_view";
|
public static final String FEED_CHOOSER_FOLDER_VIEW = "feed_chooser_folder_view";
|
||||||
public static final String WIDGET_BACKGROUND = "widget_background";
|
public static final String WIDGET_BACKGROUND = "widget_background";
|
||||||
public static final String IN_APP_REVIEW = "in_app_review";
|
public static final String IN_APP_REVIEW = "in_app_review";
|
||||||
|
public static final String LOAD_NEXT_ON_MARK_READ = "load_next_on_mark_read";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1064,4 +1064,9 @@ public class PrefsUtils {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||||
return MarkStoryReadBehavior.valueOf(preferences.getString(PrefConstants.STORY_MARK_READ_BEHAVIOR, MarkStoryReadBehavior.IMMEDIATELY.name()));
|
return MarkStoryReadBehavior.valueOf(preferences.getString(PrefConstants.STORY_MARK_READ_BEHAVIOR, MarkStoryReadBehavior.IMMEDIATELY.name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean loadNextOnMarkRead(Context context) {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
|
||||||
|
return prefs.getBoolean(PrefConstants.LOAD_NEXT_ON_MARK_READ, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
package com.newsblur.util
|
||||||
|
|
||||||
|
import com.newsblur.domain.Feed
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Set of folder keys that don't support
|
||||||
|
* mark all read action
|
||||||
|
*/
|
||||||
|
private val invalidMarkAllReadFolderKeys by lazy {
|
||||||
|
setOf(
|
||||||
|
AppConstants.GLOBAL_SHARED_STORIES_GROUP_KEY,
|
||||||
|
AppConstants.ALL_SHARED_STORIES_GROUP_KEY,
|
||||||
|
AppConstants.INFREQUENT_SITE_STORIES_GROUP_KEY,
|
||||||
|
AppConstants.READ_STORIES_GROUP_KEY,
|
||||||
|
AppConstants.SAVED_STORIES_GROUP_KEY,
|
||||||
|
AppConstants.SAVED_SEARCHES_GROUP_KEY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* As of writing this function, the zipping of the two sources
|
||||||
|
* is valid as the "activeFolderNames" and "activeFolderChildren"
|
||||||
|
* can be mapped by their folder name.
|
||||||
|
* @return Map of folder names to their feed list.
|
||||||
|
*/
|
||||||
|
private fun List<String>.zipFolderFeed(foldersChildren: List<List<Feed>>): Map<String, List<Feed>> {
|
||||||
|
val first = this.iterator()
|
||||||
|
val second = foldersChildren.iterator()
|
||||||
|
return buildMap {
|
||||||
|
while (first.hasNext() && second.hasNext()) {
|
||||||
|
this[first.next()] = second.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Feed.toFeedSet() = FeedSet.singleFeed(this.feedId).apply {
|
||||||
|
isMuted = !this@toFeedSet.active
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user's current reading session data source
|
||||||
|
* as constructed and filtered by the home list adapter
|
||||||
|
* based on settings and preferences.
|
||||||
|
*/
|
||||||
|
class SessionDataSource private constructor(
|
||||||
|
private val folders: List<String>,
|
||||||
|
private val foldersChildrenMap: Map<String, List<Feed>>
|
||||||
|
) : Serializable {
|
||||||
|
|
||||||
|
private lateinit var session: Session
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
activeSession: Session,
|
||||||
|
folders: List<String>,
|
||||||
|
foldersChildren: List<List<Feed>>,
|
||||||
|
) : this(
|
||||||
|
folders = folders.filterNot { invalidMarkAllReadFolderKeys.contains(it) },
|
||||||
|
foldersChildrenMap = folders.zipFolderFeed(foldersChildren)
|
||||||
|
.filterNot { invalidMarkAllReadFolderKeys.contains(it.key) },
|
||||||
|
) {
|
||||||
|
this.session = activeSession
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The next feed within a folder or null if the folder
|
||||||
|
* is showing the last feed.
|
||||||
|
*/
|
||||||
|
private fun getNextFolderFeed(feed: Feed, folderName: String): Feed? {
|
||||||
|
val cleanFolderName =
|
||||||
|
// ROOT FOLDER maps to ALL_STORIES_GROUP_KEY
|
||||||
|
if (folderName == AppConstants.ROOT_FOLDER)
|
||||||
|
AppConstants.ALL_STORIES_GROUP_KEY
|
||||||
|
else folderName
|
||||||
|
val folderFeeds = foldersChildrenMap[cleanFolderName]
|
||||||
|
return folderFeeds?.let { feeds ->
|
||||||
|
val feedIndex = feeds.indexOf(feed)
|
||||||
|
if (feedIndex == -1) return null // invalid feed
|
||||||
|
|
||||||
|
val nextFeedIndex = when (feedIndex) {
|
||||||
|
feeds.size - 1 -> null // null feed if EOL
|
||||||
|
in feeds.indices -> feedIndex + 1 // next feed
|
||||||
|
else -> null // no valid feed found
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFeedIndex?.let { feeds[it] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The next non empty folder and its feeds based on the given folder name.
|
||||||
|
* If the next folder doesn't have feeds, it will call itself with the new folder name
|
||||||
|
* until it finds a non empty folder or it will get to the end of the folder list.
|
||||||
|
*/
|
||||||
|
private fun getNextNonEmptyFolder(folderName: String): Pair<String, List<Feed>>? = with(folders.indexOf(folderName)) {
|
||||||
|
val nextIndex = if (this == folders.size - 1) {
|
||||||
|
0 // first folder if EOL
|
||||||
|
} else if (this in folders.indices) {
|
||||||
|
this + 1 // next folder
|
||||||
|
} else this // no folder found
|
||||||
|
|
||||||
|
val nextFolderName = if (nextIndex in folders.indices) {
|
||||||
|
folders[nextIndex]
|
||||||
|
} else null
|
||||||
|
|
||||||
|
if (nextFolderName == null || nextFolderName == folderName)
|
||||||
|
return null
|
||||||
|
|
||||||
|
val feeds = foldersChildrenMap[nextFolderName]
|
||||||
|
if (feeds == null || feeds.isEmpty())
|
||||||
|
// try and get the next non empty folder name
|
||||||
|
getNextNonEmptyFolder(nextFolderName)
|
||||||
|
else nextFolderName to feeds
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getNextSession(): Session? = if (session.feedSet.isFolder) {
|
||||||
|
val folderName = session.feedSet.folderName
|
||||||
|
getNextNonEmptyFolder(folderName)?.let { (nextFolderName, nextFolderFeeds) ->
|
||||||
|
val nextFeedSet = FeedSet.folder(nextFolderName, nextFolderFeeds.map { it.feedId }.toSet())
|
||||||
|
Session(feedSet = nextFeedSet, folderName = nextFolderName).also { nextSession ->
|
||||||
|
session = nextSession
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (session.feed != null && session.folderName != null) {
|
||||||
|
val nextFeed = getNextFolderFeed(feed = session.feed!!, folderName = session.folderName!!)
|
||||||
|
nextFeed?.let {
|
||||||
|
Session(feedSet = it.toFeedSet(), session.folderName, it).also { nextSession ->
|
||||||
|
session = nextSession
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the user's current reading session.
|
||||||
|
*
|
||||||
|
* When reading a folder, [folderName] and [feed] will be null.
|
||||||
|
*
|
||||||
|
* When reading a feed, [folderName] and [feed] will be non null.
|
||||||
|
*/
|
||||||
|
data class Session(
|
||||||
|
val feedSet: FeedSet,
|
||||||
|
val folderName: String? = null,
|
||||||
|
val feed: Feed? = null,
|
||||||
|
) : Serializable
|
||||||
|
|
||||||
|
interface ReadingActionListener : Serializable {
|
||||||
|
fun onReadingActionCompleted()
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.newsblur.viewModel
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.newsblur.util.Session
|
||||||
|
|
||||||
|
class ItemListViewModel : ViewModel() {
|
||||||
|
|
||||||
|
private val _nextSession = MutableLiveData<Session>()
|
||||||
|
val nextSession: LiveData<Session> = _nextSession
|
||||||
|
|
||||||
|
fun updateSession(session: Session) {
|
||||||
|
_nextSession.value = session
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
package com.newsblur
|
||||||
|
|
||||||
|
import com.newsblur.domain.Feed
|
||||||
|
import com.newsblur.util.FeedSet
|
||||||
|
import com.newsblur.util.Session
|
||||||
|
import com.newsblur.util.SessionDataSource
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SessionDataSourceTest {
|
||||||
|
|
||||||
|
private val folders = listOf(
|
||||||
|
"F1",
|
||||||
|
"F2",
|
||||||
|
"F3",
|
||||||
|
"F4",
|
||||||
|
"F5",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val folderChildren = listOf(
|
||||||
|
emptyList(),
|
||||||
|
listOf(
|
||||||
|
createFeed("20"),
|
||||||
|
createFeed("21"),
|
||||||
|
createFeed("22"),
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
createFeed("30"),
|
||||||
|
),
|
||||||
|
emptyList(),
|
||||||
|
listOf(
|
||||||
|
createFeed("50"),
|
||||||
|
createFeed("51"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `session constructor test`() {
|
||||||
|
val feedSet = FeedSet.singleFeed("1")
|
||||||
|
val session = Session(feedSet)
|
||||||
|
Assert.assertEquals(feedSet, session.feedSet)
|
||||||
|
Assert.assertNull(session.feed)
|
||||||
|
Assert.assertNull(session.folderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `session full constructor test`() {
|
||||||
|
val feedSet = FeedSet.singleFeed("10")
|
||||||
|
val feed = createFeed("10")
|
||||||
|
val session = Session(feedSet, "folderName", feed)
|
||||||
|
Assert.assertEquals(feedSet, session.feedSet)
|
||||||
|
Assert.assertEquals("folderName", session.folderName)
|
||||||
|
Assert.assertEquals(feed, session.feed)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `next session for unknown feedId`() {
|
||||||
|
val session = Session(FeedSet.singleFeed("123"))
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
Assert.assertNull(sessionDs.getNextSession())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `next session for empty folder`() {
|
||||||
|
val feedSet = FeedSet.singleFeed("123")
|
||||||
|
val feed = createFeed("123")
|
||||||
|
val session = Session(feedSet, "F1", feed)
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
|
||||||
|
Assert.assertNull(sessionDs.getNextSession())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected to return the next [Session] containing feed id 11
|
||||||
|
* as the second feed in folder F2 after feed id 10
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `next session for F2 feedSet`() {
|
||||||
|
val feedSet = FeedSet.singleFeed("20")
|
||||||
|
val feed = createFeed("20")
|
||||||
|
val session = Session(feedSet, "F2", feed)
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
|
||||||
|
sessionDs.getNextSession()?.let {
|
||||||
|
Assert.assertEquals("F2", it.folderName)
|
||||||
|
Assert.assertEquals("21", it.feed?.feedId)
|
||||||
|
with(it.feedSet) {
|
||||||
|
Assert.assertNotNull(this)
|
||||||
|
Assert.assertTrue(it.feedSet.flatFeedIds.size == 1)
|
||||||
|
Assert.assertEquals("21", it.feedSet.flatFeedIds.first())
|
||||||
|
}
|
||||||
|
} ?: Assert.fail("Next session was null")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected to return a null [Session] because feed id 12
|
||||||
|
* is the last feed id in folder F2
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `next session for end of F2 feedSet`() {
|
||||||
|
val feedSet = FeedSet.singleFeed("22")
|
||||||
|
val feed = createFeed("22")
|
||||||
|
val session = Session(feedSet, "F2", feed)
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
|
||||||
|
Assert.assertNull(sessionDs.getNextSession())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `next session for F2 feedSetFolder`() {
|
||||||
|
val feedSet = FeedSet.folder("F2", setOf("21"))
|
||||||
|
val feed = createFeed("21")
|
||||||
|
val session = Session(feedSet, "F2", feed)
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
|
||||||
|
sessionDs.getNextSession()?.let {
|
||||||
|
Assert.assertNull(it.feed)
|
||||||
|
Assert.assertEquals("F3", it.folderName)
|
||||||
|
Assert.assertEquals("F3", it.feedSet.folderName)
|
||||||
|
Assert.assertEquals("30", it.feedSet.flatFeedIds.firstOrNull())
|
||||||
|
} ?: Assert.fail("Next session is null for F2 feedSetFolder")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected to return folder "F5" because folder "F3"
|
||||||
|
* doesn't have any feeds
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `next session for F3 feedSetFolder`() {
|
||||||
|
val feedSet = FeedSet.folder("F3", setOf("30"))
|
||||||
|
val feed = createFeed("30")
|
||||||
|
val session = Session(feedSet, "F3", feed)
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
|
||||||
|
sessionDs.getNextSession()?.let {
|
||||||
|
Assert.assertNull(it.feed)
|
||||||
|
Assert.assertEquals("F5", it.folderName)
|
||||||
|
Assert.assertEquals("F5", it.feedSet.folderName)
|
||||||
|
Assert.assertEquals("50", it.feedSet.flatFeedIds.firstOrNull())
|
||||||
|
} ?: Assert.fail("Next session is null for F5 feedSetFolder")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expected to return session for F1 feedSetFolder
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `next session for F5 feedSetFolder`() {
|
||||||
|
val feedSet = FeedSet.folder("F5", setOf("50"))
|
||||||
|
val feed = createFeed("50")
|
||||||
|
val session = Session(feedSet, "F5", feed)
|
||||||
|
val sessionDs = SessionDataSource(session, folders, folderChildren)
|
||||||
|
|
||||||
|
sessionDs.getNextSession()?.let {
|
||||||
|
Assert.assertNull(it.feed)
|
||||||
|
Assert.assertEquals("F2", it.folderName)
|
||||||
|
Assert.assertEquals("F2", it.feedSet.folderName)
|
||||||
|
Assert.assertEquals(setOf("20", "21", "22"), it.feedSet.flatFeedIds)
|
||||||
|
} ?: Assert.fail("Next session is null for F5 feedSetFolder")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFeed(id: String) = Feed().apply {
|
||||||
|
feedId = id
|
||||||
|
title = "Feed #$id"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue