Merge branch 'dejal' into catalyst

This commit is contained in:
David Sinclair 2022-10-26 14:52:09 -06:00
commit 0a5e6fe848
56 changed files with 1766 additions and 659 deletions

View file

@ -1028,7 +1028,9 @@ class Profile(models.Model):
self.setup_premium_history()
if not self.is_premium:
if order_id == "nb.premium.archive.99":
self.activate_archive()
elif not self.is_premium:
self.activate_premium()
logging.user(self.user, "~FG~BBNew Android premium subscription: $%s~FW" % amount)

View file

@ -75,6 +75,7 @@
android:label="@string/settings"/>
<activity android:name=".activity.ImportExportActivity" />
<activity android:name=".activity.NotificationsActivity" />
<activity
android:name=".activity.WidgetConfig"
android:launchMode="singleTask"
@ -167,6 +168,7 @@
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />
<receiver android:name=".util.NotifyShareReceiver" android:exported="false" />
<receiver android:name=".widget.WidgetProvider"
android:exported="false">
<intent-filter>

View file

@ -1,17 +1,16 @@
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.10'
repositories {
mavenCentral()
maven {
url 'https://maven.google.com'
}
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.43.2'
}
}
@ -20,7 +19,6 @@ repositories {
maven {
url 'https://maven.google.com'
}
jcenter()
google()
}
@ -44,8 +42,10 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "com.google.dagger:hilt-android:2.40.5"
kapt "com.google.dagger:hilt-compiler:2.40.5"
implementation "com.google.dagger:hilt-android:2.43.2"
kapt "com.google.dagger:hilt-compiler:2.43.2"
testImplementation "junit:junit:4.13.2"
}
android {
@ -54,14 +54,17 @@ android {
applicationId "com.newsblur"
minSdkVersion 21
targetSdkVersion 31
versionCode 205
versionName "12.0.1"
versionCode 207
versionName "12.1.1"
}
compileOptions.with {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
android.buildFeatures.viewBinding = true
buildFeatures {
viewBinding = true
}
sourceSets {
main {
@ -70,6 +73,9 @@ android {
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
test {
java.srcDirs = ['test']
}
}
buildTypes {

View file

@ -1,6 +1,6 @@
#Wed Aug 11 15:59:29 EDT 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

@ -0,0 +1,4 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/gray55" android:state_checked="true" />
<item android:color="@color/gray30" android:state_checked="false" />
</selector>

View file

@ -0,0 +1,4 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/gray75" android:state_checked="true" />
<item android:color="@color/gray90" android:state_checked="false" />
</selector>

View file

@ -0,0 +1,6 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/gray96" android:state_checkable="true" android:state_checked="true" android:state_enabled="true" />
<item android:color="@color/gray65" android:state_checkable="true" android:state_checked="false" android:state_enabled="true" />
<item android:color="@color/gray65" android:state_enabled="true" />
<item android:color="@color/gray65" />
</selector>

View file

@ -0,0 +1,6 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/gray10" android:state_checkable="true" android:state_checked="true" android:state_enabled="true" />
<item android:color="@color/gray30" android:state_checkable="true" android:state_checked="false" android:state_enabled="true" />
<item android:color="@color/gray30" android:state_enabled="true" />
<item android:color="@color/gray30" />
</selector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/gray55" />
<corners android:radius="4dp" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/gray30" />
<corners android:radius="4dp" />
</shape>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<include layout="@layout/toolbar_newsblur" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_feeds"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/txt_no_notifications"
style="?defaultText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:gravity="center"
android:text="@string/no_feed_notifications"
android:textSize="15sp"
android:visibility="gone" />
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="12dp"
android:paddingVertical="8dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_icon"
android:layout_width="19dp"
android:layout_height="19dp"
app:shapeAppearanceOverlay="@style/smallRoundImageShapeAppearance" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="28dp"
android:ellipsize="end"
android:singleLine="true"
android:textSize="15sp"
android:textStyle="bold" />
</FrameLayout>
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/group_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:orientation="horizontal"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_unread"
style="?toggleButton"
android:layout_width="wrap_content"
android:layout_weight="1"
android:text="@string/state_unread"
app:icon="@drawable/ic_indicator_unread"
app:iconGravity="textStart"
app:iconSize="19dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_focus"
style="?toggleButton"
android:layout_width="wrap_content"
android:layout_weight="1"
android:text="@string/state_focus"
app:icon="@drawable/ic_indicator_focus"
app:iconGravity="textStart"
app:iconSize="16dp"
app:iconTint="#FF6BBF7A" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/group_platform"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_email"
style="?toggleButton"
android:layout_width="wrap_content"
android:layout_marginEnd="2dp"
android:layout_weight="1"
android:checkable="true"
android:gravity="center"
android:text="@string/notification_email" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_web"
style="?toggleButton"
android:layout_width="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/notification_web" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_ios"
style="?toggleButton"
android:layout_width="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/notification_ios" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_android"
style="?toggleButton"
android:layout_width="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/notification_android" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</androidx.appcompat.widget.LinearLayoutCompat>

View file

@ -12,7 +12,8 @@
<item android:id="@+id/menu_widget"
android:title="@string/widget"
app:showAsAction="never" />
app:showAsAction="never"
android:visible="false"/>
<item android:id="@+id/menu_premium_account"
android:title="@string/menu_premium_account"
@ -22,6 +23,10 @@
android:title="@string/import_export"
app:showAsAction="never" />
<item android:id="@+id/menu_notifications"
android:title="@string/menu_notifications"
app:showAsAction="never" />
<item android:id="@+id/menu_text_size"
android:title="@string/menu_text_size" >
<menu>

View file

@ -41,6 +41,7 @@
<attr name="muteicon" format="string" />
<attr name="circleProgressIndicator" format="string" />
<attr name="delimiter" format="string" />
<attr name="toggleButton" format="string" />
<attr name="flow" format="string" />
<attr name="imageViewSize" format="integer" />

View file

@ -281,12 +281,14 @@
<string name="menu_mute_sites">Mute Sites…</string>
<string name="mute_sites">Mute Sites</string>
<string name="menu_widget">Widget…</string>
<string name="menu_notifications">Notifications…</string>
<string name="widget">Widget</string>
<string name="title_widget_setup">Tap to setup in NewsBlur</string>
<string name="title_no_subscriptions">No active subscriptions detected</string>
<string name="title_widget_loading">Loading…</string>
<string name="import_export_title">Import/Export OPML</string>
<string name="notifications_title">Notifications</string>
<string name="premium_subscribers_folder">Reading by folder is only available to</string>
<string name="premium_subscribers_search">Search is only available to</string>
@ -577,6 +579,8 @@
<item>DOWN_NEXT</item>
</string-array>
<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="none">Neither</string>
@ -711,4 +715,10 @@
<string name="add_feed">Add feed</string>
<string name="story_author">• %s</string>
<string name="notification_email">Email</string>
<string name="notification_web">Web</string>
<string name="notification_ios">iOS</string>
<string name="notification_android">Android</string>
<string name="no_feed_notifications">No feed notifications</string>
</resources>

View file

@ -21,11 +21,13 @@
<item name="colorControlNormal">@color/gray55</item>
<item name="fontFamily">@font/whitney</item>
</style>
<style name="actionbar.dark" parent="ThemeOverlay.MaterialComponents">
<item name="android:background">@color/dark_bar_background</item>
<item name="colorControlNormal">@color/gray55</item>
<item name="fontFamily">@font/whitney</item>
</style>
<style name="actionbar.black" parent="ThemeOverlay.MaterialComponents">
<item name="android:background">@color/black</item>
<item name="colorControlNormal">@color/gray55</item>
@ -35,6 +37,7 @@
<style name="delimiter">
<item name="android:background">@color/gray85</item>
</style>
<style name="delimiter.dark">
<item name="android:background">@color/gray30</item>
</style>
@ -43,12 +46,15 @@
<item name="android:paddingTop">2dp</item>
<item name="android:paddingBottom">2dp</item>
</style>
<style name="selectorFolderBackground" parent="selectorFolderParent">
<item name="android:background">@drawable/selector_folder_background</item>
</style>
<style name="selectorFolderBackground.dark" parent="selectorFolderParent">
<item name="android:background">@drawable/dark_selector_folder_background</item>
</style>
<style name="selectorFolderBackground.black" parent="selectorFolderParent">
<item name="android:background">@drawable/black_selector_folder_background</item>
</style>
@ -56,19 +62,25 @@
<style name="selectorFeedAlmostBlueBackground" parent="selectorFeedBackground">
<item name="android:background">@drawable/selector_feed_almost_blue_background</item>
</style>
<style name="selectorFeedAlmostRedBackground" parent="selectorFeedBackground">
<item name="android:background">@drawable/selector_feed_almost_red_background</item>
</style>
<style name="selectorFeedAlmostGreenBackground" parent="selectorFeedBackground">
<item name="android:background">@drawable/selector_feed_almost_green_background</item>
</style>
<style name="selectorFeedAlmostBlueBackground.dark" parent="selectorFeedBackground.dark" />
<style name="selectorFeedAlmostRedBackground.dark" parent="selectorFeedBackground.dark" />
<style name="selectorFeedAlmostGreenBackground.dark" parent="selectorFeedBackground.dark" />
<style name="selectorFeedBackground">
<item name="android:background">@drawable/selector_feed_background</item>
</style>
<style name="selectorFeedBackground.dark">
<item name="android:background">@drawable/dark_selector_feed_background</item>
</style>
@ -76,6 +88,7 @@
<style name="feedRowNeutCountText">
<item name="android:textColor">@color/white</item>
</style>
<style name="feedRowNeutCountText.dark">
<item name="android:textColor">@color/black</item>
</style>
@ -83,9 +96,11 @@
<style name="actionbarBackground">
<item name="android:background">@color/bar_background</item>
</style>
<style name="actionbarBackground.dark">
<item name="android:background">@color/dark_bar_background</item>
</style>
<style name="actionbarBackground.black">
<item name="android:background">@color/black</item>
</style>
@ -93,6 +108,7 @@
<style name="listBackground">
<item name="android:background">@drawable/list_background</item>
</style>
<style name="listBackground.dark">
<item name="android:background">@drawable/dark_list_background</item>
</style>
@ -100,9 +116,11 @@
<style name="layoutBackground">
<item name="android:background">@color/item_background</item>
</style>
<style name="layoutBackground.dark">
<item name="android:background">@color/dark_item_background</item>
</style>
<style name="layoutBackground.black">
<item name="android:background">@color/black</item>
</style>
@ -110,9 +128,11 @@
<style name="layoutRoundedBackground">
<item name="android:background">@drawable/shape_rounded_corners_6dp_light</item>
</style>
<style name="layoutRoundedBackground.dark">
<item name="android:background">@drawable/shape_rounded_corners_6dp_dark</item>
</style>
<style name="layoutRoundedBackground.black">
<item name="android:background">@drawable/shape_rounded_corners_6dp_black</item>
</style>
@ -120,9 +140,11 @@
<style name="readingBackground">
<item name="android:background">@color/white</item>
</style>
<style name="readingBackground.dark">
<item name="android:background">@color/dark_item_background</item>
</style>
<style name="readingBackground.black">
<item name="android:background">@color/black</item>
</style>
@ -131,6 +153,7 @@
<item name="android:textColorLink">@color/linkblue</item>
<item name="android:textColor">@color/text</item>
</style>
<style name="defaultText.dark">
<item name="android:textColorLink">@color/dark_linkblue</item>
<item name="android:textColor">@color/white</item>
@ -140,6 +163,7 @@
<item name="android:textColorLink">@color/linkblue</item>
<item name="android:textColor">@color/linkblue</item>
</style>
<style name="linkText.dark">
<item name="android:textColorLink">@color/dark_linkblue</item>
<item name="android:textColor">@color/dark_linkblue</item>
@ -148,6 +172,7 @@
<style name="storySnippetText">
<item name="android:textColor">@color/story_content_text</item>
</style>
<style name="storySnippetText.dark">
<item name="android:textColor">@color/dark_story_content_text</item>
</style>
@ -155,6 +180,7 @@
<style name="storyFeedTitleText">
<item name="android:textColor">@color/story_feed_title_text</item>
</style>
<style name="storyFeedTitleText.dark">
<item name="android:textColor">@color/dark_story_feed_title_text</item>
</style>
@ -162,9 +188,11 @@
<style name="selectorStoryBackground">
<item name="android:background">@drawable/selector_story_background</item>
</style>
<style name="selectorStoryBackground.dark">
<item name="android:background">@drawable/dark_selector_story_background</item>
</style>
<style name="selectorStoryBackground.black">
<item name="android:background">@drawable/black_selector_story_background</item>
</style>
@ -172,9 +200,11 @@
<style name="rowItemHeaderBackground">
<item name="android:background">@drawable/row_item_header_background</item>
</style>
<style name="rowItemHeaderBackground.dark">
<item name="android:background">@drawable/dark_row_item_header_background</item>
</style>
<style name="rowItemHeaderBackground.black">
<item name="android:background">@drawable/black_row_item_header_background</item>
</style>
@ -182,6 +212,7 @@
<style name="readingItemMetadata">
<item name="android:textColor">@color/half_darkgray</item>
</style>
<style name="readingItemMetadata.dark">
<item name="android:textColor">@color/half_white</item>
</style>
@ -191,6 +222,7 @@
<item name="chipBackgroundColor">@color/tag_gray</item>
<item name="android:fontFamily">@font/whitney</item>
</style>
<style name="chip.dark">
<item name="android:textColor">@color/tag_gray</item>
<item name="chipBackgroundColor">@color/tag_bg_dark</item>
@ -203,6 +235,7 @@
<item name="android:background">@color/gray90</item>
<item name="android:letterSpacing">0</item>
</style>
<style name="actionButtons.dark">
<item name="android:textSize">14sp</item>
<item name="android:textColor">@color/button_text_dark</item>
@ -217,6 +250,7 @@
<item name="iconTint">@color/gray75</item>
<item name="iconSize">16dp</item>
</style>
<style name="storyButtons.dark" parent="Widget.MaterialComponents.Button.TextButton">
<item name="android:textSize">14sp</item>
<item name="android:textColor">@color/gray55</item>
@ -229,6 +263,7 @@
<style name="storyButtonsDimmed" parent="storyButtons">
<item name="android:textColor">@color/gray85</item>
</style>
<style name="storyButtonsDimmed.dark" parent="storyButtons.dark">
<item name="android:textColor">@color/gray30</item>
</style>
@ -236,9 +271,11 @@
<style name="shareBarBackground">
<item name="android:background">@color/share_bar_background</item>
</style>
<style name="shareBarBackground.dark">
<item name="android:background">@color/dark_share_bar_background</item>
</style>
<style name="shareBarBackground.black">
<item name="android:background">@color/black</item>
</style>
@ -250,6 +287,7 @@
<item name="android:shadowDy">1</item>
<item name="android:shadowRadius">1</item>
</style>
<style name="shareBarText.dark">
<item name="android:textColor">@color/gray55</item>
<item name="android:shadowDx">0</item>
@ -261,10 +299,12 @@
<item name="android:background">@drawable/gradient_background_default</item>
<item name="android:textColor">@color/text</item>
</style>
<style name="commentsHeader.dark">
<item name="android:background">@drawable/dark_gradient_background_default</item>
<item name="android:textColor">@color/gray55</item>
</style>
<style name="commentsHeader.black">
<item name="android:background">@drawable/black_gradient_background_default</item>
<item name="android:textColor">@color/gray55</item>
@ -274,10 +314,12 @@
<item name="android:background">@drawable/gradient_background_default</item>
<item name="android:textColor">@color/text</item>
</style>
<style name="activityDetailsPager.dark">
<item name="android:background">@drawable/dark_gradient_background_default</item>
<item name="android:textColor">@color/dark_text</item>
</style>
<style name="activityDetailsPager.black">
<item name="android:background">@drawable/black_gradient_background_default</item>
<item name="android:textColor">@color/dark_text</item>
@ -286,6 +328,7 @@
<style name="rowBorder">
<item name="android:background">@color/row_border</item>
</style>
<style name="rowBorder.dark">
<item name="android:background">@color/dark_row_border</item>
</style>
@ -294,10 +337,12 @@
<item name="android:background">@color/item_background</item>
<item name="android:textColor">@color/text</item>
</style>
<style name="profileCount.dark">
<item name="android:background">@color/dark_item_background</item>
<item name="android:textColor">@color/white</item>
</style>
<style name="profileCount.black">
<item name="android:background">@color/black</item>
<item name="android:textColor">@color/white</item>
@ -307,10 +352,12 @@
<item name="android:background">@color/item_background</item>
<item name="android:divider">@drawable/divider_light</item>
</style>
<style name="profileActivityList.dark">
<item name="android:background">@color/dark_item_background</item>
<item name="android:divider">@drawable/divider_dark</item>
</style>
<style name="profileActivityList.black">
<item name="android:background">@color/black</item>
<item name="android:divider">@drawable/divider_dark</item>
@ -319,9 +366,11 @@
<style name="storyCommentDivider">
<item name="android:background">@color/story_comment_divider</item>
</style>
<style name="storyCommentDivider.dark">
<item name="android:background">@color/dark_story_comment_divider</item>
</style>
<style name="storyCommentDivider.black">
<item name="android:background">@color/gray07</item>
</style>
@ -329,6 +378,7 @@
<style name="explainerText">
<item name="android:textColor">@color/gray55</item>
</style>
<style name="explainerText.dark">
<item name="android:textColor">@color/dark_text</item>
</style>
@ -338,15 +388,17 @@
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="toggleText.dark">
<item name="android:textColor">@color/gray55</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
</style>
<style name="selectorOverlayBackgroundLeft">
<item name="android:background">@drawable/selector_overlay_bg_left</item>
</style>
<style name="selectorOverlayBackgroundLeft.dark">
<item name="android:background">@drawable/selector_overlay_bg_dark_left</item>
</style>
@ -355,6 +407,7 @@
<item name="android:background">@drawable/selector_overlay_bg_right</item>
<item name="android:textColor">@color/button_text</item>
</style>
<style name="selectorOverlayBackgroundRight.dark">
<item name="android:background">@drawable/selector_overlay_bg_dark_right</item>
<item name="android:textColor">@color/button_text_dark</item>
@ -364,6 +417,7 @@
<item name="android:background">@drawable/selector_overlay_bg_right_done</item>
<item name="android:textColor">@color/button_text</item>
</style>
<style name="selectorOverlayBackgroundRightDone.dark">
<item name="android:background">@drawable/selector_overlay_bg_dark_right_done</item>
<item name="android:textColor">@color/button_text_dark</item>
@ -372,6 +426,7 @@
<style name="selectorOverlayBackgroundSend">
<item name="android:background">@drawable/selector_overlay_bg_send</item>
</style>
<style name="selectorOverlayBackgroundSend.dark">
<item name="android:background">@drawable/selector_overlay_bg_dark_send</item>
</style>
@ -380,6 +435,7 @@
<item name="android:background">@drawable/selector_overlay_bg_story</item>
<item name="android:textColor">@color/button_text</item>
</style>
<style name="selectorOverlayBackgroundStory.dark">
<item name="android:background">@drawable/selector_overlay_bg_dark_story</item>
<item name="android:textColor">@color/button_text_dark</item>
@ -389,6 +445,7 @@
<item name="android:background">@drawable/selector_overlay_bg_text</item>
<item name="android:textColor">@color/button_text</item>
</style>
<style name="selectorOverlayBackgroundText.dark">
<item name="android:background">@drawable/selector_overlay_bg_dark_text</item>
<item name="android:textColor">@color/button_text_dark</item>
@ -397,6 +454,7 @@
<style name="muteicon">
<item name="android:src">@drawable/mute_white</item>
</style>
<style name="muteicon.dark">
<item name="android:src">@drawable/mute_black</item>
</style>
@ -429,7 +487,7 @@
<style name="dialogPreference" parent="Preference.DialogPreference.Material">
<item name="iconSpaceReserved">false</item>
</style>
<style name="circleProgressIndicator" parent="Widget.MaterialComponents.CircularProgressIndicator">
<item name="indicatorColor">@color/newsblur_blue</item>
<item name="indicatorSize">24dp</item>
@ -448,4 +506,22 @@
<item name="cornerSize">10%</item>
</style>
<style name="toggleButton" parent="Widget.MaterialComponents.Button.TextButton">
<item name="android:letterSpacing">0</item>
<item name="android:textAllCaps">false</item>
<item name="backgroundTint">@color/mtrl_btn_bg_color_selector_light</item>
<item name="android:textColor">@color/mtrl_btn_text_color_selector_light</item>
<item name="android:textSize">14sp</item>
<item name="android:layout_height">40dp</item>
</style>
<style name="toggleButton.dark" parent="Widget.MaterialComponents.Button.TextButton">
<item name="android:letterSpacing">0</item>
<item name="android:textAllCaps">false</item>
<item name="backgroundTint">@color/mtrl_btn_bg_color_selector_dark</item>
<item name="android:textColor">@color/mtrl_btn_text_color_selector_dark</item>
<item name="android:textSize">14sp</item>
<item name="android:layout_height">40dp</item>
</style>
</resources>

View file

@ -54,6 +54,7 @@
<item name="preferenceTheme">@style/preferenceTheme</item>
<item name="fontFamily">@font/whitney</item>
<item name="circleProgressIndicator">@style/circleProgressIndicator</item>
<item name="toggleButton">@style/toggleButton</item>
</style>
<style name="NewsBlurDarkTheme" parent="Theme.MaterialComponents.NoActionBar">
@ -111,6 +112,7 @@
<item name="android:navigationBarColor">@android:color/black</item>
<item name="fontFamily">@font/whitney</item>
<item name="circleProgressIndicator">@style/circleProgressIndicator</item>
<item name="toggleButton">@style/toggleButton.dark</item>
</style>
<style name="NewsBlurBlackTheme" parent="Theme.MaterialComponents.NoActionBar" >
@ -168,6 +170,7 @@
<item name="android:navigationBarColor">@android:color/black</item>
<item name="fontFamily">@font/whitney</item>
<item name="circleProgressIndicator">@style/circleProgressIndicator</item>
<item name="toggleButton">@style/toggleButton.dark</item>
</style>
<style name="NewsBlurTheme.Translucent" parent="NewsBlurTheme">

View file

@ -148,6 +148,11 @@
android:entries="@array/volume_key_navigation_entries"
android:entryValues="@array/volume_key_navigation_values"
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

View file

@ -3,6 +3,9 @@ package com.newsblur.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import android.view.Menu;
import android.view.MenuItem;
@ -12,16 +15,30 @@ import com.google.android.play.core.review.ReviewManager;
import com.google.android.play.core.review.ReviewManagerFactory;
import com.google.android.play.core.tasks.Task;
import com.newsblur.R;
import com.newsblur.di.IconLoader;
import com.newsblur.domain.Feed;
import com.newsblur.fragment.DeleteFeedFragment;
import com.newsblur.fragment.FeedIntelTrainerFragment;
import com.newsblur.fragment.RenameDialogFragment;
import com.newsblur.util.FeedExt;
import com.newsblur.util.FeedSet;
import com.newsblur.util.ImageLoader;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.Session;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.UIUtils;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class FeedItemsList extends ItemsList {
@Inject
@IconLoader
ImageLoader iconLoader;
public static final String EXTRA_FEED = "feed";
public static final String EXTRA_FOLDER_NAME = "folderName";
private Feed feed;
@ -30,22 +47,21 @@ public class FeedItemsList extends ItemsList {
private ReviewInfo reviewInfo;
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.putExtra(ItemsList.EXTRA_FEED_SET, feedSet);
intent.putExtra(FeedItemsList.EXTRA_FEED, feed);
intent.putExtra(FeedItemsList.EXTRA_FOLDER_NAME, folderName);
intent.putExtra(ItemsList.EXTRA_FEED_SET, feedSet);
intent.putExtra(ItemsList.EXTRA_SESSION_DATA, sessionDataSource);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle bundle) {
feed = (Feed) getIntent().getSerializableExtra(EXTRA_FEED);
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
super.onCreate(bundle);
UIUtils.setupToolbar(this, feed.faviconUrl, feed.title, iconLoader, false);
setupFeedItems(getIntent());
viewModel.getNextSession().observe(this, this::setupFeedItems);
checkInAppReview();
}
@ -116,11 +132,11 @@ public class FeedItemsList extends ItemsList {
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (feed.isNotifyUnread()) {
if (FeedExt.isAndroidNotifyUnread(feed)) {
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
menu.findItem(R.id.menu_notifications_unread).setChecked(true);
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
} else if (feed.isNotifyFocus()) {
} else if (FeedExt.isAndroidNotifyFocus(feed)) {
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
menu.findItem(R.id.menu_notifications_focus).setChecked(true);
@ -137,6 +153,28 @@ public class FeedItemsList extends ItemsList {
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() {
if (!PrefsUtils.hasInAppReviewed(this)) {
reviewManager = ReviewManagerFactory.create(this);

View file

@ -20,7 +20,6 @@ import com.newsblur.util.executeAsyncTask
import dagger.hilt.android.AndroidEntryPoint
import java.net.MalformedURLException
import java.net.URL
import java.util.*
import javax.inject.Inject
@AndroidEntryPoint
@ -86,13 +85,9 @@ class FeedSearchActivity : NbActivity(), OnFeedSearchResultClickListener, AddFee
override fun afterTextChanged(s: Editable) {
searchQueryRunnable?.let { handler.removeCallbacks(it) }
searchQueryRunnable = Runnable {
if (tryAddByURL(s.toString())) {
return@Runnable
}
syncClearIconVisibility(s)
if (s.isNotEmpty()) searchQuery(s)
else syncSearchResults(arrayOf())
else syncSearchResults(emptyList())
}
handler.postDelayed(searchQueryRunnable!!, 350)
}
@ -111,7 +106,12 @@ class FeedSearchActivity : NbActivity(), OnFeedSearchResultClickListener, AddFee
onPostExecute = {
binding.loadingCircle.visibility = View.GONE
binding.clearText.visibility = View.VISIBLE
syncSearchResults(it ?: arrayOf())
syncSearchResults(buildList {
if (matchesUrl(query.toString())) {
add(FeedResult.createFeedResultForUrl(query.toString().lowercase()))
}
addAll(it ?: arrayOf())
})
}
)
}
@ -120,15 +120,20 @@ class FeedSearchActivity : NbActivity(), OnFeedSearchResultClickListener, AddFee
binding.clearText.visibility = if (query.isNotEmpty()) View.VISIBLE else View.GONE
}
private fun syncSearchResults(results: Array<FeedResult>) {
private fun syncSearchResults(results: List<FeedResult>) {
adapter.replaceAll(results)
}
private fun showAddFeedDialog(feedUrl: String, feedLabel: String) {
val addFeedFragment: DialogFragment = AddFeedFragment.newInstance(feedUrl, feedLabel)
addFeedFragment.show(supportFragmentManager, "dialog")
}
/**
* See if the text entered in the query field was actually a URL so we can skip the
* search step and just let users who know feed URLs directly subscribe.
* See if the text entered in the query field was actually a URL
* to let users who know feed URLs directly subscribe.
*/
private fun tryAddByURL(s: String): Boolean {
private fun matchesUrl(s: String): Boolean {
var u: URL? = null
try {
u = URL(s)
@ -144,12 +149,6 @@ class FeedSearchActivity : NbActivity(), OnFeedSearchResultClickListener, AddFee
if (u.host == null || u.host.trim().isEmpty()) {
return false
}
showAddFeedDialog(s, s)
return true
}
private fun showAddFeedDialog(feedUrl: String, feedLabel: String) {
val addFeedFragment: DialogFragment = AddFeedFragment.newInstance(feedUrl, feedLabel)
addFeedFragment.show(supportFragmentManager, "dialog")
}
}

View file

@ -8,8 +8,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.newsblur.R
import com.newsblur.databinding.ViewFeedSearchRowBinding
import com.newsblur.domain.FeedResult
import com.newsblur.util.FeedUtils
import com.newsblur.util.ImageLoader
import com.newsblur.util.setViewGone
import com.newsblur.util.setViewVisible
class FeedSearchAdapter(
private val onClickListener: OnFeedSearchResultClickListener,
@ -30,12 +31,11 @@ class FeedSearchAdapter(
override fun getItemCount(): Int = resultsList.size
fun replaceAll(results: Array<FeedResult>) {
val newResultsList: List<FeedResult> = results.toList()
val diffCallback = ResultDiffCallback(resultsList, newResultsList)
fun replaceAll(results: List<FeedResult>) {
val diffCallback = ResultDiffCallback(resultsList, results)
val diffResult = DiffUtil.calculateDiff(diffCallback)
resultsList.clear()
resultsList.addAll(newResultsList)
resultsList.addAll(results)
diffResult.dispatchUpdatesTo(this)
}
@ -46,19 +46,25 @@ class FeedSearchAdapter(
fun bind(result: FeedResult) {
val resultFaviconUrl = result.faviconUrl
if (resultFaviconUrl.isNotEmpty()) {
iconLoader.displayImage(resultFaviconUrl, binding.imgFeedIcon)
iconLoader.displayImage(resultFaviconUrl, binding.imgFeedIcon)
}
binding.textTitle.text = result.label
binding.textTagline.text = result.tagline
val subscribersCountText = binding.root.context.getString(R.string.feed_subscribers, result.numberOfSubscriber)
binding.textSubscriptionCount.text = subscribersCountText
if (result.numberOfSubscriber > 0) {
val subscribersCountText = binding.root.context.getString(R.string.feed_subscribers, result.numberOfSubscriber)
binding.textSubscriptionCount.text = subscribersCountText
binding.textSubscriptionCount.setViewVisible()
} else {
binding.textSubscriptionCount.setViewGone()
}
if (result.url.isNotEmpty()) {
binding.rowResultAddress.text = result.url
binding.rowResultAddress.visibility = View.VISIBLE
binding.rowResultAddress.setViewVisible()
} else {
binding.rowResultAddress.visibility = View.GONE
binding.rowResultAddress.setViewGone()
}
itemView.setOnClickListener {

View file

@ -12,15 +12,19 @@ public class FolderItemsList extends ItemsList {
@Override
protected void onCreate(Bundle bundle) {
folderName = getIntent().getStringExtra(EXTRA_FOLDER_NAME);
super.onCreate(bundle);
UIUtils.setupToolbar(this, R.drawable.ic_folder_closed, folderName, false);
setupFolder(getIntent().getStringExtra(EXTRA_FOLDER_NAME));
viewModel.getNextSession().observe(this, session ->
setupFolder(session.getFolderName()));
}
@Override
String getSaveSearchFeedId() {
return "river:" + folderName;
}
private void setupFolder(String folderName) {
this.folderName = folderName;
UIUtils.setupToolbar(this, R.drawable.ic_folder_closed, folderName, false);
}
}

View file

@ -5,12 +5,15 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import static com.newsblur.service.NBSyncReceiver.UPDATE_STORY;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnKeyListener;
@ -18,32 +21,27 @@ import android.view.View.OnKeyListener;
import com.newsblur.R;
import com.newsblur.database.BlurDatabaseHelper;
import com.newsblur.databinding.ActivityItemslistBinding;
import com.newsblur.di.IconLoader;
import com.newsblur.delegate.ItemListContextMenuDelegate;
import com.newsblur.delegate.ItemListContextMenuDelegateImpl;
import com.newsblur.fragment.ItemSetFragment;
import com.newsblur.fragment.SaveSearchFragment;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ImageLoader;
import com.newsblur.util.PrefConstants.ThemeValue;
import com.newsblur.util.ReadingActionListener;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.SpacingStyle;
import com.newsblur.util.Session;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryContentPreviewStyle;
import com.newsblur.util.StoryListStyle;
import com.newsblur.util.StoryOrder;
import com.newsblur.util.ListTextSize;
import com.newsblur.util.ThumbnailStyle;
import com.newsblur.util.UIUtils;
import com.newsblur.viewModel.ItemListViewModel;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public abstract class ItemsList extends NbActivity {
public abstract class ItemsList extends NbActivity implements ReadingActionListener {
@Inject
BlurDatabaseHelper dbHelper;
@ -51,21 +49,21 @@ public abstract class ItemsList extends NbActivity {
@Inject
FeedUtils feedUtils;
@Inject
@IconLoader
ImageLoader iconLoader;
public static final String EXTRA_FEED_SET = "feed_set";
public static final String EXTRA_STORY_HASH = "story_hash";
public static final String EXTRA_WIDGET_STORY = "widget_story";
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 ActivityItemslistBinding binding;
protected ItemSetFragment itemSetFragment;
protected StateFilter intelState;
protected ItemListViewModel viewModel;
protected FeedSet fs;
private ItemSetFragment itemSetFragment;
private ActivityItemslistBinding binding;
private ItemListContextMenuDelegate contextMenuDelegate;
@Nullable
private SessionDataSource sessionDataSource;
@Override
protected void onCreate(Bundle bundle) {
@ -73,8 +71,10 @@ public abstract class ItemsList extends NbActivity {
overridePendingTransition(R.anim.slide_in_from_right, R.anim.slide_out_to_left);
contextMenuDelegate = new ItemListContextMenuDelegateImpl(this, feedUtils);
viewModel = new ViewModelProvider(this).get(ItemListViewModel.class);
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
// the correct session, but that can be delayed by sync backup, so we try here to
@ -84,6 +84,7 @@ public abstract class ItemsList extends NbActivity {
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
UIUtils.startReadingActivity(fs, hash, this);
} else if (PrefsUtils.isAutoOpenFirstUnread(this)) {
StateFilter intelState = PrefsUtils.getStateFilter(this);
if (dbHelper.getUnreadCount(fs, intelState) > 0) {
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this);
}
@ -137,11 +138,9 @@ public abstract class ItemsList extends NbActivity {
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (binding.itemlistSearchQuery != null) {
String q = binding.itemlistSearchQuery.getText().toString().trim();
if (q.length() > 0) {
outState.putString(BUNDLE_ACTIVE_SEARCH_QUERY, q);
}
String q = binding.itemlistSearchQuery.getText().toString().trim();
if (q.length() > 0) {
outState.putString(BUNDLE_ACTIVE_SEARCH_QUERY, q);
}
}
@ -167,277 +166,19 @@ public abstract class ItemsList extends NbActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.itemslist, menu);
if (fs.isGlobalShared() ||
fs.isAllSocial() ||
fs.isFilterSaved() ||
fs.isAllSaved() ||
fs.isSingleSavedTag() ||
fs.isInfrequent() ||
fs.isAllRead() ) {
menu.findItem(R.id.menu_mark_all_as_read).setVisible(false);
}
if (fs.isGlobalShared() ||
fs.isAllSocial() ||
fs.isAllRead() ) {
menu.findItem(R.id.menu_story_order).setVisible(false);
}
if (fs.isGlobalShared() ||
fs.isFilterSaved() ||
fs.isAllSaved() ||
fs.isSingleSavedTag() ||
fs.isInfrequent() ||
fs.isAllRead() ) {
menu.findItem(R.id.menu_read_filter).setVisible(false);
menu.findItem(R.id.menu_mark_read_on_scroll).setVisible(false);
menu.findItem(R.id.menu_story_content_preview_style).setVisible(false);
menu.findItem(R.id.menu_story_thumbnail_style).setVisible(false);
}
if (fs.isGlobalShared() ||
fs.isAllSocial() ||
fs.isInfrequent() ||
fs.isAllRead() ) {
menu.findItem(R.id.menu_search_stories).setVisible(false);
}
if ((!fs.isSingleNormal()) || fs.isFilterSaved()) {
menu.findItem(R.id.menu_notifications).setVisible(false);
menu.findItem(R.id.menu_delete_feed).setVisible(false);
menu.findItem(R.id.menu_instafetch_feed).setVisible(false);
menu.findItem(R.id.menu_intel).setVisible(false);
menu.findItem(R.id.menu_rename_feed).setVisible(false);
menu.findItem(R.id.menu_statistics).setVisible(false);
}
if (!fs.isInfrequent()) {
menu.findItem(R.id.menu_infrequent_cutoff).setVisible(false);
}
return true;
return contextMenuDelegate.onCreateMenuOptions(menu, getMenuInflater(), fs);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
super.onPrepareOptionsMenu(menu);
boolean showSavedSearch = !TextUtils.isEmpty(binding.itemlistSearchQuery.getText());
return contextMenuDelegate.onPrepareMenuOptions(menu, fs, showSavedSearch);
}
StoryOrder storyOrder = PrefsUtils.getStoryOrder(this, fs);
if (storyOrder == StoryOrder.NEWEST) {
menu.findItem(R.id.menu_story_order_newest).setChecked(true);
} else if (storyOrder == StoryOrder.OLDEST) {
menu.findItem(R.id.menu_story_order_oldest).setChecked(true);
}
ReadFilter readFilter = PrefsUtils.getReadFilter(this, fs);
if (readFilter == ReadFilter.ALL) {
menu.findItem(R.id.menu_read_filter_all_stories).setChecked(true);
} else if (readFilter == ReadFilter.UNREAD) {
menu.findItem(R.id.menu_read_filter_unread_only).setChecked(true);
}
StoryListStyle listStyle = PrefsUtils.getStoryListStyle(this, fs);
if (listStyle == StoryListStyle.GRID_F) {
menu.findItem(R.id.menu_list_style_grid_f).setChecked(true);
} else if (listStyle == StoryListStyle.GRID_M) {
menu.findItem(R.id.menu_list_style_grid_m).setChecked(true);
} else if (listStyle == StoryListStyle.GRID_C) {
menu.findItem(R.id.menu_list_style_grid_c).setChecked(true);
} else {
menu.findItem(R.id.menu_list_style_list).setChecked(true);
}
ThemeValue themeValue = PrefsUtils.getSelectedTheme(this);
if (themeValue == ThemeValue.LIGHT) {
menu.findItem(R.id.menu_theme_light).setChecked(true);
} else if (themeValue == ThemeValue.DARK) {
menu.findItem(R.id.menu_theme_dark).setChecked(true);
} else if (themeValue == ThemeValue.BLACK) {
menu.findItem(R.id.menu_theme_black).setChecked(true);
} else if (themeValue == ThemeValue.AUTO) {
menu.findItem(R.id.menu_theme_auto).setChecked(true);
}
if (!TextUtils.isEmpty(binding.itemlistSearchQuery.getText())) {
menu.findItem(R.id.menu_save_search).setVisible(true);
} else {
menu.findItem(R.id.menu_save_search).setVisible(false);
}
StoryContentPreviewStyle previewStyle = PrefsUtils.getStoryContentPreviewStyle(this);
if (previewStyle == StoryContentPreviewStyle.NONE) {
menu.findItem(R.id.menu_story_content_preview_none).setChecked(true);
} else if (previewStyle == StoryContentPreviewStyle.SMALL) {
menu.findItem(R.id.menu_story_content_preview_small).setChecked(true);
} else if (previewStyle == StoryContentPreviewStyle.MEDIUM) {
menu.findItem(R.id.menu_story_content_preview_medium).setChecked(true);
} else if (previewStyle == StoryContentPreviewStyle.LARGE) {
menu.findItem(R.id.menu_story_content_preview_large).setChecked(true);
}
ThumbnailStyle thumbnailStyle = PrefsUtils.getThumbnailStyle(this);
if (thumbnailStyle == ThumbnailStyle.LEFT_SMALL) {
menu.findItem(R.id.menu_story_thumbnail_left_small).setChecked(true);
} else if (thumbnailStyle == ThumbnailStyle.LEFT_LARGE) {
menu.findItem(R.id.menu_story_thumbnail_left_large).setChecked(true);
} else if (thumbnailStyle == ThumbnailStyle.RIGHT_SMALL) {
menu.findItem(R.id.menu_story_thumbnail_right_small).setChecked(true);
} else if (thumbnailStyle == ThumbnailStyle.RIGHT_LARGE) {
menu.findItem(R.id.menu_story_thumbnail_right_large).setChecked(true);
} else if (thumbnailStyle.isOff()) {
menu.findItem(R.id.menu_story_thumbnail_no_preview).setChecked(true);
}
SpacingStyle spacingStyle = PrefsUtils.getSpacingStyle(this);
if (spacingStyle == SpacingStyle.COMFORTABLE) {
menu.findItem(R.id.menu_spacing_comfortable).setChecked(true);
} else if (spacingStyle == SpacingStyle.COMPACT) {
menu.findItem(R.id.menu_spacing_compact).setChecked(true);
}
ListTextSize listTextSize = ListTextSize.fromSize(PrefsUtils.getListTextSize(this));
if (listTextSize == ListTextSize.XS) {
menu.findItem(R.id.menu_text_size_xs).setChecked(true);
} else if (listTextSize == ListTextSize.S) {
menu.findItem(R.id.menu_text_size_s).setChecked(true);
} else if (listTextSize == ListTextSize.M) {
menu.findItem(R.id.menu_text_size_m).setChecked(true);
} else if (listTextSize == ListTextSize.L) {
menu.findItem(R.id.menu_text_size_l).setChecked(true);
} else if (listTextSize == ListTextSize.XL) {
menu.findItem(R.id.menu_text_size_xl).setChecked(true);
} else if (listTextSize == ListTextSize.XXL) {
menu.findItem(R.id.menu_text_size_xxl).setChecked(true);
}
boolean isMarkReadOnScroll = PrefsUtils.isMarkReadOnFeedScroll(this);
if (isMarkReadOnScroll) {
menu.findItem(R.id.menu_mark_read_on_scroll_enabled).setChecked(true);
} else {
menu.findItem(R.id.menu_mark_read_on_scroll_disabled).setChecked(true);
}
return true;
}
@Override
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.menu_mark_all_as_read) {
feedUtils.markRead(this, fs, null, null, R.array.mark_all_read_options, true);
return true;
} else if (item.getItemId() == R.id.menu_story_order_newest) {
updateStoryOrder(StoryOrder.NEWEST);
return true;
} else if (item.getItemId() == R.id.menu_story_order_oldest) {
updateStoryOrder(StoryOrder.OLDEST);
return true;
} else if (item.getItemId() == R.id.menu_read_filter_all_stories) {
updateReadFilter(ReadFilter.ALL);
return true;
} else if (item.getItemId() == R.id.menu_read_filter_unread_only) {
updateReadFilter(ReadFilter.UNREAD);
return true;
} else if (item.getItemId() == R.id.menu_text_size_xs) {
updateTextSizeStyle(ListTextSize.XS);
return true;
} else if (item.getItemId() == R.id.menu_text_size_s) {
updateTextSizeStyle(ListTextSize.S);
return true;
} else if (item.getItemId() == R.id.menu_text_size_m) {
updateTextSizeStyle(ListTextSize.M);
return true;
} else if (item.getItemId() == R.id.menu_text_size_l) {
updateTextSizeStyle(ListTextSize.L);
return true;
} else if (item.getItemId() == R.id.menu_text_size_xl) {
updateTextSizeStyle(ListTextSize.XL);
return true;
} else if (item.getItemId() == R.id.menu_text_size_xxl) {
updateTextSizeStyle(ListTextSize.XXL);
return true;
} else if (item.getItemId() == R.id.menu_search_stories) {
if (binding.itemlistSearchQuery.getVisibility() != View.VISIBLE) {
binding.itemlistSearchQuery.setVisibility(View.VISIBLE);
binding.itemlistSearchQuery.requestFocus();
} else {
binding.itemlistSearchQuery.setVisibility(View.GONE);
checkSearchQuery();
}
} else if(item.getItemId() == R.id.menu_theme_auto) {
PrefsUtils.setSelectedTheme(this, ThemeValue.AUTO);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_theme_light) {
PrefsUtils.setSelectedTheme(this, ThemeValue.LIGHT);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_theme_dark) {
PrefsUtils.setSelectedTheme(this, ThemeValue.DARK);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_spacing_comfortable) {
updateSpacingStyle(SpacingStyle.COMFORTABLE);
} else if (item.getItemId() == R.id.menu_spacing_compact) {
updateSpacingStyle(SpacingStyle.COMPACT);
} else if (item.getItemId() == R.id.menu_list_style_list) {
PrefsUtils.updateStoryListStyle(this, fs, StoryListStyle.LIST);
itemSetFragment.updateListStyle();
} else if (item.getItemId() == R.id.menu_list_style_grid_f) {
PrefsUtils.updateStoryListStyle(this, fs, StoryListStyle.GRID_F);
itemSetFragment.updateListStyle();
} else if (item.getItemId() == R.id.menu_list_style_grid_m) {
PrefsUtils.updateStoryListStyle(this, fs, StoryListStyle.GRID_M);
itemSetFragment.updateListStyle();
} else if (item.getItemId() == R.id.menu_list_style_grid_c) {
PrefsUtils.updateStoryListStyle(this, fs, StoryListStyle.GRID_C);
itemSetFragment.updateListStyle();
} else if (item.getItemId() == R.id.menu_save_search) {
String feedId = getSaveSearchFeedId();
if (feedId != null) {
String query = binding.itemlistSearchQuery.getText().toString();
SaveSearchFragment frag = SaveSearchFragment.newInstance(feedId, query);
frag.show(getSupportFragmentManager(), SaveSearchFragment.class.getName());
}
} else if (item.getItemId() == R.id.menu_story_content_preview_none) {
PrefsUtils.setStoryContentPreviewStyle(this, StoryContentPreviewStyle.NONE);
itemSetFragment.notifyContentPrefsChanged();
} else if (item.getItemId() == R.id.menu_story_content_preview_small) {
PrefsUtils.setStoryContentPreviewStyle(this, StoryContentPreviewStyle.SMALL);
itemSetFragment.notifyContentPrefsChanged();
} else if (item.getItemId() == R.id.menu_story_content_preview_medium) {
PrefsUtils.setStoryContentPreviewStyle(this, StoryContentPreviewStyle.MEDIUM);
itemSetFragment.notifyContentPrefsChanged();
} else if (item.getItemId() == R.id.menu_story_content_preview_large) {
PrefsUtils.setStoryContentPreviewStyle(this, StoryContentPreviewStyle.LARGE);
itemSetFragment.notifyContentPrefsChanged();
} else if (item.getItemId() == R.id.menu_mark_read_on_scroll_disabled) {
PrefsUtils.setMarkReadOnScroll(this, false);
} else if (item.getItemId() == R.id.menu_mark_read_on_scroll_enabled) {
PrefsUtils.setMarkReadOnScroll(this, true);
} else if (item.getItemId() == R.id.menu_story_thumbnail_left_small) {
PrefsUtils.setThumbnailStyle(this, ThumbnailStyle.LEFT_SMALL);
itemSetFragment.updateThumbnailStyle();
} else if (item.getItemId() == R.id.menu_story_thumbnail_left_large) {
PrefsUtils.setThumbnailStyle(this, ThumbnailStyle.LEFT_LARGE);
itemSetFragment.updateThumbnailStyle();
} else if (item.getItemId() == R.id.menu_story_thumbnail_right_small) {
PrefsUtils.setThumbnailStyle(this, ThumbnailStyle.RIGHT_SMALL);
itemSetFragment.updateThumbnailStyle();
} else if (item.getItemId() == R.id.menu_story_thumbnail_right_large) {
PrefsUtils.setThumbnailStyle(this, ThumbnailStyle.RIGHT_LARGE);
itemSetFragment.updateThumbnailStyle();
} else if (item.getItemId() == R.id.menu_story_thumbnail_no_preview) {
PrefsUtils.setThumbnailStyle(this, ThumbnailStyle.OFF);
itemSetFragment.updateThumbnailStyle();
}
return false;
return contextMenuDelegate.onOptionsItemSelected(item, itemSetFragment, fs, binding.itemlistSearchQuery, getSaveSearchFeedId());
}
@Override
@ -455,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() {
if (binding.itemlistSyncStatus != null) {
String syncStatus = NBSyncService.getSyncStatusMessage(this, true);
@ -511,26 +273,6 @@ public abstract class ItemsList extends NbActivity {
transaction.commit();
}
private void updateTextSizeStyle(ListTextSize listTextSize) {
PrefsUtils.setListTextSize(this, listTextSize.getSize());
itemSetFragment.updateTextSize();
}
private void updateSpacingStyle(SpacingStyle spacingStyle) {
PrefsUtils.setSpacingStyle(this, spacingStyle);
itemSetFragment.updateSpacingStyle();
}
private void updateStoryOrder(StoryOrder storyOrder) {
PrefsUtils.updateStoryOrder(this, fs, storyOrder);
restartReadingSession();
}
private void updateReadFilter(ReadFilter readFilter) {
PrefsUtils.updateReadFilter(this, fs, readFilter);
restartReadingSession();
}
protected void restartReadingSession() {
NBSyncService.resetFetchState(fs);
feedUtils.prepareReadingSession(fs, true);
@ -553,4 +295,5 @@ public abstract class ItemsList extends NbActivity {
}
abstract String getSaveSearchFeedId();
}

View file

@ -7,19 +7,15 @@ import static com.newsblur.service.NBSyncReceiver.UPDATE_STATUS;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.appcompat.widget.PopupMenu;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnKeyListener;
@ -28,24 +24,20 @@ import android.widget.AbsListView;
import com.newsblur.R;
import com.newsblur.database.BlurDatabaseHelper;
import com.newsblur.databinding.ActivityMainBinding;
import com.newsblur.delegate.MainContextMenuDelegate;
import com.newsblur.delegate.MainContextMenuDelegateImpl;
import com.newsblur.fragment.FeedIntelligenceSelectorFragment;
import com.newsblur.fragment.FolderListFragment;
import com.newsblur.fragment.LoginAsDialogFragment;
import com.newsblur.fragment.LogoutDialogFragment;
import com.newsblur.service.BootReceiver;
import com.newsblur.service.NBSyncService;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefConstants.ThemeValue;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ShortcutUtils;
import com.newsblur.util.SpacingStyle;
import com.newsblur.util.StateFilter;
import com.newsblur.util.ListTextSize;
import com.newsblur.util.UIUtils;
import com.newsblur.view.StateToggleButton.StateChangedListener;
import com.newsblur.widget.WidgetUtils;
import javax.inject.Inject;
@ -63,10 +55,9 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
public static final String EXTRA_FORCE_SHOW_FEED_ID = "force_show_feed_id";
private FolderListFragment folderFeedList;
private FragmentManager fragmentManager;
private SwipeRefreshLayout swipeLayout;
private boolean wasSwipeEnabled = false;
private ActivityMainBinding binding;
private MainContextMenuDelegate contextMenuDelegate;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -75,6 +66,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
super.onCreate(savedInstanceState);
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
binding = ActivityMainBinding.inflate(getLayoutInflater());
contextMenuDelegate = new MainContextMenuDelegateImpl(this, dbHelper);
setContentView(binding.getRoot());
// set the status bar to an generic loading message when the activity is first created so
@ -82,12 +74,11 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
binding.mainSyncStatus.setText(R.string.loading);
binding.mainSyncStatus.setVisibility(View.VISIBLE);
swipeLayout = (SwipeRefreshLayout)findViewById(R.id.swipe_container);
swipeLayout.setColorSchemeResources(R.color.refresh_1, R.color.refresh_2, R.color.refresh_3, R.color.refresh_4);
swipeLayout.setProgressBackgroundColorSchemeResource(UIUtils.getThemedResource(this, R.attr.actionbarBackground, android.R.attr.background));
swipeLayout.setOnRefreshListener(this);
binding.swipeContainer.setColorSchemeResources(R.color.refresh_1, R.color.refresh_2, R.color.refresh_3, R.color.refresh_4);
binding.swipeContainer.setProgressBackgroundColorSchemeResource(UIUtils.getThemedResource(this, R.attr.actionbarBackground, android.R.attr.background));
binding.swipeContainer.setOnRefreshListener(this);
fragmentManager = getSupportFragmentManager();
FragmentManager fragmentManager = getSupportFragmentManager();
folderFeedList = (FolderListFragment) fragmentManager.findFragmentByTag("folderFeedListFragment");
((FeedIntelligenceSelectorFragment) fragmentManager.findFragmentByTag("feedIntelligenceSelector")).setState(folderFeedList.currentState);
@ -196,7 +187,7 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
folderFeedList.changeState(state);
}
@Override
public void handleUpdate(int updateType) {
if ((updateType & UPDATE_REBUILD) != 0) {
@ -250,23 +241,17 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
}
private void updateStatusIndicators() {
if (NBSyncService.isFeedFolderSyncRunning()) {
swipeLayout.setRefreshing(true);
} else {
swipeLayout.setRefreshing(false);
}
binding.swipeContainer.setRefreshing(NBSyncService.isFeedFolderSyncRunning());
if (binding.mainSyncStatus != null) {
String syncStatus = NBSyncService.getSyncStatusMessage(this, false);
if (syncStatus != null) {
if (AppConstants.VERBOSE_LOG) {
syncStatus = syncStatus + UIUtils.getMemoryUsageDebug(this);
}
binding.mainSyncStatus.setText(syncStatus);
binding.mainSyncStatus.setVisibility(View.VISIBLE);
} else {
binding.mainSyncStatus.setVisibility(View.GONE);
String syncStatus = NBSyncService.getSyncStatusMessage(this, false);
if (syncStatus != null) {
if (AppConstants.VERBOSE_LOG) {
syncStatus = syncStatus + UIUtils.getMemoryUsageDebug(this);
}
binding.mainSyncStatus.setText(syncStatus);
binding.mainSyncStatus.setVisibility(View.VISIBLE);
} else {
binding.mainSyncStatus.setVisibility(View.GONE);
}
}
@ -278,135 +263,12 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
}
private void onClickMenuButton() {
PopupMenu pm = new PopupMenu(this, binding.mainMenuButton);
Menu menu = pm.getMenu();
pm.getMenuInflater().inflate(R.menu.main, menu);
MenuItem loginAsItem = menu.findItem(R.id.menu_loginas);
if (NBSyncService.isStaff == Boolean.TRUE) {
loginAsItem.setVisible(true);
} else {
loginAsItem.setVisible(false);
}
ThemeValue themeValue = PrefsUtils.getSelectedTheme(this);
if (themeValue == ThemeValue.LIGHT) {
menu.findItem(R.id.menu_theme_light).setChecked(true);
} else if (themeValue == ThemeValue.DARK) {
menu.findItem(R.id.menu_theme_dark).setChecked(true);
} else if (themeValue == ThemeValue.BLACK) {
menu.findItem(R.id.menu_theme_black).setChecked(true);
} else if (themeValue == ThemeValue.AUTO) {
menu.findItem(R.id.menu_theme_auto).setChecked(true);
}
SpacingStyle spacingStyle = PrefsUtils.getSpacingStyle(this);
if (spacingStyle == SpacingStyle.COMFORTABLE) {
menu.findItem(R.id.menu_spacing_comfortable).setChecked(true);
} else if (spacingStyle == SpacingStyle.COMPACT) {
menu.findItem(R.id.menu_spacing_compact).setChecked(true);
}
ListTextSize listTextSize = ListTextSize.fromSize(PrefsUtils.getListTextSize(this));
if (listTextSize == ListTextSize.XS) {
menu.findItem(R.id.menu_text_size_xs).setChecked(true);
} else if (listTextSize == ListTextSize.S) {
menu.findItem(R.id.menu_text_size_s).setChecked(true);
} else if (listTextSize == ListTextSize.M) {
menu.findItem(R.id.menu_text_size_m).setChecked(true);
} else if (listTextSize == ListTextSize.L) {
menu.findItem(R.id.menu_text_size_l).setChecked(true);
} else if (listTextSize == ListTextSize.XL) {
menu.findItem(R.id.menu_text_size_xl).setChecked(true);
} else if (listTextSize == ListTextSize.XXL) {
menu.findItem(R.id.menu_text_size_xxl).setChecked(true);
}
menu.findItem(R.id.menu_widget).setVisible(WidgetUtils.hasActiveAppWidgets(this));
pm.setOnMenuItemClickListener(this);
pm.show();
contextMenuDelegate.onMenuClick(binding.mainMenuButton, this);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.menu_logout) {
DialogFragment newFragment = new LogoutDialogFragment();
newFragment.show(getSupportFragmentManager(), "dialog");
} else if (item.getItemId() == R.id.menu_settings) {
Intent settingsIntent = new Intent(this, Settings.class);
startActivity(settingsIntent);
return true;
} else if (item.getItemId() == R.id.menu_widget) {
Intent widgetIntent = new Intent(this, WidgetConfig.class);
startActivity(widgetIntent);
return true;
} else if (item.getItemId() == R.id.menu_feedback_email) {
PrefsUtils.sendLogEmail(this, dbHelper);
return true;
} else if (item.getItemId() == R.id.menu_feedback_post) {
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(PrefsUtils.createFeedbackLink(this, dbHelper)));
startActivity(i);
} catch (Exception e) {
Log.wtf(this.getClass().getName(), "device cannot even open URLs to report feedback");
}
return true;
} else if (item.getItemId() == R.id.menu_text_size_xs) {
folderFeedList.setListTextSize(ListTextSize.XS);
return true;
} else if (item.getItemId() == R.id.menu_text_size_s) {
folderFeedList.setListTextSize(ListTextSize.S);
return true;
} else if (item.getItemId() == R.id.menu_text_size_m) {
folderFeedList.setListTextSize(ListTextSize.M);
return true;
} else if (item.getItemId() == R.id.menu_text_size_l) {
folderFeedList.setListTextSize(ListTextSize.L);
return true;
} else if (item.getItemId() == R.id.menu_text_size_xl) {
folderFeedList.setListTextSize(ListTextSize.XL);
return true;
} else if (item.getItemId() == R.id.menu_text_size_xxl) {
folderFeedList.setListTextSize(ListTextSize.XXL);
return true;
} else if (item.getItemId() == R.id.menu_spacing_comfortable) {
folderFeedList.setSpacingStyle(SpacingStyle.COMFORTABLE);
return true;
} else if (item.getItemId() == R.id.menu_spacing_compact) {
folderFeedList.setSpacingStyle(SpacingStyle.COMPACT);
return true;
} else if (item.getItemId() == R.id.menu_loginas) {
DialogFragment newFragment = new LoginAsDialogFragment();
newFragment.show(getSupportFragmentManager(), "dialog");
return true;
} else if (item.getItemId() == R.id.menu_theme_auto) {
PrefsUtils.setSelectedTheme(this, ThemeValue.AUTO);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_theme_light) {
PrefsUtils.setSelectedTheme(this, ThemeValue.LIGHT);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_theme_dark) {
PrefsUtils.setSelectedTheme(this, ThemeValue.DARK);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(this, ThemeValue.BLACK);
UIUtils.restartActivity(this);
} else if (item.getItemId() == R.id.menu_premium_account) {
Intent intent = new Intent(this, Premium.class);
startActivity(intent);
return true;
} else if (item.getItemId() == R.id.menu_mute_sites) {
Intent intent = new Intent(this, MuteConfig.class);
startActivity(intent);
return true;
} else if (item.getItemId() == R.id.menu_import_export) {
Intent intent = new Intent(this, ImportExportActivity.class);
startActivity(intent);
return true;
}
return false;
return contextMenuDelegate.onMenuItemClick(item, folderFeedList);
}
private void onClickAddButton() {
@ -442,10 +304,10 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (swipeLayout != null) {
if (binding != null) {
boolean enable = (firstVisibleItem == 0);
if (wasSwipeEnabled != enable) {
swipeLayout.setEnabled(enable);
binding.swipeContainer.setEnabled(enable);
wasSwipeEnabled = enable;
}
}

View file

@ -0,0 +1,66 @@
package com.newsblur.activity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.newsblur.R
import com.newsblur.databinding.ActivityNotificationsBinding
import com.newsblur.di.IconLoader
import com.newsblur.domain.Feed
import com.newsblur.util.ImageLoader
import com.newsblur.util.UIUtils
import com.newsblur.util.setViewGone
import com.newsblur.util.setViewVisible
import com.newsblur.viewModel.NotificationsViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import javax.inject.Inject
@AndroidEntryPoint
class NotificationsActivity : NbActivity(), NotificationsAdapter.Listener {
@IconLoader
@Inject
lateinit var imageLoader: ImageLoader
private lateinit var binding: ActivityNotificationsBinding
private lateinit var viewModel: NotificationsViewModel
private lateinit var adapter: NotificationsAdapter
override fun onCreate(bundle: Bundle?) {
super.onCreate(bundle)
viewModel = ViewModelProvider(this)[NotificationsViewModel::class.java]
binding = ActivityNotificationsBinding.inflate(layoutInflater)
setContentView(binding.root)
setupUI()
setupListeners()
}
private fun setupUI() {
UIUtils.setupToolbar(this, R.drawable.logo, getString(R.string.notifications_title), true)
adapter = NotificationsAdapter(imageLoader, this).also {
binding.recyclerViewFeeds.adapter = it
}
}
private fun setupListeners() {
lifecycleScope.launchWhenStarted {
viewModel.feeds.collectLatest {
val feeds = it.values
if (feeds.isNotEmpty()) {
binding.recyclerViewFeeds.setViewVisible()
binding.txtNoNotifications.setViewGone()
} else {
binding.recyclerViewFeeds.setViewGone()
binding.txtNoNotifications.setViewVisible()
}
adapter.refreshFeeds(feeds)
}
}
}
override fun onFeedUpdated(feed: Feed) {
viewModel.updateFeed(this, feed)
}
}

View file

@ -0,0 +1,95 @@
package com.newsblur.activity
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.newsblur.databinding.ViewNotificationsItemBinding
import com.newsblur.domain.Feed
import com.newsblur.util.FeedExt
import com.newsblur.util.FeedExt.disableNotificationType
import com.newsblur.util.FeedExt.enableNotificationType
import com.newsblur.util.FeedExt.isNotifyAndroid
import com.newsblur.util.FeedExt.isNotifyEmail
import com.newsblur.util.FeedExt.isNotifyFocus
import com.newsblur.util.FeedExt.isNotifyIOS
import com.newsblur.util.FeedExt.isNotifyUnread
import com.newsblur.util.FeedExt.isNotifyWeb
import com.newsblur.util.FeedExt.setNotifyFocus
import com.newsblur.util.FeedExt.setNotifyUnread
import com.newsblur.util.ImageLoader
class NotificationsAdapter(
private val imageLoader: ImageLoader,
private val listener: Listener,
) : RecyclerView.Adapter<NotificationsAdapter.ViewHolder>() {
private val feeds: MutableList<Feed> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ViewNotificationsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding, imageLoader)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(feeds[position], listener)
override fun getItemCount(): Int = feeds.size
fun refreshFeeds(feeds: Collection<Feed>) {
this.feeds.clear()
this.feeds.addAll(feeds)
this.notifyItemRangeInserted(0, feeds.size)
}
class ViewHolder(val binding: ViewNotificationsItemBinding, val imageLoader: ImageLoader) : RecyclerView.ViewHolder(binding.root) {
fun bind(feed: Feed, listener: Listener) {
binding.textTitle.text = feed.title
imageLoader.displayImage(feed.faviconUrl, binding.imgIcon, binding.imgIcon.height, true)
with(binding.groupFilter) {
if (feed.isNotifyUnread()) check(binding.btnUnread.id)
else if (feed.isNotifyFocus()) check(binding.btnFocus.id)
}
with(binding.groupPlatform) {
if (feed.isNotifyEmail()) check(binding.btnEmail.id)
if (feed.isNotifyWeb()) check(binding.btnWeb.id)
if (feed.isNotifyIOS()) check(binding.btnIos.id)
if (feed.isNotifyAndroid()) check(binding.btnAndroid.id)
}
binding.groupFilter.addOnButtonCheckedListener { _, checkedId, isChecked ->
updateFilter(feed, checkedId, isChecked)
listener.onFeedUpdated(feed)
}
binding.groupPlatform.addOnButtonCheckedListener { _, checkedId, isChecked ->
updatePlatform(feed, checkedId, isChecked)
listener.onFeedUpdated(feed)
}
}
private fun updateFilter(feed: Feed, checkedBtnId: Int, isChecked: Boolean) {
when (checkedBtnId) {
binding.btnUnread.id -> if (isChecked) feed.setNotifyUnread()
binding.btnFocus.id -> if (isChecked) feed.setNotifyFocus()
}
}
private fun updatePlatform(feed: Feed, checkedBtnId: Int, isChecked: Boolean) {
when (checkedBtnId) {
binding.btnEmail.id -> FeedExt.NOTIFY_EMAIL
binding.btnWeb.id -> FeedExt.NOTIFY_WEB
binding.btnIos.id -> FeedExt.NOTIFY_IOS
binding.btnIos.id -> FeedExt.NOTIFY_ANDROID
else -> null
}?.let {
if (isChecked) feed.enableNotificationType(it)
else feed.disableNotificationType(it)
}
}
}
interface Listener {
fun onFeedUpdated(feed: Feed)
}
}

View file

@ -800,6 +800,7 @@ abstract class Reading : NbActivity(), OnPageChangeListener, ScrollChangeListene
companion object {
const val EXTRA_FEEDSET = "feed_set"
const val EXTRA_STORY_HASH = "story_hash"
const val EXTRA_STORY = "story"
private const val BUNDLE_STARTING_UNREAD = "starting_unread"
/** special value for starting story hash that jumps to the first unread. */

View file

@ -2,11 +2,22 @@ package com.newsblur.activity;
import android.os.Bundle;
import com.newsblur.di.IconLoader;
import com.newsblur.domain.SocialFeed;
import com.newsblur.util.ImageLoader;
import com.newsblur.util.UIUtils;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class SocialFeedItemsList extends ItemsList {
@Inject
@IconLoader
ImageLoader iconLoader;
public static final String EXTRA_SOCIAL_FEED = "social_feed";
private SocialFeed socialFeed;

View file

@ -8,8 +8,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.CancellationSignal;
import androidx.annotation.Nullable;
import androidx.loader.content.AsyncTaskLoader;
import androidx.loader.content.Loader;
import android.text.TextUtils;
import android.util.Log;
@ -26,7 +25,6 @@ import com.newsblur.network.domain.CommentResponse;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter;

View file

@ -1,5 +1,13 @@
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.util.ArrayList;
import java.util.Collection;
@ -35,8 +43,10 @@ import com.newsblur.domain.Folder;
import com.newsblur.domain.SavedSearch;
import com.newsblur.domain.StarredCount;
import com.newsblur.domain.SocialFeed;
import com.newsblur.util.Session;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedListOrder;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.SpacingStyle;
import com.newsblur.util.FeedSet;
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 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_groupName = 13;
private final static float defaultTextSize_count = 13;
@ -982,4 +977,7 @@ public class FolderListAdapter extends BaseExpandableListAdapter {
this.spacingStyle = spacingStyle;
}
public SessionDataSource buildSessionDataSource(Session activeSession) {
return new SessionDataSource(activeSession, activeFolderNames, activeFolderChildren);
}
}

View file

@ -423,11 +423,11 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
return true;
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;
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;
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:
FeedSet fs = FeedSet.singleFeed(story.feedId);
FeedItemsList.startActivity(context, fs,
feedUtils.getFeed(story.feedId), null);
feedUtils.getFeed(story.feedId), null, null);
return true;
default:
return false;

View file

@ -0,0 +1,312 @@
package com.newsblur.delegate
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.EditText
import androidx.core.view.isVisible
import com.newsblur.R
import com.newsblur.activity.ItemsList
import com.newsblur.fragment.ItemSetFragment
import com.newsblur.fragment.SaveSearchFragment
import com.newsblur.service.NBSyncService
import com.newsblur.util.*
import com.newsblur.util.FeedUtils.Companion.triggerSync
import com.newsblur.util.ListTextSize.Companion.fromSize
import com.newsblur.util.PrefConstants.ThemeValue
interface ItemListContextMenuDelegate {
fun onCreateMenuOptions(menu: Menu, menuInflater: MenuInflater, fs: FeedSet): Boolean
fun onPrepareMenuOptions(menu: Menu, fs: FeedSet, showSavedSearch: Boolean): Boolean
fun onOptionsItemSelected(item: MenuItem, fragment: ItemSetFragment, fs: FeedSet, searchInputView: EditText, saveSearchFeedId: String?): Boolean
}
open class ItemListContextMenuDelegateImpl(
private val activity: ItemsList,
private val feedUtils: FeedUtils,
) : ItemListContextMenuDelegate, ReadingActionListener by activity {
override fun onCreateMenuOptions(menu: Menu, menuInflater: MenuInflater, fs: FeedSet): Boolean {
menuInflater.inflate(R.menu.itemslist, menu)
if (fs.isGlobalShared ||
fs.isAllSocial ||
fs.isFilterSaved ||
fs.isAllSaved ||
fs.isSingleSavedTag ||
fs.isInfrequent ||
fs.isAllRead) {
menu.findItem(R.id.menu_mark_all_as_read).isVisible = false
}
if (fs.isGlobalShared ||
fs.isAllSocial ||
fs.isAllRead) {
menu.findItem(R.id.menu_story_order).isVisible = false
}
if (fs.isGlobalShared ||
fs.isFilterSaved ||
fs.isAllSaved ||
fs.isSingleSavedTag ||
fs.isInfrequent ||
fs.isAllRead) {
menu.findItem(R.id.menu_read_filter).isVisible = false
menu.findItem(R.id.menu_mark_read_on_scroll).isVisible = false
menu.findItem(R.id.menu_story_content_preview_style).isVisible = false
menu.findItem(R.id.menu_story_thumbnail_style).isVisible = false
}
if (fs.isGlobalShared ||
fs.isAllSocial ||
fs.isInfrequent ||
fs.isAllRead) {
menu.findItem(R.id.menu_search_stories).isVisible = false
}
if (!fs.isSingleNormal || fs.isFilterSaved) {
menu.findItem(R.id.menu_notifications).isVisible = false
menu.findItem(R.id.menu_delete_feed).isVisible = false
menu.findItem(R.id.menu_instafetch_feed).isVisible = false
menu.findItem(R.id.menu_intel).isVisible = false
menu.findItem(R.id.menu_rename_feed).isVisible = false
menu.findItem(R.id.menu_statistics).isVisible = false
}
if (!fs.isInfrequent) {
menu.findItem(R.id.menu_infrequent_cutoff).isVisible = false
}
return true
}
override fun onPrepareMenuOptions(menu: Menu, fs: FeedSet, showSavedSearch: Boolean): Boolean {
val storyOrder = PrefsUtils.getStoryOrder(activity, fs)
if (storyOrder == StoryOrder.NEWEST) {
menu.findItem(R.id.menu_story_order_newest).isChecked = true
} else if (storyOrder == StoryOrder.OLDEST) {
menu.findItem(R.id.menu_story_order_oldest).isChecked = true
}
val readFilter = PrefsUtils.getReadFilter(activity, fs)
if (readFilter == ReadFilter.ALL) {
menu.findItem(R.id.menu_read_filter_all_stories).isChecked = true
} else if (readFilter == ReadFilter.UNREAD) {
menu.findItem(R.id.menu_read_filter_unread_only).isChecked = true
}
when (PrefsUtils.getStoryListStyle(activity, fs)) {
StoryListStyle.GRID_F -> menu.findItem(R.id.menu_list_style_grid_f).isChecked = true
StoryListStyle.GRID_M -> menu.findItem(R.id.menu_list_style_grid_m).isChecked = true
StoryListStyle.GRID_C -> menu.findItem(R.id.menu_list_style_grid_c).isChecked = true
else -> menu.findItem(R.id.menu_list_style_list).isChecked = true
}
when (PrefsUtils.getSelectedTheme(activity)) {
ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true
ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true
ThemeValue.BLACK -> menu.findItem(R.id.menu_theme_black).isChecked = true
ThemeValue.AUTO -> menu.findItem(R.id.menu_theme_auto).isChecked = true
else -> Unit
}
if (showSavedSearch) {
menu.findItem(R.id.menu_save_search).isVisible = true
}
when (PrefsUtils.getStoryContentPreviewStyle(activity)) {
StoryContentPreviewStyle.NONE -> menu.findItem(R.id.menu_story_content_preview_none).isChecked = true
StoryContentPreviewStyle.SMALL -> menu.findItem(R.id.menu_story_content_preview_small).isChecked = true
StoryContentPreviewStyle.MEDIUM -> menu.findItem(R.id.menu_story_content_preview_medium).isChecked = true
StoryContentPreviewStyle.LARGE -> menu.findItem(R.id.menu_story_content_preview_large).isChecked = true
else -> Unit
}
when (PrefsUtils.getThumbnailStyle(activity)) {
ThumbnailStyle.LEFT_SMALL -> menu.findItem(R.id.menu_story_thumbnail_left_small).isChecked = true
ThumbnailStyle.LEFT_LARGE -> menu.findItem(R.id.menu_story_thumbnail_left_large).isChecked = true
ThumbnailStyle.RIGHT_SMALL -> menu.findItem(R.id.menu_story_thumbnail_right_small).isChecked = true
ThumbnailStyle.RIGHT_LARGE -> menu.findItem(R.id.menu_story_thumbnail_right_large).isChecked = true
ThumbnailStyle.OFF -> menu.findItem(R.id.menu_story_thumbnail_no_preview).isChecked = true
else -> Unit
}
val spacingStyle = PrefsUtils.getSpacingStyle(activity)
if (spacingStyle === SpacingStyle.COMFORTABLE) {
menu.findItem(R.id.menu_spacing_comfortable).isChecked = true
} else if (spacingStyle == SpacingStyle.COMPACT) {
menu.findItem(R.id.menu_spacing_compact).isChecked = true
}
when (fromSize(PrefsUtils.getListTextSize(activity))) {
ListTextSize.XS -> menu.findItem(R.id.menu_text_size_xs).isChecked = true
ListTextSize.S -> menu.findItem(R.id.menu_text_size_s).isChecked = true
ListTextSize.M -> menu.findItem(R.id.menu_text_size_m).isChecked = true
ListTextSize.L -> menu.findItem(R.id.menu_text_size_l).isChecked = true
ListTextSize.XL -> menu.findItem(R.id.menu_text_size_xl).isChecked = true
ListTextSize.XXL -> menu.findItem(R.id.menu_text_size_xxl).isChecked = true
}
val isMarkReadOnScroll = PrefsUtils.isMarkReadOnFeedScroll(activity)
if (isMarkReadOnScroll) {
menu.findItem(R.id.menu_mark_read_on_scroll_enabled).isChecked = true
} else {
menu.findItem(R.id.menu_mark_read_on_scroll_disabled).isChecked = true
}
return true
}
override fun onOptionsItemSelected(
item: MenuItem,
fragment: ItemSetFragment,
fs: FeedSet,
searchInputView: EditText,
saveSearchFeedId: String?,
): Boolean {
if (item.itemId == android.R.id.home) {
activity.finish()
return true
} else if (item.itemId == R.id.menu_mark_all_as_read) {
feedUtils.markRead(activity, fs, null, null, R.array.mark_all_read_options, this)
return true
} else if (item.itemId == R.id.menu_story_order_newest) {
updateStoryOrder(fragment, fs, StoryOrder.NEWEST)
return true
} else if (item.itemId == R.id.menu_story_order_oldest) {
updateStoryOrder(fragment, fs, StoryOrder.OLDEST)
return true
} else if (item.itemId == R.id.menu_read_filter_all_stories) {
updateReadFilter(fragment, fs, ReadFilter.ALL)
return true
} else if (item.itemId == R.id.menu_read_filter_unread_only) {
updateReadFilter(fragment, fs, ReadFilter.UNREAD)
return true
} else if (item.itemId == R.id.menu_text_size_xs) {
updateTextSizeStyle(fragment, ListTextSize.XS)
return true
} else if (item.itemId == R.id.menu_text_size_s) {
updateTextSizeStyle(fragment, ListTextSize.S)
return true
} else if (item.itemId == R.id.menu_text_size_m) {
updateTextSizeStyle(fragment, ListTextSize.M)
return true
} else if (item.itemId == R.id.menu_text_size_l) {
updateTextSizeStyle(fragment, ListTextSize.L)
return true
} else if (item.itemId == R.id.menu_text_size_xl) {
updateTextSizeStyle(fragment, ListTextSize.XL)
return true
} else if (item.itemId == R.id.menu_text_size_xxl) {
updateTextSizeStyle(fragment, ListTextSize.XXL)
return true
} else if (item.itemId == R.id.menu_search_stories) {
if (!searchInputView.isVisible) {
searchInputView.visibility = View.VISIBLE
searchInputView.requestFocus()
} else {
searchInputView.text.clear()
searchInputView.visibility = View.GONE
}
} else if (item.itemId == R.id.menu_theme_auto) {
PrefsUtils.setSelectedTheme(activity, ThemeValue.AUTO)
UIUtils.restartActivity(activity)
} else if (item.itemId == R.id.menu_theme_light) {
PrefsUtils.setSelectedTheme(activity, ThemeValue.LIGHT)
UIUtils.restartActivity(activity)
} else if (item.itemId == R.id.menu_theme_dark) {
PrefsUtils.setSelectedTheme(activity, ThemeValue.DARK)
UIUtils.restartActivity(activity)
} else if (item.itemId == R.id.menu_theme_black) {
PrefsUtils.setSelectedTheme(activity, ThemeValue.BLACK)
UIUtils.restartActivity(activity)
} else if (item.itemId == R.id.menu_spacing_comfortable) {
updateSpacingStyle(fragment, SpacingStyle.COMFORTABLE)
} else if (item.itemId == R.id.menu_spacing_compact) {
updateSpacingStyle(fragment, SpacingStyle.COMPACT)
} else if (item.itemId == R.id.menu_list_style_list) {
PrefsUtils.updateStoryListStyle(activity, fs, StoryListStyle.LIST)
fragment.updateListStyle()
} else if (item.itemId == R.id.menu_list_style_grid_f) {
PrefsUtils.updateStoryListStyle(activity, fs, StoryListStyle.GRID_F)
fragment.updateListStyle()
} else if (item.itemId == R.id.menu_list_style_grid_m) {
PrefsUtils.updateStoryListStyle(activity, fs, StoryListStyle.GRID_M)
fragment.updateListStyle()
} else if (item.itemId == R.id.menu_list_style_grid_c) {
PrefsUtils.updateStoryListStyle(activity, fs, StoryListStyle.GRID_C)
fragment.updateListStyle()
} else if (item.itemId == R.id.menu_save_search) {
saveSearchFeedId?.let {
val query: String = searchInputView.text.toString()
val frag = SaveSearchFragment.newInstance(it, query)
frag.show(activity.supportFragmentManager, SaveSearchFragment::class.java.name)
}
} else if (item.itemId == R.id.menu_story_content_preview_none) {
PrefsUtils.setStoryContentPreviewStyle(activity, StoryContentPreviewStyle.NONE)
fragment.notifyContentPrefsChanged()
} else if (item.itemId == R.id.menu_story_content_preview_small) {
PrefsUtils.setStoryContentPreviewStyle(activity, StoryContentPreviewStyle.SMALL)
fragment.notifyContentPrefsChanged()
} else if (item.itemId == R.id.menu_story_content_preview_medium) {
PrefsUtils.setStoryContentPreviewStyle(activity, StoryContentPreviewStyle.MEDIUM)
fragment.notifyContentPrefsChanged()
} else if (item.itemId == R.id.menu_story_content_preview_large) {
PrefsUtils.setStoryContentPreviewStyle(activity, StoryContentPreviewStyle.LARGE)
fragment.notifyContentPrefsChanged()
} else if (item.itemId == R.id.menu_mark_read_on_scroll_disabled) {
PrefsUtils.setMarkReadOnScroll(activity, false)
} else if (item.itemId == R.id.menu_mark_read_on_scroll_enabled) {
PrefsUtils.setMarkReadOnScroll(activity, true)
} else if (item.itemId == R.id.menu_story_thumbnail_left_small) {
PrefsUtils.setThumbnailStyle(activity, ThumbnailStyle.LEFT_SMALL)
fragment.updateThumbnailStyle()
} else if (item.itemId == R.id.menu_story_thumbnail_left_large) {
PrefsUtils.setThumbnailStyle(activity, ThumbnailStyle.LEFT_LARGE)
fragment.updateThumbnailStyle()
} else if (item.itemId == R.id.menu_story_thumbnail_right_small) {
PrefsUtils.setThumbnailStyle(activity, ThumbnailStyle.RIGHT_SMALL)
fragment.updateThumbnailStyle()
} else if (item.itemId == R.id.menu_story_thumbnail_right_large) {
PrefsUtils.setThumbnailStyle(activity, ThumbnailStyle.RIGHT_LARGE)
fragment.updateThumbnailStyle()
} else if (item.itemId == R.id.menu_story_thumbnail_no_preview) {
PrefsUtils.setThumbnailStyle(activity, ThumbnailStyle.OFF)
fragment.updateThumbnailStyle()
}
return false
}
private fun updateTextSizeStyle(fragment: ItemSetFragment, listTextSize: ListTextSize) {
PrefsUtils.setListTextSize(activity, listTextSize.size)
fragment.updateTextSize()
}
private fun updateSpacingStyle(fragment: ItemSetFragment, spacingStyle: SpacingStyle) {
PrefsUtils.setSpacingStyle(activity, spacingStyle)
fragment.updateSpacingStyle()
}
private fun updateStoryOrder(fragment: ItemSetFragment, fs: FeedSet, storyOrder: StoryOrder) {
PrefsUtils.updateStoryOrder(activity, fs, storyOrder)
restartReadingSession(fragment, fs)
}
private fun updateReadFilter(fragment: ItemSetFragment, fs: FeedSet, readFilter: ReadFilter) {
PrefsUtils.updateReadFilter(activity, fs, readFilter)
restartReadingSession(fragment, fs)
}
private fun restartReadingSession(fragment: ItemSetFragment, fs: FeedSet) {
NBSyncService.resetFetchState(fs)
feedUtils.prepareReadingSession(fs, true)
triggerSync(activity)
fragment.resetEmptyState()
fragment.hasUpdated()
fragment.scrollToTop()
}
}

View file

@ -0,0 +1,187 @@
package com.newsblur.delegate
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.DialogFragment
import com.newsblur.R
import com.newsblur.activity.*
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.fragment.FolderListFragment
import com.newsblur.fragment.LoginAsDialogFragment
import com.newsblur.fragment.LogoutDialogFragment
import com.newsblur.service.NBSyncService
import com.newsblur.util.ListTextSize
import com.newsblur.util.ListTextSize.Companion.fromSize
import com.newsblur.util.PrefConstants.ThemeValue
import com.newsblur.util.PrefsUtils
import com.newsblur.util.SpacingStyle
import com.newsblur.util.UIUtils
import com.newsblur.widget.WidgetUtils
interface MainContextMenuDelegate {
fun onMenuClick(anchor: View, listener: PopupMenu.OnMenuItemClickListener)
fun onMenuItemClick(item: MenuItem, fragment: FolderListFragment): Boolean
}
class MainContextMenuDelegateImpl(
private val activity: Main,
private val dbHelper: BlurDatabaseHelper,
) : MainContextMenuDelegate {
override fun onMenuClick(anchor: View, listener: PopupMenu.OnMenuItemClickListener) {
val pm = PopupMenu(activity, anchor)
val menu = pm.menu
pm.menuInflater.inflate(R.menu.main, menu)
if (NBSyncService.isStaff == true) {
menu.findItem(R.id.menu_loginas).isVisible = true
}
when (PrefsUtils.getSelectedTheme(activity)) {
ThemeValue.LIGHT -> menu.findItem(R.id.menu_theme_light).isChecked = true
ThemeValue.DARK -> menu.findItem(R.id.menu_theme_dark).isChecked = true
ThemeValue.BLACK -> menu.findItem(R.id.menu_theme_black).isChecked = true
ThemeValue.AUTO -> menu.findItem(R.id.menu_theme_auto).isChecked = true
else -> Unit
}
val spacingStyle = PrefsUtils.getSpacingStyle(activity)
if (spacingStyle == SpacingStyle.COMFORTABLE) {
menu.findItem(R.id.menu_spacing_comfortable).isChecked = true
} else if (spacingStyle == SpacingStyle.COMPACT) {
menu.findItem(R.id.menu_spacing_compact).isChecked = true
}
when (fromSize(PrefsUtils.getListTextSize(activity))) {
ListTextSize.XS -> menu.findItem(R.id.menu_text_size_xs).isChecked = true
ListTextSize.S -> menu.findItem(R.id.menu_text_size_s).isChecked = true
ListTextSize.M -> menu.findItem(R.id.menu_text_size_m).isChecked = true
ListTextSize.L -> menu.findItem(R.id.menu_text_size_l).isChecked = true
ListTextSize.XL -> menu.findItem(R.id.menu_text_size_xl).isChecked = true
ListTextSize.XXL -> menu.findItem(R.id.menu_text_size_xxl).isChecked = true
}
if (WidgetUtils.hasActiveAppWidgets(activity)) {
menu.findItem(R.id.menu_widget).isVisible = true
}
pm.setOnMenuItemClickListener(listener)
pm.show()
}
override fun onMenuItemClick(item: MenuItem, fragment: FolderListFragment): Boolean = when (item.itemId) {
R.id.menu_logout -> {
val newFragment: DialogFragment = LogoutDialogFragment()
newFragment.show(activity.supportFragmentManager, "dialog")
true
}
R.id.menu_settings -> {
val settingsIntent = Intent(activity, Settings::class.java)
activity.startActivity(settingsIntent)
true
}
R.id.menu_widget -> {
val widgetIntent = Intent(activity, WidgetConfig::class.java)
activity.startActivity(widgetIntent)
true
}
R.id.menu_feedback_email -> {
PrefsUtils.sendLogEmail(activity, dbHelper)
true
}
R.id.menu_feedback_post -> {
try {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(PrefsUtils.createFeedbackLink(activity, dbHelper))
activity.startActivity(i)
} catch (e: Exception) {
Log.wtf(this.javaClass.name, "device cannot even open URLs to report feedback")
}
true
}
R.id.menu_text_size_xs -> {
fragment.setListTextSize(ListTextSize.XS)
true
}
R.id.menu_text_size_s -> {
fragment.setListTextSize(ListTextSize.S)
true
}
R.id.menu_text_size_m -> {
fragment.setListTextSize(ListTextSize.M)
true
}
R.id.menu_text_size_l -> {
fragment.setListTextSize(ListTextSize.L)
true
}
R.id.menu_text_size_xl -> {
fragment.setListTextSize(ListTextSize.XL)
true
}
R.id.menu_text_size_xxl -> {
fragment.setListTextSize(ListTextSize.XXL)
true
}
R.id.menu_spacing_comfortable -> {
fragment.setSpacingStyle(SpacingStyle.COMFORTABLE)
true
}
R.id.menu_spacing_compact -> {
fragment.setSpacingStyle(SpacingStyle.COMPACT)
true
}
R.id.menu_loginas -> {
val newFragment: DialogFragment = LoginAsDialogFragment()
newFragment.show(activity.supportFragmentManager, "dialog")
true
}
R.id.menu_theme_auto -> {
PrefsUtils.setSelectedTheme(activity, ThemeValue.AUTO)
UIUtils.restartActivity(activity)
false
}
R.id.menu_theme_light -> {
PrefsUtils.setSelectedTheme(activity, ThemeValue.LIGHT)
UIUtils.restartActivity(activity)
false
}
R.id.menu_theme_dark -> {
PrefsUtils.setSelectedTheme(activity, ThemeValue.DARK)
UIUtils.restartActivity(activity)
false
}
R.id.menu_theme_black -> {
PrefsUtils.setSelectedTheme(activity, ThemeValue.BLACK)
UIUtils.restartActivity(activity)
false
}
R.id.menu_premium_account -> {
val intent = Intent(activity, Premium::class.java)
activity.startActivity(intent)
true
}
R.id.menu_mute_sites -> {
val intent = Intent(activity, MuteConfig::class.java)
activity.startActivity(intent)
true
}
R.id.menu_import_export -> {
val intent = Intent(activity, ImportExportActivity::class.java)
activity.startActivity(intent)
true
}
R.id.menu_notifications -> {
val intent = Intent(activity, NotificationsActivity::class.java)
activity.startActivity(intent)
true
}
else -> false
}
}

View file

@ -10,10 +10,6 @@ annotation class StoryFileCache
@Retention(AnnotationRetention.BINARY)
annotation class IconFileCache
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ThumbnailFileCache
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IconLoader

View file

@ -12,6 +12,7 @@ import java.util.List;
import com.google.gson.annotations.SerializedName;
import com.newsblur.database.DatabaseConstants;
import com.newsblur.util.FeedListOrder;
import com.newsblur.util.FeedUtils;
public class Feed implements Comparable<Feed>, Serializable {
@ -74,7 +75,6 @@ public class Feed implements Comparable<Feed>, Serializable {
@SerializedName("notification_types")
public List<String> notificationTypes;
// NB: only stored if notificationTypes was set to include android
@SerializedName("notification_filter")
public String notificationFilter;
@ -102,9 +102,7 @@ public class Feed implements Comparable<Feed>, Serializable {
values.put(DatabaseConstants.FEED_TITLE, title);
values.put(DatabaseConstants.FEED_UPDATED_SECONDS, lastUpdated);
values.put(DatabaseConstants.FEED_NOTIFICATION_TYPES, DatabaseConstants.flattenStringList(notificationTypes));
if (isNotifyAndroid()) {
values.put(DatabaseConstants.FEED_NOTIFICATION_FILTER, notificationFilter);
}
values.put(DatabaseConstants.FEED_NOTIFICATION_FILTER, notificationFilter);
values.put(DatabaseConstants.FEED_FETCH_PENDING, fetchPending);
return values;
}
@ -156,7 +154,7 @@ public class Feed implements Comparable<Feed>, Serializable {
public boolean equals(Object o) {
if (! (o instanceof Feed)) return false;
Feed otherFeed = (Feed) o;
return (TextUtils.equals(feedId, otherFeed.feedId));
return (FeedUtils.textUtilsEquals(feedId, otherFeed.feedId));
}
@Override
@ -168,47 +166,6 @@ public class Feed implements Comparable<Feed>, Serializable {
return title.compareToIgnoreCase(f.title);
}
private boolean isNotifyAndroid() {
if (notificationTypes == null) return false;
for (String type : notificationTypes) {
if (type.equals(NOTIFY_TYPE_ANDROID)) return true;
}
return false;
}
public void enableAndroidNotifications(boolean enable) {
if (notificationTypes == null) notificationTypes = new ArrayList<String>();
if (enable && (!notificationTypes.contains(NOTIFY_TYPE_ANDROID))) {
notificationTypes.add(NOTIFY_TYPE_ANDROID);
}
if (!enable) {
notificationTypes.remove(NOTIFY_TYPE_ANDROID);
notificationFilter = null;
}
}
public boolean isNotifyUnread() {
if (!isNotifyAndroid()) return false;
return NOTIFY_FILTER_UNREAD.equals(notificationFilter);
}
public boolean isNotifyFocus() {
if (!isNotifyAndroid()) return false;
return NOTIFY_FILTER_FOCUS.equals(notificationFilter);
}
public void setNotifyUnread() {
this.notificationFilter = NOTIFY_FILTER_UNREAD;
}
public void setNotifyFocus() {
this.notificationFilter = NOTIFY_FILTER_FOCUS;
}
private static final String NOTIFY_TYPE_ANDROID = "android";
public static final String NOTIFY_FILTER_UNREAD = "unread";
public static final String NOTIFY_FILTER_FOCUS = "focus";
public static Comparator<Feed> getFeedListOrderComparator(FeedListOrder feedListOrder) {
return (o1, o2) -> {
if (feedListOrder == FeedListOrder.MOST_USED_AT_TOP) {
@ -218,4 +175,7 @@ public class Feed implements Comparable<Feed>, Serializable {
}
};
}
public static String NOTIFY_FILTER_UNREAD = "unread";
public static String NOTIFY_FILTER_FOCUS = "focus";
}

View file

@ -18,4 +18,13 @@ data class FeedResult(
val faviconUrl: String
get() = "${APIConstants.buildUrl(APIConstants.PATH_FEED_FAVICON_URL)}$id"
companion object {
fun createFeedResultForUrl(url: String) = FeedResult(
id = -1,
tagline = "Add feed manually by URL",
label = url,
url = url
)
}
}

View file

@ -51,7 +51,10 @@ import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder;
import com.newsblur.domain.SavedSearch;
import com.newsblur.domain.SocialFeed;
import com.newsblur.util.Session;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedExt;
import com.newsblur.util.SessionDataSource;
import com.newsblur.util.SpacingStyle;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
@ -277,11 +280,11 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
} else {
menu.removeItem(R.id.menu_mute_feed);
}
if (feed.isNotifyUnread()) {
if (FeedExt.isAndroidNotifyUnread(feed)) {
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
menu.findItem(R.id.menu_notifications_unread).setChecked(true);
menu.findItem(R.id.menu_notifications_focus).setChecked(false);
} else if (feed.isNotifyFocus()) {
} else if (FeedExt.isAndroidNotifyFocus(feed)) {
menu.findItem(R.id.menu_notifications_disable).setChecked(false);
menu.findItem(R.id.menu_notifications_unread).setChecked(false);
menu.findItem(R.id.menu_notifications_focus).setChecked(true);
@ -391,7 +394,7 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
}
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.lastFolderViewed = fs.getFolderName();
}
@ -436,7 +439,13 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
@Override
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 (currentState == StateFilter.SAVED) {
// the existence of this row in saved mode is something of a framework artifact and may
@ -455,17 +464,15 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
i = new Intent(getActivity(), ReadStoriesItemsList.class);
} else if (adapter.isRowSavedStories(groupPosition)) {
i = new Intent(getActivity(), SavedStoriesItemsList.class);
} else if (adapter.isRowSavedSearches(groupPosition)) {
// group not clickable
return true;
} else {
i = new Intent(getActivity(), FolderItemsList.class);
String canonicalFolderName = adapter.getGroupFolderName(groupPosition);
SessionDataSource sessionDataSource = getSessionData(fs, canonicalFolderName, null);
i.putExtra(FolderItemsList.EXTRA_FOLDER_NAME, canonicalFolderName);
i.putExtra(ItemsList.EXTRA_SESSION_DATA, sessionDataSource);
adapter.lastFeedViewedId = null;
adapter.lastFolderViewed = canonicalFolderName;
}
FeedSet fs = adapter.getGroup(groupPosition);
i.putExtra(ItemsList.EXTRA_FEED_SET, fs);
startActivity(i);
@ -540,7 +547,8 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
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.lastFolderViewed = null;
}
@ -618,4 +626,13 @@ public class FolderListFragment extends NbFragment implements OnCreateContextMen
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;
}
}

View file

@ -1,15 +1,18 @@
package com.newsblur.fragment;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.ReadingAction;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
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 dagger.hilt.android.AndroidEntryPoint;
@ -24,39 +27,40 @@ public class ReadingActionConfirmationFragment extends DialogFragment {
private static final String DIALOG_TITLE = "dialog_title";
private static final String DIALOG_MESSAGE = "dialog_message";
private static final String DIALOG_CHOICES_RID = "dialog_choices_rid";
private static final String FINISH_AFTER = "finish_after";
public static ReadingActionConfirmationFragment newInstance(ReadingAction ra, CharSequence title, CharSequence message, int choicesId, boolean finishAfter) {
private static final String ACTION_CALLBACK = "action_callback";
public static ReadingActionConfirmationFragment newInstance(ReadingAction ra, CharSequence title, CharSequence message, int choicesId, @Nullable ReadingActionListener callback) {
ReadingActionConfirmationFragment fragment = new ReadingActionConfirmationFragment();
Bundle args = new Bundle();
args.putSerializable(READING_ACTION, ra);
args.putCharSequence(DIALOG_TITLE, title);
args.putCharSequence(DIALOG_MESSAGE, message);
args.putInt(DIALOG_CHOICES_RID, choicesId);
args.putBoolean(FINISH_AFTER, finishAfter);
args.putSerializable(ACTION_CALLBACK, callback);
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
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 message = getArguments().getCharSequence(DIALOG_MESSAGE);
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);
// NB: setting a message will override the display of the buttons, making the dialogue a no-op
if (message != null) builder.setMessage(message);
builder.setItems(choicesId, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
feedUtils.doAction(ra, getActivity());
if (finishAfter) {
getActivity().finish();
feedUtils.doAction(ra, requireContext());
if (callback != null) {
callback.onReadingActionCompleted();
}
}
}

View file

@ -419,7 +419,7 @@ class ReadingItemFragment : NbFragment(), PopupMenu.OnMenuItemClickListener {
true
}
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
}
else -> {

View file

@ -570,6 +570,7 @@ public class APIManager {
return response.getResponse(gson, AddFeedResponse.class);
}
@Nullable
public FeedResult[] searchForFeed(String searchTerm) {
ContentValues values = new ContentValues();
values.put(APIConstants.PARAMETER_FEED_SEARCH_TERM, searchTerm);

View file

@ -95,4 +95,19 @@ public class AppConstants {
// Free standard account sites limit
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";
}

View file

@ -0,0 +1,54 @@
package com.newsblur.util
import com.newsblur.domain.Feed
object FeedExt {
fun Feed.isNotifyEmail(): Boolean = isNotify(NOTIFY_EMAIL)
fun Feed.isNotifyWeb(): Boolean = isNotify(NOTIFY_WEB)
fun Feed.isNotifyIOS(): Boolean = isNotify(NOTIFY_IOS)
fun Feed.isNotifyAndroid(): Boolean = isNotify(NOTIFY_ANDROID)
fun Feed.enableNotificationType(type: String) {
if (notificationTypes == null) notificationTypes = mutableListOf()
if (!notificationTypes.contains(type)) notificationTypes.add(type)
}
fun Feed.disableNotificationType(type: String) {
notificationTypes?.remove(type)
}
fun Feed.disableNotification() {
notificationFilter = null
}
@JvmStatic
fun Feed.isAndroidNotifyUnread(): Boolean = isNotifyUnread() && isNotifyAndroid()
@JvmStatic
fun Feed.isAndroidNotifyFocus(): Boolean = isNotifyFocus() && isNotifyAndroid()
@JvmStatic
fun Feed.isNotifyUnread(): Boolean = notificationFilter == Feed.NOTIFY_FILTER_UNREAD
@JvmStatic
fun Feed.isNotifyFocus(): Boolean = notificationFilter == Feed.NOTIFY_FILTER_FOCUS
fun Feed.setNotifyFocus() {
notificationFilter = Feed.NOTIFY_FILTER_FOCUS
}
fun Feed.setNotifyUnread() {
notificationFilter = Feed.NOTIFY_FILTER_UNREAD
}
private fun Feed.isNotify(type: String): Boolean = notificationTypes?.contains(type) ?: false
const val NOTIFY_EMAIL = "email"
const val NOTIFY_WEB = "web"
const val NOTIFY_IOS = "ios"
const val NOTIFY_ANDROID = "android"
}

View file

@ -330,8 +330,8 @@ public class FeedSet implements Serializable {
if (!( o instanceof FeedSet)) return false;
FeedSet s = (FeedSet) o;
if ( !TextUtils.equals(searchQuery, s.searchQuery)) return false;
if ( !TextUtils.equals(folderName, s.folderName)) return false;
if ( !FeedUtils.textUtilsEquals(searchQuery, s.searchQuery)) return false;
if ( !FeedUtils.textUtilsEquals(folderName, s.folderName)) return false;
if ( isFilterSaved != s.isFilterSaved ) return false;
if ( (feeds != null) && (s.feeds != null) && s.feeds.equals(feeds) ) return true;
if ( (socialFeeds != null) && (s.socialFeeds != null) && s.socialFeeds.equals(socialFeeds) ) return true;

View file

@ -7,17 +7,20 @@ import android.text.TextUtils
import com.newsblur.R
import com.newsblur.activity.NbActivity
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.domain.*
import com.newsblur.domain.Classifier
import com.newsblur.domain.Feed
import com.newsblur.domain.Story
import com.newsblur.fragment.ReadingActionConfirmationFragment
import com.newsblur.network.APIConstants
import com.newsblur.network.APIManager
import com.newsblur.service.NBSyncService
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_METADATA
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_SOCIAL
import com.newsblur.service.NBSyncReceiver.Companion.UPDATE_STORY
import com.newsblur.service.NBSyncService
import com.newsblur.util.FeedExt.disableNotification
import com.newsblur.util.FeedExt.setNotifyFocus
import com.newsblur.util.FeedExt.setNotifyUnread
import com.newsblur.util.UIUtils.syncUpdateStatus
import java.lang.IllegalStateException
import java.util.*
class FeedUtils(
private val dbHelper: BlurDatabaseHelper,
@ -207,10 +210,13 @@ class FeedUtils(
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.
*/
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)) {
// the mark-all-read API doesn't support range bounding, so we need to pass each and every
// feed ID to the API instead.
@ -249,9 +255,7 @@ class FeedUtils(
}
if (doImmediate) {
doAction(ra, activity)
if (finishAfter) {
activity.finish()
}
callback?.onReadingActionCompleted()
} else {
val title: String? = when {
fs.isAllNormal -> {
@ -267,32 +271,29 @@ class FeedUtils(
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")
}
}
fun disableNotifications(context: Context, feed: Feed) {
updateFeedNotifications(context, feed, enable = false, focusOnly = false)
feed.disableNotification()
updateFeedNotifications(context, feed)
}
fun enableUnreadNotifications(context: Context, feed: Feed) {
updateFeedNotifications(context, feed, enable = true, focusOnly = false)
feed.setNotifyUnread()
updateFeedNotifications(context, feed)
}
fun enableFocusNotifications(context: Context, feed: Feed) {
updateFeedNotifications(context, feed, enable = true, focusOnly = true)
feed.setNotifyFocus()
updateFeedNotifications(context, feed)
}
private fun updateFeedNotifications(context: Context, feed: Feed, enable: Boolean, focusOnly: Boolean) {
fun updateFeedNotifications(context: Context, feed: Feed) {
NBScope.executeAsyncTask(
doInBackground = {
if (focusOnly) {
feed.setNotifyFocus()
} else {
feed.setNotifyUnread()
}
feed.enableAndroidNotifications(enable)
dbHelper.updateFeed(feed)
val ra = ReadingAction.setNotify(feed.feedId, feed.notificationTypes, feed.notificationFilter)
doAction(ra, context)
@ -508,5 +509,23 @@ class FeedUtils(
val parts = TextUtils.split(storyHash, ":")
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
}
}
}

View file

@ -128,6 +128,10 @@ public class NotificationUtils {
markreadIntent.putExtra(Reading.EXTRA_STORY_HASH, story.storyHash);
PendingIntent markreadPendingIntent = PendingIntentUtils.getImmutableBroadcast(context.getApplicationContext(), story.hashCode(), markreadIntent, 0);
Intent shareIntent = new Intent(context, NotifyShareReceiver.class);
shareIntent.putExtra(Reading.EXTRA_STORY, story);
PendingIntent sharePendingIntent = PendingIntentUtils.getImmutableBroadcast(context.getApplicationContext(), story.hashCode(), shareIntent, 0);
String feedTitle = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
StringBuilder title = new StringBuilder();
title.append(feedTitle).append(": ").append(story.title);
@ -144,8 +148,9 @@ public class NotificationUtils {
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.setWhen(story.timestamp)
.addAction(0, "Save", savePendingIntent)
.addAction(0, "Mark Read", markreadPendingIntent)
.addAction(0, "Save", savePendingIntent)
.addAction(0, "Share", sharePendingIntent)
.setColor(NOTIFY_COLOUR);
if (feedIcon != null) {
nb.setLargeIcon(feedIcon);

View file

@ -0,0 +1,33 @@
package com.newsblur.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.newsblur.activity.Reading
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.domain.Story
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class NotifyShareReceiver : BroadcastReceiver() {
@Inject
lateinit var feedUtils: FeedUtils
@Inject
lateinit var dbHelper: BlurDatabaseHelper
override fun onReceive(context: Context, intent: Intent) {
val story = intent.getSerializableExtra(Reading.EXTRA_STORY) as? Story?
NotificationUtils.cancel(context, story?.storyHash.hashCode())
story?.let {
NBScope.executeAsyncTask(
doInBackground = {
dbHelper.putStoryDismissed(it.storyHash)
feedUtils.shareStory(it, "", it.sourceUserId, context)
}
)
}
}
}

View file

@ -123,4 +123,5 @@ public class PrefConstants {
public static final String FEED_CHOOSER_FOLDER_VIEW = "feed_chooser_folder_view";
public static final String WIDGET_BACKGROUND = "widget_background";
public static final String IN_APP_REVIEW = "in_app_review";
public static final String LOAD_NEXT_ON_MARK_READ = "load_next_on_mark_read";
}

View file

@ -1064,4 +1064,9 @@ public class PrefsUtils {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
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);
}
}

View file

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

View file

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

View file

@ -0,0 +1,66 @@
package com.newsblur.viewModel
import android.content.Context
import android.database.Cursor
import android.os.CancellationSignal
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.newsblur.database.BlurDatabaseHelper
import com.newsblur.domain.Feed
import com.newsblur.util.FeedUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NotificationsViewModel
@Inject constructor(
private val dbHelper: BlurDatabaseHelper,
private val feedUtils: FeedUtils,
) : ViewModel() {
private val cancellationSignal = CancellationSignal()
private val _feeds = MutableStateFlow<Map<String, Feed>>(emptyMap())
val feeds: StateFlow<Map<String, Feed>> = _feeds.asStateFlow()
init {
loadFeeds()
}
fun updateFeed(context: Context, feed: Feed) {
viewModelScope.launch(Dispatchers.IO) {
feedUtils.updateFeedNotifications(context, feed)
}
}
private fun loadFeeds() {
viewModelScope.launch(Dispatchers.IO) {
val cursor = dbHelper.getFeedsCursor(cancellationSignal)
val feeds = extractFeeds(cursor).filterValues(notificationFeedFilter)
_feeds.emit(feeds)
}
}
private fun extractFeeds(cursor: Cursor): Map<String, Feed> = buildMap {
if (!cursor.isBeforeFirst) return@buildMap
while (cursor.moveToNext()) {
val feed = Feed.fromCursor(cursor)
this[feed.feedId] = feed
}
}
private val notificationFeedFilter: (Feed) -> Boolean = {
it.active && !it.notificationFilter.isNullOrBlank()
}
override fun onCleared() {
cancellationSignal.cancel()
super.onCleared()
}
}

View file

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

View file

@ -683,13 +683,14 @@ static NSArray<NSString *> *NewsBlurTopSectionNames;
activityButton.accessibilityLabel = @"Activities";
[activityButton setImage:activityImage forState:UIControlStateNormal];
activityButton.tintColor = UIColorFromRGB(0x8F918B);
[activityButton setImageEdgeInsets:UIEdgeInsetsMake(4, 12, 4, -12)];
[activityButton setImageEdgeInsets:UIEdgeInsetsMake(4, 0, 4, 0)];
[activityButton addTarget:self
action:@selector(showInteractionsPopover:)
forControlEvents:UIControlEventTouchUpInside];
activitiesButton = [[UIBarButtonItem alloc]
initWithCustomView:activityButton];
activitiesButton.width = 32;
// activityButton.backgroundColor = UIColor.redColor;
self.navigationItem.rightBarButtonItem = activitiesButton;
NSMutableDictionary *sortedFolders = [[NSMutableDictionary alloc] init];
@ -2765,7 +2766,7 @@ heightForHeaderInSection:(NSInteger)section {
userAvatarButton.pointerInteractionEnabled = YES;
userAvatarButton.accessibilityLabel = @"User info";
userAvatarButton.accessibilityHint = @"Double-tap for information about your account.";
UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 10, 0);
UIEdgeInsets insets = UIEdgeInsetsMake(0, -10, 10, 0);
userAvatarButton.contentEdgeInsets = insets;
NSMutableURLRequest *avatarRequest = [NSMutableURLRequest requestWithURL:imageURL];
@ -2785,7 +2786,7 @@ heightForHeaderInSection:(NSInteger)section {
[userInfoView addSubview:userAvatarButton];
userLabel = [[UILabel alloc] initWithFrame:CGRectMake(54, yOffset, userInfoView.frame.size.width, 16)];
userLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, yOffset, userInfoView.frame.size.width, 16)];
userLabel.text = appDelegate.activeUsername;
userLabel.font = userLabelFont;
userLabel.textColor = UIColorFromRGB(0x404040);
@ -2817,6 +2818,8 @@ heightForHeaderInSection:(NSInteger)section {
[userInfoView sizeToFit];
// userInfoView.backgroundColor = UIColor.blueColor;
self.navigationItem.titleView = userInfoView;
}

View file

@ -2,24 +2,15 @@
<svg width="150px" height="150px" viewBox="0 0 150 150" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>dialog-notifications</title>
<defs>
<polygon id="path-1" points="0 0 90 0 90 150 0 150"></polygon>
<polygon id="path-3" points="90 0 0 84.8333333 39.0265487 84.8333333 17.7212389 150 90 72 51.7699115 72"></polygon>
<polygon id="path-1" points="76 0 0 84.8333333 32.9557522 84.8333333 14.9646018 150 76 72 43.7168142 72"></polygon>
</defs>
<g id="dialog-notifications" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="noun-lightning-bolt-77533-95968E" transform="translate(23.000000, 0.000000)">
<g id="noun-lightning-bolt-77533-95968E" transform="translate(37.000000, 0.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<g id="Path"></g>
<g id="Clipped" mask="url(#mask-2)">
<g transform="translate(0.000000, 0.000000)">
<mask id="mask-4" fill="white">
<use xlink:href="#path-3"></use>
</mask>
<g id="Path" stroke="none" fill="none"></g>
<polygon id="Path" stroke="none" fill="#95958E" fill-rule="evenodd" mask="url(#mask-4)" points="-3.78318584 -3.00027778 93.5840708 -3.00027778 93.5840708 153.166389 -3.78318584 153.166389"></polygon>
</g>
</g>
<polygon id="Path" fill="#95958E" mask="url(#mask-2)" points="-3.19469027 -3.00027778 79.0265487 -3.00027778 79.0265487 153.166389 -3.19469027 153.166389"></polygon>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.