mirror of https://github.com/readrops/Readrops.git
Remove old app module
Time to say goodbye, see you old friend
This commit is contained in:
parent
8aac6e4bf4
commit
4388615a59
|
@ -1 +0,0 @@
|
|||
/build
|
118
app/build.gradle
118
app/build.gradle
|
@ -1,118 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.readrops.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
versionCode 14
|
||||
versionName "1.3.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
|
||||
testCoverageEnabled true
|
||||
applicationIdSuffix ".debug"
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.0"
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
|
||||
namespace 'com.readrops.app'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(':api')
|
||||
implementation project(':db')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.palette:palette-ktx:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.1.1'
|
||||
implementation "androidx.work:work-runtime-ktx:2.8.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.5"
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
|
||||
implementation(libs.bundles.koin)
|
||||
testImplementation(libs.bundles.kointest)
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.12.0'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
|
||||
implementation('com.github.bumptech.glide:recyclerview-integration:4.12.0') {
|
||||
transitive = false
|
||||
}
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
kapt 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
|
||||
|
||||
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
|
||||
|
||||
implementation 'com.mikepenz:fastadapter:3.2.9'
|
||||
implementation 'com.mikepenz:fastadapter-commons:3.3.0'
|
||||
implementation 'com.mikepenz:materialdrawer:6.1.2'
|
||||
implementation "com.mikepenz:aboutlibraries:6.2.3"
|
||||
implementation "com.mikepenz:iconics-views:3.2.5"
|
||||
implementation "com.mikepenz:iconics-core:3.2.5"
|
||||
|
||||
debugImplementation 'com.facebook.flipper:flipper:0.96.1'
|
||||
debugImplementation 'com.facebook.soloader:soloader:0.10.1'
|
||||
debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.96.1'
|
||||
|
||||
implementation(libs.bundles.room)
|
||||
implementation(libs.bundles.paging)
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontwarn org.xmlpull.v1.XmlPullParser
|
||||
-dontwarn org.xmlpull.v1.XmlSerializer
|
||||
-keep class org.xmlpull.v1.* {*;}
|
||||
|
||||
-keep class org.simpleframework.xml.** { *; }
|
||||
|
||||
-keep class com.readrops.api.services.freshrss.json.** { *; }
|
||||
-keep class com.readrops.api.services.nextcloudnews.json.** { *; }
|
||||
|
||||
-keep class com.readrops.api.localfeed.** { *; }
|
||||
|
||||
-keep class com.readrops.api.opml.model.** { *; }
|
||||
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn javax.xml.stream.Location
|
||||
-dontwarn javax.xml.stream.XMLInputFactory
|
||||
-dontwarn javax.xml.stream.XMLStreamReader
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.joda.convert.FromString
|
||||
-dontwarn org.joda.convert.ToString
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
|
@ -1,306 +0,0 @@
|
|||
package com.readrops.app
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.readrops.app.notifications.sync.SyncResultAnalyser
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import com.readrops.api.services.SyncResult
|
||||
import org.joda.time.LocalDateTime
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SyncResultAnalyserTest {
|
||||
|
||||
private lateinit var database: Database
|
||||
|
||||
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val account1 = Account().apply {
|
||||
accountName = "test account 1"
|
||||
accountType = AccountType.FRESHRSS
|
||||
isNotificationsEnabled = true
|
||||
}
|
||||
|
||||
private val account2 = Account().apply {
|
||||
accountName = "test account 2"
|
||||
accountType = AccountType.NEXTCLOUD_NEWS
|
||||
isNotificationsEnabled = false
|
||||
}
|
||||
|
||||
private val account3 = Account().apply {
|
||||
accountName = "test account 3"
|
||||
accountType = AccountType.LOCAL
|
||||
isNotificationsEnabled = true
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setupDb() {
|
||||
database = Room.inMemoryDatabaseBuilder(context, Database::class.java)
|
||||
.build()
|
||||
|
||||
var account1Id = 0
|
||||
database.accountDao().insert(account1).subscribe { id -> account1Id = id.toInt() }
|
||||
account1.id = account1Id
|
||||
|
||||
var account2Id = 0
|
||||
database.accountDao().insert(account2).subscribe { id -> account2Id = id.toInt() }
|
||||
account2.id = account2Id
|
||||
|
||||
var account3Id = 0
|
||||
database.accountDao().insert(account3).subscribe { id -> account3Id = id.toInt() }
|
||||
account3.id = account3Id
|
||||
|
||||
val accountIds = listOf(account1Id, account2Id, account3Id)
|
||||
for (i in 0..2) {
|
||||
val feed = Feed().apply {
|
||||
name = "feed ${i + 1}"
|
||||
iconUrl = "https://i0.wp.com/mrmondialisation.org/wp-content/uploads/2017/05/ico_final.gif"
|
||||
this.accountId = accountIds.find { it == (i + 1) }!!
|
||||
isNotificationEnabled = i % 2 == 0
|
||||
}
|
||||
|
||||
database.feedDao().insert(feed).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOneElementEveryWhere() {
|
||||
val item = Item().apply {
|
||||
title = "caseOneElementEveryWhere"
|
||||
feedId = 1
|
||||
remoteId = "item 1"
|
||||
pubDate = LocalDateTime.now()
|
||||
}
|
||||
|
||||
database.itemDao()
|
||||
.insert(item)
|
||||
.subscribe()
|
||||
|
||||
val syncResult = SyncResult().apply { items = mutableListOf(item) }
|
||||
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
|
||||
|
||||
assertEquals("caseOneElementEveryWhere", notifContent.content)
|
||||
assertEquals("feed 1", notifContent.title)
|
||||
assertTrue(notifContent.largeIcon != null)
|
||||
assertTrue(notifContent.accountId!! > 0)
|
||||
|
||||
database.itemDao()
|
||||
.delete(item)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoItemsOneFeed() {
|
||||
val item = Item().apply {
|
||||
title = "caseTwoItemsOneFeed"
|
||||
feedId = 1
|
||||
}
|
||||
|
||||
val syncResult = SyncResult().apply { items = listOf(item, item, item) }
|
||||
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
|
||||
|
||||
assertEquals(context.getString(R.string.new_items, 3), notifContent.content)
|
||||
assertEquals("feed 1", notifContent.title)
|
||||
assertTrue(notifContent.largeIcon != null)
|
||||
assertTrue(notifContent.accountId!! > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleFeeds() {
|
||||
val item = Item().apply { feedId = 1 }
|
||||
val item2 = Item().apply { feedId = 3 }
|
||||
|
||||
val syncResult = SyncResult().apply { items = listOf(item, item2) }
|
||||
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
|
||||
|
||||
assertEquals(context.getString(R.string.new_items, 2), notifContent.content)
|
||||
assertEquals(account1.accountName, notifContent.title)
|
||||
assertTrue(notifContent.largeIcon != null)
|
||||
assertTrue(notifContent.accountId!! > 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleAccounts() {
|
||||
val item = Item().apply { feedId = 1 }
|
||||
val item2 = Item().apply { feedId = 3 }
|
||||
|
||||
val syncResult = SyncResult().apply { items = listOf(item, item2) }
|
||||
val syncResult2 = SyncResult().apply { items = listOf(item, item2) }
|
||||
|
||||
val syncResults = mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, syncResult)
|
||||
put(account3, syncResult2)
|
||||
}
|
||||
|
||||
val notifContent = SyncResultAnalyser(context, syncResults, database).getSyncNotifContent()
|
||||
|
||||
assertEquals(context.getString(R.string.new_items, 4), notifContent.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAccountNotificationsDisabled() {
|
||||
val item1 = Item().apply {
|
||||
title = "testAccountNotificationsDisabled"
|
||||
feedId = 1
|
||||
}
|
||||
|
||||
val item2 = Item().apply {
|
||||
title = "testAccountNotificationsDisabled2"
|
||||
feedId = 1
|
||||
}
|
||||
|
||||
val syncResult = SyncResult().apply { items = listOf(item1, item2) }
|
||||
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account2, syncResult)), database).getSyncNotifContent()
|
||||
|
||||
assert(notifContent.title == null)
|
||||
assert(notifContent.content == null)
|
||||
assert(notifContent.largeIcon == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFeedNotificationsDisabled() {
|
||||
val item1 = Item().apply {
|
||||
title = "testAccountNotificationsDisabled"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
val item2 = Item().apply {
|
||||
title = "testAccountNotificationsDisabled2"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
val syncResult = SyncResult().apply { items = listOf(item1, item2) }
|
||||
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
|
||||
|
||||
assert(notifContent.title == null)
|
||||
assert(notifContent.content == null)
|
||||
assert(notifContent.largeIcon == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoAccountsWithOneAccountNotificationsEnabled() {
|
||||
val item1 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled"
|
||||
feedId = 1
|
||||
remoteId = "remoteId 1"
|
||||
pubDate = LocalDateTime.now()
|
||||
}
|
||||
|
||||
val item2 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled2"
|
||||
feedId = 3
|
||||
}
|
||||
|
||||
val item3 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled3"
|
||||
feedId = 3
|
||||
}
|
||||
|
||||
database.itemDao().insert(item1).subscribe()
|
||||
|
||||
val syncResult1 = SyncResult().apply { items = listOf(item1) }
|
||||
val syncResult2 = SyncResult().apply { items = listOf(item2, item3) }
|
||||
|
||||
val syncResults = mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, syncResult1)
|
||||
put(account2, syncResult2)
|
||||
}
|
||||
|
||||
val notifContent = SyncResultAnalyser(context, syncResults, database).getSyncNotifContent()
|
||||
|
||||
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notifContent.content)
|
||||
assertEquals("feed 1", notifContent.title)
|
||||
assertTrue(notifContent.largeIcon != null)
|
||||
assertTrue(notifContent.item != null)
|
||||
|
||||
database.itemDao().delete(item1).subscribe()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoAccountsWithOneFeedNotificationEnabled() {
|
||||
val item1 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled"
|
||||
feedId = 1
|
||||
remoteId = "remoteId 1"
|
||||
pubDate = LocalDateTime.now()
|
||||
}
|
||||
|
||||
val item2 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled2"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
val item3 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled3"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
database.itemDao().insert(item1).subscribe()
|
||||
|
||||
val syncResult1 = SyncResult().apply { items = listOf(item1) }
|
||||
val syncResult2 = SyncResult().apply { items = listOf(item2, item3) }
|
||||
|
||||
val syncResults = mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, syncResult1)
|
||||
put(account2, syncResult2)
|
||||
}
|
||||
|
||||
val notifContent = SyncResultAnalyser(context, syncResults, database).getSyncNotifContent()
|
||||
|
||||
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notifContent.content)
|
||||
assertEquals("feed 1", notifContent.title)
|
||||
assertTrue(notifContent.largeIcon != null)
|
||||
assertTrue(notifContent.item != null)
|
||||
|
||||
database.itemDao().delete(item1).subscribe()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testOneAccountTwoFeedsWithOneFeedNotificationEnabled() {
|
||||
val item1 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled"
|
||||
feedId = 1
|
||||
remoteId = "remoteId 1"
|
||||
pubDate = LocalDateTime.now()
|
||||
}
|
||||
|
||||
val item2 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled2"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
val item3 = Item().apply {
|
||||
title = "testTwoAccountsWithOneAccountNotificationsEnabled3"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
database.itemDao().insert(item1).subscribe()
|
||||
|
||||
val syncResult = SyncResult().apply { items = listOf(item1, item2, item3) }
|
||||
val notifContent = SyncResultAnalyser(context, mapOf(Pair(account1, syncResult)), database).getSyncNotifContent()
|
||||
|
||||
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notifContent.content)
|
||||
assertEquals("feed 1", notifContent.title)
|
||||
assertTrue(notifContent.largeIcon != null)
|
||||
assertTrue(notifContent.item != null)
|
||||
assertTrue(notifContent.accountId!! > 0)
|
||||
|
||||
database.itemDao().delete(item1).subscribe()
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".ReadropsDebugApp"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning"
|
||||
tools:replace="android:name">
|
||||
|
||||
<meta-data android:name="com.niddler.icon" android:value="android"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
tools:node="remove"
|
||||
android:exported="false" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,55 +0,0 @@
|
|||
package com.readrops.app;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Configuration;
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provider {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, false);
|
||||
|
||||
//initFlipper();
|
||||
}
|
||||
|
||||
private void initFlipper() {
|
||||
if (FlipperUtils.shouldEnableFlipper(this)) {
|
||||
FlipperClient client = AndroidFlipperClient.getInstance(this);
|
||||
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
|
||||
|
||||
NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin();
|
||||
client.addPlugin(networkPlugin);
|
||||
|
||||
client.addPlugin(new DatabasesFlipperPlugin(this));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
client.addPlugin(NavigationFlipperPlugin.getInstance());
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
|
||||
|
||||
client.start();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Configuration getWorkManagerConfiguration() {
|
||||
return new Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">ReadropsDebug</string>
|
||||
</resources>
|
|
@ -1,90 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<application
|
||||
android:name=".ReadropsApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute">
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name=".notifications.NotificationPermissionActivity"
|
||||
android:theme="@style/AppTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".item.WebViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<service android:name=".utils.feedscolors.FeedsColorsIntentService" />
|
||||
|
||||
<receiver android:name=".notifications.sync.SyncWorker$MarkReadReceiver" />
|
||||
<receiver android:name=".notifications.sync.SyncWorker$ReadLaterReceiver" />
|
||||
|
||||
<activity android:name=".settings.SettingsActivity" />
|
||||
|
||||
<activity android:name=".account.AccountTypeListActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".account.AddAccountActivity"
|
||||
android:label="@string/add_account" />
|
||||
<activity
|
||||
android:name=".feedsfolders.ManageFeedsFoldersActivity"
|
||||
android:label="@string/manage_feeds_folders"
|
||||
android:parentActivityName=".itemslist.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".itemslist.MainActivity"
|
||||
android:label="@string/articles"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/SplashTheme"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".item.ItemActivity"
|
||||
android:parentActivityName=".itemslist.MainActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".addfeed.AddFeedActivity"
|
||||
android:label="@string/add_feed_title"
|
||||
android:parentActivityName=".itemslist.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter android:label="@string/new_feed">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,75 +0,0 @@
|
|||
package com.readrops.app
|
||||
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.readrops.api.services.Credentials
|
||||
import com.readrops.app.account.AccountViewModel
|
||||
import com.readrops.app.addfeed.AddFeedsViewModel
|
||||
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel
|
||||
import com.readrops.app.item.ItemViewModel
|
||||
import com.readrops.app.itemslist.MainViewModel
|
||||
import com.readrops.app.notifications.NotificationPermissionViewModel
|
||||
import com.readrops.app.repositories.FreshRSSRepository
|
||||
import com.readrops.app.repositories.LocalFeedRepository
|
||||
import com.readrops.app.repositories.NextNewsRepository
|
||||
import com.readrops.app.utils.GlideApp
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import org.koin.android.ext.koin.androidApplication
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appModule = module {
|
||||
|
||||
factory { (account: Account) ->
|
||||
when (account.accountType) {
|
||||
AccountType.LOCAL -> LocalFeedRepository(get(), get(), androidContext(), account)
|
||||
AccountType.NEXTCLOUD_NEWS -> NextNewsRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }),
|
||||
get(), androidContext(), account)
|
||||
AccountType.FRESHRSS -> FreshRSSRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }),
|
||||
get(), androidContext(), account)
|
||||
else -> throw IllegalArgumentException("Account type not supported")
|
||||
}
|
||||
}
|
||||
|
||||
viewModel {
|
||||
MainViewModel(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
AddFeedsViewModel(get(), get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
ItemViewModel(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
ManageFeedsFoldersViewModel(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
NotificationPermissionViewModel(get())
|
||||
}
|
||||
|
||||
viewModel {
|
||||
AccountViewModel(get())
|
||||
}
|
||||
|
||||
single { GlideApp.with(androidApplication()) }
|
||||
|
||||
single { PreferenceManager.getDefaultSharedPreferences(androidContext()) }
|
||||
|
||||
/* single<Niddler> {
|
||||
val niddler = AndroidNiddler.Builder()
|
||||
.setNiddlerInformation(AndroidNiddler.fromApplication(get()))
|
||||
.setPort(0)
|
||||
.setMaxStackTraceSize(10)
|
||||
.build()
|
||||
|
||||
niddler.attachToApplication(get())
|
||||
|
||||
niddler.apply { start() }
|
||||
}*/
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package com.readrops.app
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.readrops.api.apiModule
|
||||
import com.readrops.app.utils.SharedPreferencesManager
|
||||
import com.readrops.db.dbModule
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.logger.Level
|
||||
|
||||
open class ReadropsApp : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
RxJavaPlugins.setErrorHandler { e: Throwable? -> }
|
||||
|
||||
createNotificationChannels()
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
||||
|
||||
startKoin {
|
||||
androidLogger(Level.ERROR)
|
||||
androidContext(this@ReadropsApp)
|
||||
|
||||
modules(apiModule, dbModule, appModule)
|
||||
}
|
||||
|
||||
val theme = when (SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME)) {
|
||||
getString(R.string.theme_value_light) -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
getString(R.string.theme_value_dark) -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val feedsColorsChannel = NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
|
||||
getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
feedsColorsChannel.description = getString(R.string.get_feeds_colors)
|
||||
|
||||
val opmlExportChannel = NotificationChannel(OPML_EXPORT_CHANNEL_ID,
|
||||
getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
opmlExportChannel.description = getString(R.string.opml_export_description)
|
||||
|
||||
val syncChannel = NotificationChannel(SYNC_CHANNEL_ID,
|
||||
getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW)
|
||||
syncChannel.description = getString(R.string.account_synchro)
|
||||
|
||||
val manager = getSystemService(NotificationManager::class.java)!!
|
||||
|
||||
manager.createNotificationChannel(feedsColorsChannel)
|
||||
manager.createNotificationChannel(opmlExportChannel)
|
||||
manager.createNotificationChannel(syncChannel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel"
|
||||
const val OPML_EXPORT_CHANNEL_ID = "opmlExportChannel"
|
||||
const val SYNC_CHANNEL_ID = "syncChannel"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
package com.readrops.app.account;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.app.utils.OPMLHelper;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ActivityAccountTypeListBinding;
|
||||
import com.readrops.app.itemslist.MainActivity;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.entities.account.AccountType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableCompletableObserver;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.OPMLHelper.OPEN_OPML_FILE_REQUEST;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
|
||||
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
|
||||
public class AccountTypeListActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = AccountTypeListActivity.class.getSimpleName();
|
||||
|
||||
private ActivityAccountTypeListBinding binding;
|
||||
private AccountTypeListAdapter adapter;
|
||||
private AccountViewModel viewModel;
|
||||
|
||||
private boolean fromMainActivity;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityAccountTypeListBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
|
||||
|
||||
setTitle(R.string.new_account);
|
||||
|
||||
binding.accountTypeRecyclerview.setLayoutManager(new LinearLayoutManager(this));
|
||||
binding.accountTypeRecyclerview.addItemDecoration(new DividerItemDecoration(this, LinearLayout.VERTICAL));
|
||||
|
||||
fromMainActivity = getIntent().getBooleanExtra(FROM_MAIN_ACTIVITY, false);
|
||||
|
||||
if (fromMainActivity)
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
adapter = new AccountTypeListAdapter(accountType -> {
|
||||
if (accountType != AccountType.LOCAL) {
|
||||
Intent intent = new Intent(getApplicationContext(), AddAccountActivity.class);
|
||||
|
||||
if (fromMainActivity)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
|
||||
|
||||
intent.putExtra(ACCOUNT_TYPE, (Parcelable) accountType);
|
||||
|
||||
startActivity(intent);
|
||||
finish();
|
||||
} else {
|
||||
Account account = new Account(null, getString(AccountType.LOCAL.getTypeName()), AccountType.LOCAL);
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
viewModel.insert(account)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<Long>() {
|
||||
@Override
|
||||
public void onSuccess(Long id) {
|
||||
account.setId(id.intValue());
|
||||
goToNextActivity(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
binding.accountTypeRecyclerview.setAdapter(adapter);
|
||||
adapter.setAccountTypes(getData());
|
||||
}
|
||||
|
||||
private List<AccountType> getData() {
|
||||
List<AccountType> accountTypes = new ArrayList<>();
|
||||
|
||||
accountTypes.add(AccountType.LOCAL);
|
||||
accountTypes.add(AccountType.NEXTCLOUD_NEWS);
|
||||
accountTypes.add(AccountType.FRESHRSS);
|
||||
|
||||
return accountTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void openOPMLFile(View view) {
|
||||
OPMLHelper.openFileIntent(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getData();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(this)
|
||||
.title(R.string.opml_processing)
|
||||
.content(R.string.operation_takes_time)
|
||||
.progress(true, 100)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
parseOPMLFile(uri, dialog);
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
|
||||
Account account = new Account(null, getString(AccountType.LOCAL.getTypeName()), AccountType.LOCAL);
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
viewModel.insert(account)
|
||||
.flatMapCompletable(id -> {
|
||||
account.setId(id.intValue());
|
||||
viewModel.setAccount(account);
|
||||
|
||||
return viewModel.parseOPMLFile(uri, this);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
dialog.dismiss();
|
||||
goToNextActivity(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
|
||||
dialog.dismiss();
|
||||
Utils.showSnackbar(binding.accountTypeListRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void goToNextActivity(Account account) {
|
||||
if (fromMainActivity) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package com.readrops.app.account;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.readrops.app.databinding.AccountTypeItemBinding;
|
||||
import com.readrops.db.entities.account.AccountType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AccountTypeListAdapter extends RecyclerView.Adapter<AccountTypeListAdapter.AccountTypeViewHolder> {
|
||||
|
||||
private List<AccountType> accountTypes;
|
||||
private OnItemClickListener listener;
|
||||
|
||||
public AccountTypeListAdapter(OnItemClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountTypeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
AccountTypeItemBinding binding = AccountTypeItemBinding.inflate(LayoutInflater.from(parent.getContext()),
|
||||
parent, false);
|
||||
|
||||
return new AccountTypeViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AccountTypeViewHolder holder, int position) {
|
||||
AccountType accountType = accountTypes.get(position);
|
||||
|
||||
holder.binding.accountTypeName.setText(accountType.getTypeName());
|
||||
holder.binding.accountTypeLogo.setImageResource(accountType.getIconRes());
|
||||
|
||||
holder.binding.getRoot().setOnClickListener(v -> listener.onItemClick(accountType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return accountTypes.size();
|
||||
}
|
||||
|
||||
public void setAccountTypes(List<AccountType> accountTypes) {
|
||||
this.accountTypes = accountTypes;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(AccountType accountType);
|
||||
}
|
||||
|
||||
public class AccountTypeViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private AccountTypeItemBinding binding;
|
||||
|
||||
public AccountTypeViewHolder(AccountTypeItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package com.readrops.app.account;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.readrops.api.opml.OPMLParser;
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import org.koin.core.parameter.ParametersHolderKt;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public class AccountViewModel extends ViewModel {
|
||||
|
||||
private ARepository repository;
|
||||
private final Database database;
|
||||
|
||||
public AccountViewModel(@NonNull Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
repository = KoinJavaComponent.get(ARepository.class, null,
|
||||
() -> ParametersHolderKt.parametersOf(account));
|
||||
}
|
||||
|
||||
public Completable login(Account account, boolean insert) {
|
||||
setAccount(account);
|
||||
return repository.login(account, insert);
|
||||
}
|
||||
|
||||
public Single<Long> insert(Account account) {
|
||||
return database.accountDao().insert(account);
|
||||
}
|
||||
|
||||
public Completable update(Account account) {
|
||||
return database.accountDao().update(account);
|
||||
}
|
||||
|
||||
public Completable delete(Account account) {
|
||||
return database.accountDao().delete(account);
|
||||
}
|
||||
|
||||
public Single<Integer> getAccountCount() {
|
||||
return database.accountDao().getAccountCount();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return repository.getFoldersWithFeeds();
|
||||
}
|
||||
|
||||
public Completable parseOPMLFile(Uri uri, Context context) throws FileNotFoundException {
|
||||
/*return OPMLParser.read(context.getContentResolver().openInputStream(uri))
|
||||
.flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));*/
|
||||
return Completable.complete();
|
||||
}
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
package com.readrops.app.account;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Patterns;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ActivityAddAccountBinding;
|
||||
import com.readrops.app.itemslist.MainActivity;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.entities.account.AccountType;
|
||||
|
||||
import io.reactivex.CompletableObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
|
||||
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
|
||||
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
|
||||
public class AddAccountActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = AddAccountActivity.class.getSimpleName();
|
||||
|
||||
private ActivityAddAccountBinding binding;
|
||||
private AccountViewModel viewModel;
|
||||
|
||||
private AccountType accountType;
|
||||
private boolean forwardResult, editAccount;
|
||||
|
||||
private Account accountToEdit;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityAddAccountBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
|
||||
|
||||
accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE);
|
||||
|
||||
int flag = getIntent().getFlags();
|
||||
forwardResult = flag == Intent.FLAG_ACTIVITY_FORWARD_RESULT;
|
||||
|
||||
accountToEdit = getIntent().getParcelableExtra(EDIT_ACCOUNT);
|
||||
|
||||
if (forwardResult || accountToEdit != null)
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
if (accountToEdit != null) {
|
||||
editAccount = true;
|
||||
fillFields();
|
||||
} else {
|
||||
binding.providerImage.setImageResource(accountType.getIconRes());
|
||||
binding.providerName.setText(accountType.getTypeName());
|
||||
binding.addAccountName.setText(accountType.getTypeName());
|
||||
|
||||
if (accountType == AccountType.FRESHRSS) {
|
||||
binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAccount(View view) {
|
||||
if (fieldsAreValid()) {
|
||||
String url = binding.addAccountUrl.getText().toString().trim();
|
||||
String name = binding.addAccountName.getText().toString().trim();
|
||||
String login = binding.addAccountLogin.getText().toString().trim();
|
||||
String password = binding.addAccountPassword.getText().toString().trim();
|
||||
|
||||
if (!(url.toLowerCase().contains(Utils.HTTP_PREFIX) || url.toLowerCase().contains(Utils.HTTPS_PREFIX))) {
|
||||
url = Utils.HTTPS_PREFIX + url;
|
||||
}
|
||||
|
||||
if (editAccount) {
|
||||
accountToEdit.setUrl(url);
|
||||
accountToEdit.setAccountName(name);
|
||||
accountToEdit.setLogin(login);
|
||||
accountToEdit.setPassword(password);
|
||||
|
||||
updateAccount();
|
||||
} else {
|
||||
Account account = new Account(url, name, accountType);
|
||||
account.setLogin(login);
|
||||
account.setPassword(password);
|
||||
|
||||
viewModel.login(account, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new CompletableObserver() {
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
binding.addAccountLoading.setVisibility(View.VISIBLE);
|
||||
binding.addAccountValidate.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
saveLoginPassword(account);
|
||||
|
||||
if (forwardResult) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.d(TAG, e.getMessage());
|
||||
binding.addAccountLoading.setVisibility(View.GONE);
|
||||
binding.addAccountValidate.setEnabled(true);
|
||||
|
||||
Utils.showSnackbar(binding.addAccountRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private boolean fieldsAreValid() {
|
||||
boolean valid = true;
|
||||
|
||||
if (binding.addAccountUrl.getText().toString().trim().isEmpty()) {
|
||||
binding.addAccountUrl.setError(getString(R.string.empty_field));
|
||||
valid = false;
|
||||
} else if (!Patterns.WEB_URL.matcher(binding.addAccountUrl.getText().toString().trim()).matches()) {
|
||||
binding.addAccountUrl.setError(getString(R.string.wrong_url));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (binding.addAccountName.getText().toString().trim().isEmpty()) {
|
||||
binding.addAccountName.setError(getString(R.string.empty_field));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (binding.addAccountLogin.getText().toString().trim().isEmpty()) {
|
||||
binding.addAccountLogin.setError(getString(R.string.empty_field));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (binding.addAccountPassword.getText().toString().trim().isEmpty()) {
|
||||
binding.addAccountPassword.setError(getString(R.string.empty_field));
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
private void saveLoginPassword(Account account) {
|
||||
SharedPreferencesManager.writeValue(account.getLoginKey(), account.getLogin());
|
||||
SharedPreferencesManager.writeValue(account.getPasswordKey(), account.getPassword());
|
||||
|
||||
account.setLogin(null);
|
||||
account.setPassword(null);
|
||||
}
|
||||
|
||||
private void fillFields() {
|
||||
binding.providerImage.setImageResource(accountToEdit.getAccountType().getIconRes());
|
||||
binding.providerName.setText(accountToEdit.getAccountType().getTypeName());
|
||||
|
||||
binding.addAccountUrl.setText(accountToEdit.getUrl());
|
||||
binding.addAccountName.setText(accountToEdit.getAccountName());
|
||||
binding.addAccountLogin.setText(SharedPreferencesManager.readString(accountToEdit.getLoginKey()));
|
||||
binding.addAccountPassword.setText(SharedPreferencesManager.readString(accountToEdit.getPasswordKey()));
|
||||
}
|
||||
|
||||
private void updateAccount() {
|
||||
viewModel.login(accountToEdit, false)
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.addAccountRoot, throwable.getMessage()))
|
||||
.doAfterTerminate(() -> saveLoginPassword(accountToEdit))
|
||||
.andThen(viewModel.update(accountToEdit))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new CompletableObserver() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
binding.addAccountLoading.setVisibility(View.VISIBLE);
|
||||
binding.addAccountValidate.setEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
binding.addAccountLoading.setVisibility(View.GONE);
|
||||
binding.addAccountValidate.setEnabled(true);
|
||||
|
||||
Utils.showSnackbar(binding.addAccountRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_ENTER:
|
||||
createAccount(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package com.readrops.app.addfeed;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AccountArrayAdapter extends ArrayAdapter<Account> {
|
||||
|
||||
public AccountArrayAdapter(@NonNull Context context, @NonNull List<Account> objects) {
|
||||
super(context, 0, objects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
return createItemView(position, convertView, parent);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
return createItemView(position, convertView, parent);
|
||||
}
|
||||
|
||||
private View createItemView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.account_type_item, parent, false);
|
||||
}
|
||||
|
||||
Account account = getItem(position);
|
||||
|
||||
ImageView accountIcon = convertView.findViewById(R.id.account_type_logo);
|
||||
TextView accountName = convertView.findViewById(R.id.account_type_name);
|
||||
|
||||
accountIcon.setImageResource(account.getAccountType().getIconRes());
|
||||
accountName.setText(account.getAccountType().getTypeName());
|
||||
|
||||
return convertView;
|
||||
}
|
||||
}
|
|
@ -1,333 +0,0 @@
|
|||
package com.readrops.app.addfeed;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.util.Patterns;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.mikepenz.fastadapter.FastAdapter;
|
||||
import com.mikepenz.fastadapter.adapters.ItemAdapter;
|
||||
import com.mikepenz.fastadapter.commons.utils.DiffCallback;
|
||||
import com.mikepenz.fastadapter.commons.utils.FastAdapterDiffUtil;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ActivityAddFeedBinding;
|
||||
import com.readrops.app.utils.customviews.ReadropsItemTouchCallback;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
|
||||
public class AddFeedActivity extends AppCompatActivity implements View.OnClickListener {
|
||||
|
||||
private AccountArrayAdapter arrayAdapter;
|
||||
|
||||
private ItemAdapter<ParsingResult> parseItemsAdapter;
|
||||
private ItemAdapter<FeedInsertionResult> insertionResultsAdapter;
|
||||
FastAdapter<ParsingResult> fastAdapter;
|
||||
|
||||
private AddFeedsViewModel viewModel;
|
||||
private ArrayList<Feed> feedsToUpdate;
|
||||
|
||||
private ActivityAddFeedBinding binding;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityAddFeedBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
binding.addFeedLoad.setOnClickListener(this);
|
||||
binding.addFeedOk.setOnClickListener(this);
|
||||
binding.addFeedOk.setEnabled(false);
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, AddFeedsViewModel.class);
|
||||
|
||||
parseItemsAdapter = new ItemAdapter<>();
|
||||
fastAdapter = FastAdapter.with(parseItemsAdapter);
|
||||
fastAdapter.withSelectable(true);
|
||||
fastAdapter.withOnClickListener((v, adapter, item, position) -> {
|
||||
item.setChecked(!item.isChecked());
|
||||
|
||||
fastAdapter.notifyAdapterItemChanged(position);
|
||||
binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
binding.addFeedResults.setAdapter(fastAdapter);
|
||||
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
binding.addFeedResults.setLayoutManager(layoutManager);
|
||||
|
||||
new ItemTouchHelper(new ReadropsItemTouchCallback(this,
|
||||
new ReadropsItemTouchCallback.Config.Builder()
|
||||
.swipeDirs(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)
|
||||
.leftDraw(Color.RED, R.drawable.ic_delete, null)
|
||||
.rightDraw(Color.RED, R.drawable.ic_delete, null)
|
||||
.swipeCallback((viewHolder, direction) -> {
|
||||
parseItemsAdapter.remove(viewHolder.getAdapterPosition());
|
||||
|
||||
if (parseItemsAdapter.getAdapterItemCount() == 0) {
|
||||
binding.addFeedResultsTextView.setVisibility(View.GONE);
|
||||
binding.addFeedResults.setVisibility(View.GONE);
|
||||
}
|
||||
})
|
||||
.build()))
|
||||
.attachToRecyclerView(binding.addFeedResults);
|
||||
|
||||
insertionResultsAdapter = new ItemAdapter<>();
|
||||
RecyclerView.LayoutManager layoutManager1 = new LinearLayoutManager(this);
|
||||
binding.addFeedInsertedResultsRecyclerview.setAdapter(FastAdapter.with(insertionResultsAdapter));
|
||||
binding.addFeedInsertedResultsRecyclerview.setLayoutManager(layoutManager1);
|
||||
|
||||
viewModel.getAccounts().observe(this, accounts -> {
|
||||
// set the current account at the top of the list
|
||||
int currentAccountId = getIntent().getIntExtra(ACCOUNT_ID, 1);
|
||||
Collections.sort(accounts, (o1, o2) -> {
|
||||
if (o1.getId() == currentAccountId) {
|
||||
return -1;
|
||||
} else if (o2.getId() == currentAccountId) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
arrayAdapter = new AccountArrayAdapter(this, accounts);
|
||||
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
binding.addFeedAccountSpinner.setAdapter(arrayAdapter);
|
||||
});
|
||||
|
||||
feedsToUpdate = new ArrayList<>();
|
||||
|
||||
// new feed intent
|
||||
if (getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_SEND)) {
|
||||
String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
binding.addFeedTextInput.setText(text);
|
||||
onClick(binding.addFeedLoad);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.add_feed_load) {
|
||||
if (isValidUrl()) {
|
||||
binding.addFeedLoadingMessage.setVisibility(View.GONE);
|
||||
binding.addFeedLoading.setVisibility(View.VISIBLE);
|
||||
loadFeed();
|
||||
}
|
||||
} else if (v.getId() == R.id.add_feed_ok) {
|
||||
insertionResultsAdapter.clear();
|
||||
insertFeeds();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidUrl() {
|
||||
String url = binding.addFeedTextInput.getText().toString().trim();
|
||||
|
||||
if (url.isEmpty()) {
|
||||
binding.addFeedTextInput.setError(getString(R.string.empty_field));
|
||||
return false;
|
||||
} else if (!Patterns.WEB_URL.matcher(url).matches()) {
|
||||
binding.addFeedTextInput.setError(getString(R.string.wrong_url));
|
||||
return false;
|
||||
} else
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean recyclerViewHasCheckedItems() {
|
||||
for (ParsingResult result : parseItemsAdapter.getAdapterItems()) {
|
||||
if (result.isChecked())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void disableParsingResult(ParsingResult parsingResult) {
|
||||
for (ParsingResult result : parseItemsAdapter.getAdapterItems()) {
|
||||
if (result.getUrl().equals(parsingResult.getUrl())) {
|
||||
result.setChecked(false);
|
||||
fastAdapter.notifyAdapterItemChanged(parseItemsAdapter.getAdapterPosition(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFeed() {
|
||||
String url = binding.addFeedTextInput.getText().toString().trim();
|
||||
|
||||
final String finalUrl;
|
||||
if (!(url.contains(Utils.HTTP_PREFIX) || url.contains(Utils.HTTPS_PREFIX)))
|
||||
finalUrl = Utils.HTTPS_PREFIX + url;
|
||||
else
|
||||
finalUrl = url;
|
||||
|
||||
viewModel.parseUrl(finalUrl)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<List<ParsingResult>>() {
|
||||
@Override
|
||||
public void onSuccess(List<ParsingResult> parsingResultList) {
|
||||
displayParseResults(parsingResultList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Utils.showSnackbar(binding.addFeedRoot, e.getMessage());
|
||||
binding.addFeedLoading.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayParseResults(List<ParsingResult> parsingResultList) {
|
||||
binding.addFeedLoading.setVisibility(View.GONE);
|
||||
|
||||
if (!parsingResultList.isEmpty()) {
|
||||
binding.addFeedResultsTextView.setVisibility(View.VISIBLE);
|
||||
binding.addFeedResults.setVisibility(View.VISIBLE);
|
||||
|
||||
DiffUtil.DiffResult diffResult = FastAdapterDiffUtil.calculateDiff(parseItemsAdapter, parsingResultList, new DiffCallback<ParsingResult>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(ParsingResult oldItem, ParsingResult newItem) {
|
||||
return oldItem.getUrl().equals(newItem.getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(ParsingResult oldItem, ParsingResult newItem) {
|
||||
return oldItem.getUrl().equals(newItem.getUrl()) &&
|
||||
oldItem.isChecked() == newItem.isChecked();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getChangePayload(ParsingResult oldItem, int oldItemPosition, ParsingResult newItem, int newItemPosition) {
|
||||
newItem.setChecked(oldItem.isChecked());
|
||||
return newItem;
|
||||
}
|
||||
}, false);
|
||||
|
||||
FastAdapterDiffUtil.set(parseItemsAdapter, diffResult);
|
||||
binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
|
||||
} else {
|
||||
parseItemsAdapter.clear();
|
||||
binding.addFeedResultsTextView.setVisibility(View.GONE);
|
||||
binding.addFeedResults.setVisibility(View.GONE);
|
||||
|
||||
binding.addFeedLoadingMessage.setVisibility(View.VISIBLE);
|
||||
binding.addFeedLoadingMessage.setText(R.string.no_feed_found);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertFeeds() {
|
||||
binding.addFeedInsertProgressbar.setVisibility(View.VISIBLE);
|
||||
binding.addFeedOk.setEnabled(false);
|
||||
|
||||
List<ParsingResult> feedsToInsert = new ArrayList<>();
|
||||
for (ParsingResult result : parseItemsAdapter.getAdapterItems()) {
|
||||
if (result.isChecked())
|
||||
feedsToInsert.add(result);
|
||||
}
|
||||
|
||||
Account account = (Account) binding.addFeedAccountSpinner.getSelectedItem();
|
||||
|
||||
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
|
||||
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
|
||||
|
||||
viewModel.addFeeds(feedsToInsert, account)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<List<FeedInsertionResult>>() {
|
||||
@Override
|
||||
public void onSuccess(List<FeedInsertionResult> feedInsertionResults) {
|
||||
displayInsertionResults(feedInsertionResults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
binding.addFeedInsertProgressbar.setVisibility(View.GONE);
|
||||
binding.addFeedOk.setEnabled(true);
|
||||
Utils.showSnackbar(binding.addFeedRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayInsertionResults(List<FeedInsertionResult> feedInsertionResults) {
|
||||
binding.addFeedInsertProgressbar.setVisibility(View.GONE);
|
||||
binding.addFeedInsertedResultsRecyclerview.setVisibility(View.VISIBLE);
|
||||
|
||||
for (FeedInsertionResult feedInsertionResult : feedInsertionResults) {
|
||||
if (feedInsertionResult.getFeed() != null)
|
||||
feedsToUpdate.add(feedInsertionResult.getFeed());
|
||||
|
||||
disableParsingResult(feedInsertionResult.getParsingResult());
|
||||
}
|
||||
|
||||
insertionResultsAdapter.add(feedInsertionResults);
|
||||
binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
finish();
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
if (!feedsToUpdate.isEmpty()) {
|
||||
Intent intent = new Intent();
|
||||
intent.putParcelableArrayListExtra(FEEDS, feedsToUpdate);
|
||||
|
||||
setResult(RESULT_OK, intent);
|
||||
}
|
||||
|
||||
super.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
onClick(binding.addFeedLoad);
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package com.readrops.app.addfeed;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource;
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.app.utils.HtmlParser;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import org.koin.core.parameter.ParametersHolderKt;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Single;
|
||||
|
||||
public class AddFeedsViewModel extends ViewModel {
|
||||
|
||||
private final Database database;
|
||||
private final LocalRSSDataSource localRSSDataSource;
|
||||
|
||||
public AddFeedsViewModel(@NonNull Database database, @NonNull LocalRSSDataSource localRSSDataSource) {
|
||||
this.database = database;
|
||||
this.localRSSDataSource = localRSSDataSource;
|
||||
}
|
||||
|
||||
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results, Account account) {
|
||||
ARepository repository = KoinJavaComponent.get(ARepository.class, null,
|
||||
() -> ParametersHolderKt.parametersOf(account));
|
||||
|
||||
return repository.addFeeds(results);
|
||||
}
|
||||
|
||||
public Single<List<ParsingResult>> parseUrl(String url) {
|
||||
return Single.create(emitter -> {
|
||||
List<ParsingResult> results = new ArrayList<>();
|
||||
|
||||
if (localRSSDataSource.isUrlRSSResource(url)) {
|
||||
ParsingResult parsingResult = new ParsingResult(url, null);
|
||||
results.add(parsingResult);
|
||||
} else {
|
||||
results.addAll(HtmlParser.getFeedLink(url));
|
||||
}
|
||||
|
||||
emitter.onSuccess(results);
|
||||
});
|
||||
}
|
||||
|
||||
public LiveData<List<Account>> getAccounts() {
|
||||
return database.accountDao().selectAllAsync();
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package com.readrops.app.addfeed;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.mikepenz.fastadapter.FastAdapter;
|
||||
import com.mikepenz.fastadapter.items.AbstractItem;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.db.entities.Feed;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FeedInsertionResult extends AbstractItem<FeedInsertionResult, FeedInsertionResult.FeedInsertionViewHolder> {
|
||||
|
||||
private Feed feed;
|
||||
|
||||
private ParsingResult parsingResult;
|
||||
|
||||
private FeedInsertionError insertionError;
|
||||
|
||||
public FeedInsertionResult() {
|
||||
// empty constructor
|
||||
}
|
||||
|
||||
public Feed getFeed() {
|
||||
return feed;
|
||||
}
|
||||
|
||||
public void setFeed(Feed feed) {
|
||||
this.feed = feed;
|
||||
}
|
||||
|
||||
public ParsingResult getParsingResult() {
|
||||
return parsingResult;
|
||||
}
|
||||
|
||||
public void setParsingResult(ParsingResult parsingResult) {
|
||||
this.parsingResult = parsingResult;
|
||||
}
|
||||
|
||||
public FeedInsertionError getInsertionError() {
|
||||
return insertionError;
|
||||
}
|
||||
|
||||
public void setInsertionError(FeedInsertionError insertionError) {
|
||||
this.insertionError = insertionError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelectable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FeedInsertionViewHolder getViewHolder(View v) {
|
||||
return new FeedInsertionViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLayoutRes() {
|
||||
return R.layout.feed_insertion_result;
|
||||
}
|
||||
|
||||
|
||||
public enum FeedInsertionError {
|
||||
ERROR,
|
||||
NETWORK_ERROR,
|
||||
DB_ERROR,
|
||||
PARSE_ERROR,
|
||||
FORMAT_ERROR,
|
||||
UNKNOWN_ERROR
|
||||
}
|
||||
|
||||
class FeedInsertionViewHolder extends FastAdapter.ViewHolder<FeedInsertionResult> {
|
||||
|
||||
private TextView feedInsertionRes;
|
||||
private ImageView feedInsertionIcon;
|
||||
|
||||
public FeedInsertionViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
feedInsertionRes = itemView.findViewById(R.id.feed_insertion_result_text_view);
|
||||
feedInsertionIcon = itemView.findViewById(R.id.feed_insertion_result_icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(FeedInsertionResult item, List<Object> payloads) {
|
||||
if (item.getInsertionError() == null) {
|
||||
setText(R.string.feed_insertion_successfull, item.parsingResult);
|
||||
feedInsertionIcon.setImageResource(R.drawable.ic_check_green);
|
||||
} else {
|
||||
switch (item.getInsertionError()) {
|
||||
case NETWORK_ERROR:
|
||||
setText(R.string.feed_insertion_network_failed, item.parsingResult);
|
||||
break;
|
||||
case DB_ERROR:
|
||||
break;
|
||||
case PARSE_ERROR:
|
||||
setText(R.string.feed_insertion_parse_failed, item.parsingResult);
|
||||
break;
|
||||
case FORMAT_ERROR:
|
||||
setText(R.string.feed_insertion_wrong_format, item.parsingResult);
|
||||
break;
|
||||
case UNKNOWN_ERROR:
|
||||
setText(R.string.feed_insertion_unknown_error, item.parsingResult);
|
||||
break;
|
||||
case ERROR:
|
||||
setText(R.string.feed_insertion_error, item.parsingResult);
|
||||
}
|
||||
|
||||
feedInsertionIcon.setImageResource(R.drawable.ic_warning_red);
|
||||
}
|
||||
}
|
||||
|
||||
private void setText(@StringRes int stringRes, ParsingResult parsingResult) {
|
||||
feedInsertionRes.setText(itemView.getContext().getString(stringRes,
|
||||
parsingResult.getLabel() != null ? parsingResult.getLabel() :
|
||||
parsingResult.getUrl()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindView(@NotNull FeedInsertionResult item) {
|
||||
// not useful
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
package com.readrops.app.addfeed;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.mikepenz.fastadapter.FastAdapter;
|
||||
import com.mikepenz.fastadapter.items.AbstractItem;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.db.entities.Feed;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ParsingResult extends AbstractItem<ParsingResult, ParsingResult.ParsingResultViewHolder> {
|
||||
|
||||
private String url;
|
||||
|
||||
private String label;
|
||||
|
||||
private boolean checked;
|
||||
|
||||
private Integer folderId;
|
||||
|
||||
public ParsingResult(String url, String label) {
|
||||
this.url = url;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public static List<ParsingResult> toParsingResults(List<Feed> feeds) {
|
||||
List<ParsingResult> parsingResults = new ArrayList<>();
|
||||
|
||||
for (Feed feed : feeds) {
|
||||
ParsingResult parsingResult = new ParsingResult(feed.getUrl(), null);
|
||||
parsingResult.setFolderId(feed.getFolderId());
|
||||
parsingResults.add(parsingResult);
|
||||
}
|
||||
|
||||
return parsingResults;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked) {
|
||||
this.checked = checked;
|
||||
}
|
||||
|
||||
public boolean isChecked() {
|
||||
return checked;
|
||||
}
|
||||
|
||||
public Integer getFolderId() {
|
||||
return folderId;
|
||||
}
|
||||
|
||||
public void setFolderId(Integer folderId) {
|
||||
this.folderId = folderId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSelectable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ParsingResultViewHolder getViewHolder(View v) {
|
||||
return new ParsingResultViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return R.id.add_feed_main_layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLayoutRes() {
|
||||
return R.layout.add_feed_item;
|
||||
}
|
||||
|
||||
class ParsingResultViewHolder extends FastAdapter.ViewHolder<ParsingResult> {
|
||||
|
||||
private TextView feedLabel;
|
||||
private TextView feedUrl;
|
||||
private CheckBox checkBox;
|
||||
|
||||
public ParsingResultViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
|
||||
feedLabel = itemView.findViewById(R.id.add_feed_item_label);
|
||||
feedUrl = itemView.findViewById(R.id.add_feed_item_url);
|
||||
checkBox = itemView.findViewById(R.id.add_feed_checkbox);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(@NotNull ParsingResult item, List<Object> payloads) {
|
||||
if (!payloads.isEmpty()) {
|
||||
ParsingResult newItem = (ParsingResult) payloads.get(0);
|
||||
|
||||
checkBox.setChecked(newItem.isChecked());
|
||||
} else {
|
||||
if (item.getLabel() != null && !item.getLabel().isEmpty())
|
||||
feedLabel.setText(item.getLabel());
|
||||
else
|
||||
feedLabel.setVisibility(View.GONE);
|
||||
|
||||
feedUrl.setText(item.getUrl());
|
||||
|
||||
checkBox.setChecked(item.isChecked());
|
||||
checkBox.setClickable(false);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unbindView(@NotNull ParsingResult item) {
|
||||
// not useful
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == null)
|
||||
return false;
|
||||
else if (!(o instanceof ParsingResult))
|
||||
return false;
|
||||
else {
|
||||
ParsingResult parsingResult = (ParsingResult) o;
|
||||
|
||||
return parsingResult.getUrl().equals(this.getUrl());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
package com.readrops.app.feedsfolders;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.api.utils.exceptions.ConflictException;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding;
|
||||
import com.readrops.app.feedsfolders.feeds.FeedsFragment;
|
||||
import com.readrops.app.feedsfolders.folders.FoldersFragment;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
|
||||
public class ManageFeedsFoldersActivity extends AppCompatActivity {
|
||||
|
||||
private ActivityManageFeedsFoldersBinding binding;
|
||||
private ManageFeedsFoldersViewModel viewModel;
|
||||
|
||||
private Account account;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityManageFeedsFoldersBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
setSupportActionBar(binding.manageFeedsFoldersToolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
account = getIntent().getParcelableExtra(ACCOUNT);
|
||||
|
||||
FeedsFoldersPageAdapter pageAdapter = new FeedsFoldersPageAdapter(getSupportFragmentManager());
|
||||
|
||||
binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter);
|
||||
binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager);
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, ManageFeedsFoldersViewModel.class);
|
||||
viewModel.setAccount(account);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (account.getAccountType().getAccountConfig().getCanCreateFolder())
|
||||
getMenuInflater().inflate(R.menu.feeds_menu, menu);
|
||||
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.add_folder:
|
||||
addFolder();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
finish();
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private void addFolder() {
|
||||
new MaterialDialog.Builder(ManageFeedsFoldersActivity.this)
|
||||
.title(R.string.add_folder)
|
||||
.positiveText(R.string.validate)
|
||||
.input(R.string.folder, 0, (dialog, input) -> {
|
||||
Folder folder = new Folder();
|
||||
folder.setName(input.toString());
|
||||
folder.setAccountId(account.getId());
|
||||
|
||||
viewModel.addFolder(folder)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> {
|
||||
String message;
|
||||
if (throwable instanceof ConflictException)
|
||||
message = getString(R.string.folder_already_exists);
|
||||
else if (throwable instanceof UnknownFormatException)
|
||||
message = getString(R.string.folder_bad_format);
|
||||
else
|
||||
message = getString(R.string.error_occured);
|
||||
|
||||
Utils.showSnackbar(binding.manageFeedsFoldersRoot, message);
|
||||
})
|
||||
.subscribe();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public class FeedsFoldersPageAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private FeedsFoldersPageAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getApplicationContext().getString(R.string.feeds);
|
||||
case 1:
|
||||
return getApplicationContext().getString(R.string.folders);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment fragment = null;
|
||||
|
||||
switch (position) {
|
||||
case 0:
|
||||
fragment = FeedsFragment.newInstance(account);
|
||||
break;
|
||||
case 1:
|
||||
fragment = FoldersFragment.newInstance(account);
|
||||
break;
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
package com.readrops.app.feedsfolders;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.FeedWithFolder;
|
||||
import com.readrops.db.pojo.FolderWithFeedCount;
|
||||
|
||||
import org.koin.core.parameter.ParametersHolderKt;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public class ManageFeedsFoldersViewModel extends ViewModel {
|
||||
|
||||
private final Database database;
|
||||
private LiveData<List<FeedWithFolder>> feedsWithFolder;
|
||||
private LiveData<List<Folder>> folders;
|
||||
private ARepository repository;
|
||||
|
||||
private Account account;
|
||||
|
||||
public ManageFeedsFoldersViewModel(@NonNull Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
repository = KoinJavaComponent.get(ARepository.class, null,
|
||||
() -> ParametersHolderKt.parametersOf(account));
|
||||
|
||||
feedsWithFolder = database.feedDao().getAllFeedsWithFolder(account.getId());
|
||||
folders = database.folderDao().getAllFolders(account.getId());
|
||||
}
|
||||
|
||||
public LiveData<List<FeedWithFolder>> getFeedsWithFolder() {
|
||||
return feedsWithFolder;
|
||||
}
|
||||
|
||||
public Completable updateFeedWithFolder(Feed feed) {
|
||||
return repository.updateFeed(feed);
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
this.account = account;
|
||||
setup();
|
||||
}
|
||||
|
||||
public LiveData<List<Folder>> getFolders() {
|
||||
return folders;
|
||||
}
|
||||
|
||||
public LiveData<List<FolderWithFeedCount>> getFoldersWithFeedCount() {
|
||||
return database.folderDao().getFoldersWithFeedCount(account.getId());
|
||||
}
|
||||
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return repository.addFolder(folder);
|
||||
}
|
||||
|
||||
public Completable updateFolder(Folder folder) {
|
||||
return repository.updateFolder(folder);
|
||||
}
|
||||
|
||||
public Completable deleteFolder(Folder folder) {
|
||||
return repository.deleteFolder(folder);
|
||||
}
|
||||
|
||||
public Completable deleteFeed(Feed feed) {
|
||||
return repository.deleteFeed(feed);
|
||||
}
|
||||
|
||||
public Single<Integer> getFeedCountByAccount() {
|
||||
return database.feedDao().getFeedCount(account.getId());
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.feeds;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.FeedWithFolder;
|
||||
|
||||
import org.koin.android.compat.SharedViewModelCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class EditFeedDialogFragment extends DialogFragment implements AdapterView.OnItemSelectedListener {
|
||||
|
||||
private TextInputEditText feedName;
|
||||
private TextInputEditText feedUrl;
|
||||
private Spinner folder;
|
||||
|
||||
private Map<String, Integer> values;
|
||||
|
||||
private FeedWithFolder feedWithFolder;
|
||||
private Account account;
|
||||
private ManageFeedsFoldersViewModel viewModel;
|
||||
|
||||
public EditFeedDialogFragment() {
|
||||
}
|
||||
|
||||
public static EditFeedDialogFragment newInstance(FeedWithFolder feedWithFolder, Account account) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable("feedWithFolder", feedWithFolder);
|
||||
args.putParcelable(ACCOUNT, account);
|
||||
|
||||
EditFeedDialogFragment fragment = new EditFeedDialogFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue();
|
||||
|
||||
feedWithFolder = getArguments().getParcelable("feedWithFolder");
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
viewModel.setAccount(account);
|
||||
|
||||
View v = getActivity().getLayoutInflater().inflate(R.layout.edit_feed_layout, null);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
|
||||
.setTitle(R.string.edit_feed)
|
||||
.setPositiveButton(R.string.validate, (dialog, which) -> {
|
||||
Feed feed = feedWithFolder.getFeed();
|
||||
feed.setName(feedName.getText().toString().trim());
|
||||
feed.setUrl(feedUrl.getText().toString().trim());
|
||||
|
||||
viewModel.updateFeedWithFolder(feed)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe();
|
||||
});
|
||||
|
||||
builder.setView(v);
|
||||
fillData(v);
|
||||
|
||||
viewModel.getFolders().observe(this, folders -> {
|
||||
values = new TreeMap<>(String::compareTo);
|
||||
|
||||
if (!account.getAccountType().getAccountConfig().getAddNoFolder())
|
||||
values.put(getString(R.string.no_folder), 0);
|
||||
|
||||
for (Folder folder : folders) {
|
||||
values.put(folder.getName(), folder.getId());
|
||||
}
|
||||
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
|
||||
android.R.layout.simple_spinner_dropdown_item, new ArrayList<>(values.keySet()));
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
folder.setAdapter(adapter);
|
||||
folder.setOnItemSelectedListener(this);
|
||||
|
||||
if (feedWithFolder.getFolder() != null)
|
||||
folder.setSelection(adapter.getPosition(feedWithFolder.getFolder().getName()));
|
||||
else
|
||||
folder.setSelection(adapter.getPosition(getString(R.string.no_folder)));
|
||||
});
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private void fillData(View v) {
|
||||
feedName = v.findViewById(R.id.edit_feed_name_edit_text);
|
||||
feedUrl = v.findViewById(R.id.edit_feed_url_edit_text);
|
||||
folder = v.findViewById(R.id.edit_feed_folder_spinner);
|
||||
|
||||
if (!account.getAccountType().getAccountConfig().isFeedUrlReadOnly())
|
||||
feedUrl.setEnabled(false);
|
||||
|
||||
feedName.setText(feedWithFolder.getFeed().getName());
|
||||
feedUrl.setText(feedWithFolder.getFeed().getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
String folderName = (String) parent.getAdapter().getItem(position);
|
||||
int folderId = values.get(folderName);
|
||||
|
||||
feedWithFolder.getFeed().setFolderId(folderId == 0 ? null : folderId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.feeds
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.readrops.app.databinding.FeedOptionsLayoutBinding
|
||||
import com.readrops.app.utils.ReadropsKeys.ACCOUNT
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.pojo.FeedWithFolder
|
||||
|
||||
class FeedOptionsDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var feedWithFolder: FeedWithFolder
|
||||
private lateinit var account: Account
|
||||
|
||||
private var _binding: FeedOptionsLayoutBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
companion object {
|
||||
const val FEED_KEY = "FEED_KEY"
|
||||
|
||||
fun newInstance(feedWithFolder: FeedWithFolder, account: Account): FeedOptionsDialogFragment {
|
||||
val bundle = Bundle()
|
||||
bundle.putParcelable(FEED_KEY, feedWithFolder)
|
||||
bundle.putParcelable(ACCOUNT, account)
|
||||
|
||||
val feedsOptionsDialogFragment = FeedOptionsDialogFragment()
|
||||
feedsOptionsDialogFragment.arguments = bundle
|
||||
|
||||
return feedsOptionsDialogFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
feedWithFolder = arguments?.getParcelable(FEED_KEY)!!
|
||||
account = arguments?.getParcelable(ACCOUNT)!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
_binding = FeedOptionsLayoutBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.feedOptionsTitle.text = feedWithFolder.feed.name
|
||||
|
||||
binding.feedOptionsEditLayout.setOnClickListener { openEditFeedDialog() }
|
||||
binding.feedOptionsOpenRootLayout.setOnClickListener { openFeedRootUrl() }
|
||||
binding.feedOptionsDeleteLayout.setOnClickListener { deleteFeed() }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun openEditFeedDialog() {
|
||||
dismiss()
|
||||
val editFeedDialogFragment = EditFeedDialogFragment.newInstance(feedWithFolder, account)
|
||||
|
||||
activity
|
||||
?.supportFragmentManager
|
||||
?.beginTransaction()
|
||||
?.add(editFeedDialogFragment, "")
|
||||
?.commit()
|
||||
}
|
||||
|
||||
private fun openFeedRootUrl() {
|
||||
dismiss()
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(feedWithFolder.feed.siteUrl)))
|
||||
}
|
||||
|
||||
private fun deleteFeed() {
|
||||
dismiss()
|
||||
(parentFragment as FeedsFragment).deleteFeed(feedWithFolder.feed)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.feeds;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.FeedLayoutBinding;
|
||||
import com.readrops.app.utils.GlideRequests;
|
||||
import com.readrops.db.pojo.FeedWithFolder;
|
||||
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.FeedViewHolder> {
|
||||
|
||||
private ManageFeedsListener listener;
|
||||
|
||||
private static final DiffUtil.ItemCallback<FeedWithFolder> DIFF_CALLBACK = new DiffUtil.ItemCallback<FeedWithFolder>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull FeedWithFolder feedWithFolder, @NonNull FeedWithFolder t1) {
|
||||
return feedWithFolder.getFeed().getId() == t1.getFeed().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull FeedWithFolder feedWithFolder, @NonNull FeedWithFolder t1) {
|
||||
boolean folder = false;
|
||||
if (feedWithFolder.getFolder() != null && t1.getFolder() != null)
|
||||
folder = feedWithFolder.getFolder().getName().equals(t1.getFolder().getName());
|
||||
|
||||
return feedWithFolder.getFeed().getName().equals(t1.getFeed().getName())
|
||||
&& folder;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getChangePayload(@NonNull FeedWithFolder oldItem, @NonNull FeedWithFolder newItem) {
|
||||
return newItem;
|
||||
}
|
||||
};
|
||||
|
||||
public FeedsAdapter(ManageFeedsListener listener) {
|
||||
super(DIFF_CALLBACK);
|
||||
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FeedViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
FeedLayoutBinding binding = FeedLayoutBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false);
|
||||
|
||||
return new FeedViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FeedViewHolder viewHolder, int i) {
|
||||
FeedWithFolder feedWithFolder = getItem(i);
|
||||
|
||||
if (feedWithFolder.getFeed().getIconUrl() != null) {
|
||||
KoinJavaComponent.<GlideRequests>get(GlideRequests.class)
|
||||
.load(feedWithFolder.getFeed().getIconUrl())
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.ic_rss_feed_grey)
|
||||
.into(viewHolder.binding.feedLayoutIcon);
|
||||
} else
|
||||
viewHolder.binding.feedLayoutIcon.setImageResource(R.drawable.ic_rss_feed_grey);
|
||||
|
||||
viewHolder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName());
|
||||
if (feedWithFolder.getFeed().getDescription() != null) {
|
||||
viewHolder.binding.feedLayoutDescription.setVisibility(View.VISIBLE);
|
||||
viewHolder.binding.feedLayoutDescription.setText(feedWithFolder.getFeed().getDescription());
|
||||
} else
|
||||
viewHolder.binding.feedLayoutDescription.setVisibility(View.GONE);
|
||||
|
||||
if (feedWithFolder.getFolder() != null)
|
||||
viewHolder.binding.feedLayoutFolder.setText(feedWithFolder.getFolder().getName());
|
||||
else
|
||||
viewHolder.binding.feedLayoutFolder.setText(R.string.no_folder);
|
||||
|
||||
viewHolder.itemView.setOnClickListener(v -> listener.onEdit(feedWithFolder));
|
||||
viewHolder.itemView.setOnLongClickListener(v -> {
|
||||
listener.onOpenLink(feedWithFolder);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FeedViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty()) {
|
||||
FeedWithFolder feedWithFolder = (FeedWithFolder) payloads.get(0);
|
||||
|
||||
holder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName());
|
||||
|
||||
if (feedWithFolder.getFolder() != null)
|
||||
holder.binding.feedLayoutName.setText(feedWithFolder.getFolder().getName());
|
||||
else
|
||||
holder.binding.feedLayoutName.setText(R.string.no_folder);
|
||||
|
||||
} else
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
public interface ManageFeedsListener {
|
||||
void onOpenLink(FeedWithFolder feedWithFolder);
|
||||
|
||||
void onEdit(FeedWithFolder feedWithFolder);
|
||||
}
|
||||
|
||||
|
||||
protected class FeedViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private FeedLayoutBinding binding;
|
||||
|
||||
public FeedViewHolder(FeedLayoutBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.feeds;
|
||||
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.FragmentFeedsBinding;
|
||||
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.FeedWithFolder;
|
||||
|
||||
import org.koin.android.compat.SharedViewModelCompat;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableCompletableObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
|
||||
public class FeedsFragment extends Fragment {
|
||||
|
||||
private FeedsAdapter adapter;
|
||||
private ManageFeedsFoldersViewModel viewModel;
|
||||
private Account account;
|
||||
|
||||
private FragmentFeedsBinding binding;
|
||||
|
||||
public FeedsFragment() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
public static FeedsFragment newInstance(Account account) {
|
||||
FeedsFragment fragment = new FeedsFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putParcelable(ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
if (account.getLogin() == null)
|
||||
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
|
||||
if (account.getPassword() == null)
|
||||
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
|
||||
|
||||
viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue();
|
||||
viewModel.setAccount(account);
|
||||
|
||||
viewModel.getFeedsWithFolder().observe(this, feedWithFolders -> {
|
||||
adapter.submitList(feedWithFolders);
|
||||
|
||||
if (feedWithFolders.size() > 0) {
|
||||
binding.feedsEmptyList.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.feedsEmptyList.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentFeedsBinding.inflate(inflater, container, false);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
binding.feedsRecyclerview.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
|
||||
adapter = new FeedsAdapter(new FeedsAdapter.ManageFeedsListener() {
|
||||
@Override
|
||||
public void onEdit(FeedWithFolder feedWithFolder) {
|
||||
openFeedOptionsFragment(feedWithFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenLink(FeedWithFolder feedWithFolder) {
|
||||
}
|
||||
});
|
||||
|
||||
binding.feedsRecyclerview.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
binding = null;
|
||||
}
|
||||
|
||||
public void deleteFeed(Feed feed) {
|
||||
new MaterialDialog.Builder(getContext())
|
||||
.title(R.string.delete_feed)
|
||||
.positiveText(R.string.validate)
|
||||
.negativeText(R.string.cancel)
|
||||
.onPositive((dialog, which) -> viewModel.deleteFeed(feed)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
Utils.showSnackbar(binding.feedsRoot,
|
||||
getString(R.string.feed_deleted, feed.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
String message;
|
||||
if (e instanceof Resources.NotFoundException)
|
||||
message = getString(R.string.feed_doesnt_exist, feed.getName());
|
||||
else
|
||||
message = getString(R.string.error_occured);
|
||||
|
||||
Utils.showSnackbar(binding.feedsRoot, message);
|
||||
}
|
||||
}))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openFeedOptionsFragment(FeedWithFolder feedWithFolder) {
|
||||
FeedOptionsDialogFragment dialogFragment = FeedOptionsDialogFragment.Companion.newInstance(feedWithFolder, account);
|
||||
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(dialogFragment, "")
|
||||
.commit();
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.folders
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.readrops.app.databinding.FolderOptionsLayoutBinding
|
||||
import com.readrops.db.entities.Folder
|
||||
|
||||
class FolderOptionsDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var folder: Folder
|
||||
|
||||
private var _binding: FolderOptionsLayoutBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
folder = arguments?.getParcelable(FOLDER_KEY)!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
_binding = FolderOptionsLayoutBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.folderOptionsTitle.text = folder.name
|
||||
binding.folderOptionsEdit.setOnClickListener { openEditFolderDialog() }
|
||||
binding.folderOptionsDelete.setOnClickListener { deleteFolder() }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun openEditFolderDialog() {
|
||||
dismiss()
|
||||
(parentFragment as FoldersFragment).editFolder(folder)
|
||||
}
|
||||
|
||||
private fun deleteFolder() {
|
||||
dismiss()
|
||||
(parentFragment as FoldersFragment).deleteFolder(folder)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FOLDER_KEY = "FOLDER_KEY"
|
||||
|
||||
fun newInstance(folder: Folder): FolderOptionsDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putParcelable(FOLDER_KEY, folder)
|
||||
|
||||
val fragment = FolderOptionsDialogFragment()
|
||||
fragment.arguments = args
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,106 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.folders;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.FolderLayoutBinding;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.pojo.FolderWithFeedCount;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FoldersAdapter extends ListAdapter<FolderWithFeedCount, FoldersAdapter.FolderViewHolder> {
|
||||
|
||||
private ManageFoldersListener listener;
|
||||
private int totalFeedCount;
|
||||
|
||||
public FoldersAdapter(ManageFoldersListener listener) {
|
||||
super(DIFF_CALLBACK);
|
||||
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setTotalFeedCount(int totalFeedCount) {
|
||||
this.totalFeedCount = totalFeedCount;
|
||||
}
|
||||
|
||||
private static final DiffUtil.ItemCallback<FolderWithFeedCount> DIFF_CALLBACK = new DiffUtil.ItemCallback<FolderWithFeedCount>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull FolderWithFeedCount oldItem, @NonNull FolderWithFeedCount newItem) {
|
||||
return oldItem.getFolder().getId() == newItem.getFolder().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull FolderWithFeedCount oldItem, @NonNull FolderWithFeedCount newItem) {
|
||||
return TextUtils.equals(oldItem.getFolder().getName(), newItem.getFolder().getName()) &&
|
||||
oldItem.getFeedCount() == newItem.getFeedCount();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object getChangePayload(@NonNull FolderWithFeedCount oldItem, @NonNull FolderWithFeedCount newItem) {
|
||||
return newItem;
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
FolderLayoutBinding binding = FolderLayoutBinding.inflate(LayoutInflater.from(parent.getContext()),
|
||||
parent, false);
|
||||
|
||||
return new FolderViewHolder(binding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty()) {
|
||||
FolderWithFeedCount folderWithFeedCount = (FolderWithFeedCount) payloads.get(0);
|
||||
|
||||
holder.bind(folderWithFeedCount);
|
||||
} else
|
||||
onBindViewHolder(holder, position);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) {
|
||||
FolderWithFeedCount folderWithFeedCount = getItem(position);
|
||||
|
||||
holder.bind(folderWithFeedCount);
|
||||
holder.itemView.setOnClickListener(v -> listener.onClick(folderWithFeedCount.getFolder()));
|
||||
}
|
||||
|
||||
public interface ManageFoldersListener {
|
||||
void onClick(Folder folder);
|
||||
}
|
||||
|
||||
public class FolderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private FolderLayoutBinding binding;
|
||||
|
||||
public FolderViewHolder(FolderLayoutBinding binding) {
|
||||
super(binding.getRoot());
|
||||
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
private void bind(FolderWithFeedCount folderWithFeedCount) {
|
||||
binding.folderName.setText(folderWithFeedCount.getFolder().getName());
|
||||
|
||||
int stringRes = folderWithFeedCount.getFeedCount() > 1 ? R.string.feeds_number : R.string.feed_number;
|
||||
binding.folderFeedsCount.setText(itemView.getContext().getString(stringRes, String.valueOf(folderWithFeedCount.getFeedCount())));
|
||||
|
||||
binding.folderProgressBar.setMax(totalFeedCount);
|
||||
binding.folderProgressBar.setProgress(folderWithFeedCount.getFeedCount());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
package com.readrops.app.feedsfolders.folders;
|
||||
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.api.utils.exceptions.ConflictException;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.FragmentFoldersBinding;
|
||||
import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import org.koin.android.compat.SharedViewModelCompat;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class FoldersFragment extends Fragment {
|
||||
|
||||
private FoldersAdapter adapter;
|
||||
private FragmentFoldersBinding binding;
|
||||
private ManageFeedsFoldersViewModel viewModel;
|
||||
|
||||
private Account account;
|
||||
|
||||
public FoldersFragment() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
public static FoldersFragment newInstance(Account account) {
|
||||
FoldersFragment fragment = new FoldersFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
if (account.getLogin() == null)
|
||||
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
|
||||
if (account.getPassword() == null)
|
||||
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
|
||||
|
||||
adapter = new FoldersAdapter(this::openFolderOptionsDialog);
|
||||
viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue();
|
||||
|
||||
viewModel.setAccount(account);
|
||||
viewModel.getFeedCountByAccount()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<Integer>() {
|
||||
@Override
|
||||
public void onSuccess(Integer feedCount) {
|
||||
adapter.setTotalFeedCount(feedCount);
|
||||
getFoldersWithFeedCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Utils.showSnackbar(binding.foldersRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getFoldersWithFeedCount() {
|
||||
viewModel.getFoldersWithFeedCount().observe(this, folders -> {
|
||||
adapter.submitList(folders);
|
||||
|
||||
if (!folders.isEmpty()) {
|
||||
binding.foldersEmptyList.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.foldersEmptyList.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentFoldersBinding.inflate(inflater, container, false);
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
binding.foldersList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
binding.foldersList.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void editFolder(Folder folder) {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.edit_folder)
|
||||
.positiveText(R.string.validate)
|
||||
.input(getString(R.string.folder), folder.getName(), false, (dialog, input) -> {
|
||||
folder.setName(input.toString());
|
||||
|
||||
viewModel.updateFolder(folder)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> {
|
||||
String message;
|
||||
if (throwable instanceof ConflictException)
|
||||
message = getString(R.string.folder_already_exists);
|
||||
else if (throwable instanceof UnknownFormatException)
|
||||
message = getString(R.string.folder_bad_format);
|
||||
else if (throwable instanceof Resources.NotFoundException)
|
||||
message = getString(R.string.folder_doesnt_exist);
|
||||
else
|
||||
message = getString(R.string.error_occured);
|
||||
|
||||
Utils.showSnackbar(binding.foldersRoot, message);
|
||||
})
|
||||
.subscribe();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public void deleteFolder(Folder folder) {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.delete_folder)
|
||||
.negativeText(R.string.cancel)
|
||||
.positiveText(R.string.validate)
|
||||
.onPositive((dialog, which) -> viewModel.deleteFolder(folder)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> {
|
||||
String message;
|
||||
if (throwable instanceof Resources.NotFoundException)
|
||||
message = getString(R.string.folder_doesnt_exist);
|
||||
else
|
||||
message = throwable.getMessage();
|
||||
|
||||
Utils.showSnackbar(binding.foldersRoot, message);
|
||||
})
|
||||
.subscribe())
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openFolderOptionsDialog(Folder folder) {
|
||||
FolderOptionsDialogFragment fragment = FolderOptionsDialogFragment.Companion.newInstance(folder);
|
||||
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(fragment, "")
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
package com.readrops.app.item;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ShareCompat;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.readrops.api.utils.DateUtils;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ActivityItemBinding;
|
||||
import com.readrops.app.utils.GlideRequests;
|
||||
import com.readrops.app.utils.PermissionManager;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR;
|
||||
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.WEB_URL;
|
||||
|
||||
public class ItemActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = ItemActivity.class.getSimpleName();
|
||||
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
|
||||
|
||||
private ActivityItemBinding binding;
|
||||
private ItemViewModel viewModel;
|
||||
|
||||
private ItemWithFeed itemWithFeed;
|
||||
|
||||
private boolean appBarCollapsed;
|
||||
|
||||
private String urlToDownload;
|
||||
private String imageTitle;
|
||||
|
||||
private boolean uiBinded;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
binding = ActivityItemBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
Intent intent = getIntent();
|
||||
int itemId = intent.getIntExtra(ITEM_ID, 0);
|
||||
String imageUrl = intent.getStringExtra(IMAGE_URL);
|
||||
Account account = intent.getParcelableExtra(ACCOUNT);
|
||||
|
||||
setSupportActionBar(binding.collapsingLayoutToolbar);
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
registerForContextMenu(binding.itemWebview);
|
||||
|
||||
if (imageUrl == null) {
|
||||
getSupportActionBar().setDisplayShowTitleEnabled(false);
|
||||
binding.collapsingLayout.setTitleEnabled(false);
|
||||
binding.collapsingLayoutScrim.setVisibility(View.GONE);
|
||||
} else {
|
||||
binding.appBarLayout.setExpanded(true);
|
||||
binding.collapsingLayout.setTitleEnabled(true);
|
||||
|
||||
KoinJavaComponent.<GlideRequests>get(GlideRequests.class)
|
||||
.load(imageUrl)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(binding.collapsingLayoutImage);
|
||||
}
|
||||
|
||||
final TypedArray styledAttributes = getTheme().obtainStyledAttributes(
|
||||
new int[]{android.R.attr.actionBarSize});
|
||||
int actionBarSize = (int) styledAttributes.getDimension(0, 0);
|
||||
styledAttributes.recycle();
|
||||
|
||||
binding.appBarLayout.addOnOffsetChangedListener(((appBarLayout1, i) -> {
|
||||
appBarCollapsed = Math.abs(i) >= (binding.appBarLayout.getTotalScrollRange() -
|
||||
actionBarSize - ((8 * binding.appBarLayout.getTotalScrollRange()) / 100));
|
||||
|
||||
invalidateOptionsMenu();
|
||||
}));
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class);
|
||||
viewModel.setAccount(account);
|
||||
viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> {
|
||||
if (!uiBinded) {
|
||||
bindUI(itemWithFeed1);
|
||||
uiBinded = true;
|
||||
}
|
||||
});
|
||||
|
||||
binding.activityItemFab.setOnClickListener(v -> openInNavigator());
|
||||
|
||||
binding.itemStarFab.setOnClickListener(v -> {
|
||||
Item item = itemWithFeed.getItem();
|
||||
|
||||
if (item.isStarred()) {
|
||||
binding.itemStarFab.setImageResource(R.drawable.ic_empty_star);
|
||||
} else {
|
||||
binding.itemStarFab.setImageResource(R.drawable.ic_star);
|
||||
}
|
||||
|
||||
item.setStarred(!item.isStarred());
|
||||
viewModel.setStarState(item)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.itemRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void bindUI(ItemWithFeed itemWithFeed) {
|
||||
this.itemWithFeed = itemWithFeed;
|
||||
Item item = itemWithFeed.getItem();
|
||||
|
||||
if (item.isStarred()) {
|
||||
binding.itemStarFab.setImageResource(R.drawable.ic_star);
|
||||
}
|
||||
|
||||
binding.activityItemDate.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate()));
|
||||
|
||||
if (item.getImageLink() == null)
|
||||
binding.collapsingLayoutToolbar.setTitle(itemWithFeed.getFeedName());
|
||||
else
|
||||
binding.collapsingLayout.setTitle(itemWithFeed.getFeedName());
|
||||
|
||||
if (itemWithFeed.getFolder() != null) {
|
||||
binding.collapsingLayoutToolbar.setSubtitle(itemWithFeed.getFolder().getName());
|
||||
}
|
||||
|
||||
binding.activityItemTitle.setText(item.getTitle());
|
||||
|
||||
if (itemWithFeed.getBgColor() != 0) {
|
||||
binding.activityItemTitle.setTextColor(itemWithFeed.getBgColor());
|
||||
Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getBgColor());
|
||||
} else if (itemWithFeed.getColor() != 0) {
|
||||
binding.activityItemTitle.setTextColor(itemWithFeed.getColor());
|
||||
Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getColor());
|
||||
}
|
||||
|
||||
if (item.getAuthor() != null && !item.getAuthor().isEmpty()) {
|
||||
binding.activityItemAuthor.setText(getString(R.string.by_author, item.getAuthor()));
|
||||
binding.activityItemAuthor.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (item.getReadTime() > 0) {
|
||||
int minutes = (int) Math.round(item.getReadTime());
|
||||
if (minutes < 1)
|
||||
binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_lower_than_1));
|
||||
else if (minutes > 1)
|
||||
binding.activityItemReadtime.setText(getResources().getString(R.string.read_time, String.valueOf(minutes)));
|
||||
else
|
||||
binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_one_minute));
|
||||
|
||||
binding.activityItemReadtimeLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
if (itemWithFeed.getBgColor() != 0) {
|
||||
binding.collapsingLayout.setBackgroundColor(itemWithFeed.getBgColor());
|
||||
binding.collapsingLayout.setContentScrimColor(itemWithFeed.getBgColor());
|
||||
binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getBgColor());
|
||||
|
||||
getWindow().setStatusBarColor(itemWithFeed.getBgColor());
|
||||
binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
|
||||
binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
|
||||
} else if (itemWithFeed.getColor() != 0) {
|
||||
binding.collapsingLayout.setBackgroundColor(itemWithFeed.getColor());
|
||||
binding.collapsingLayout.setContentScrimColor(itemWithFeed.getColor());
|
||||
binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getColor());
|
||||
|
||||
getWindow().setStatusBarColor(itemWithFeed.getColor());
|
||||
binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
|
||||
binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
|
||||
}
|
||||
|
||||
binding.itemWebview.setItem(itemWithFeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.item_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
MenuItem item = menu.findItem(R.id.item_open);
|
||||
item.setVisible(appBarCollapsed);
|
||||
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.item_share:
|
||||
shareArticle();
|
||||
return true;
|
||||
case R.id.item_open:
|
||||
openUrl();
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
finish();
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private void openUrl() {
|
||||
int value = Integer.parseInt(SharedPreferencesManager.readString(
|
||||
SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN));
|
||||
switch (value) {
|
||||
case 0:
|
||||
openInNavigator();
|
||||
break;
|
||||
case 1:
|
||||
openInWebView();
|
||||
break;
|
||||
default:
|
||||
openInCustomTab();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void openInNavigator() {
|
||||
Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(itemWithFeed.getItem().getLink()));
|
||||
startActivity(urlIntent);
|
||||
}
|
||||
|
||||
private void openInWebView() {
|
||||
Intent intent = new Intent(this, WebViewActivity.class);
|
||||
intent.putExtra(WEB_URL, itemWithFeed.getItem().getLink());
|
||||
intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor());
|
||||
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void openInCustomTab() {
|
||||
boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME));
|
||||
int color = itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor();
|
||||
|
||||
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
|
||||
.addDefaultShareMenuItem()
|
||||
.setToolbarColor(color)
|
||||
.setSecondaryToolbarColor(color)
|
||||
.setColorScheme(darkTheme ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT)
|
||||
.enableUrlBarHiding()
|
||||
.setShowTitle(true)
|
||||
.build();
|
||||
|
||||
customTabsIntent.launchUrl(this, Uri.parse(itemWithFeed.getItem().getLink()));
|
||||
}
|
||||
|
||||
private void shareArticle() {
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, itemWithFeed.getItem().getTitle() + " - " + itemWithFeed.getItem().getLink());
|
||||
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_article)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
WebView.HitTestResult hitTestResult = binding.itemWebview.getHitTestResult();
|
||||
|
||||
if (hitTestResult.getType() == WebView.HitTestResult.IMAGE_TYPE ||
|
||||
hitTestResult.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.image_options)
|
||||
.items(R.array.image_options)
|
||||
.itemsCallback((dialog, itemView, position, text) -> {
|
||||
switch (position) {
|
||||
case 0:
|
||||
shareImage(hitTestResult.getExtra());
|
||||
break;
|
||||
case 1:
|
||||
if (PermissionManager.isPermissionGranted(this, Manifest.permission.WRITE_EXTERNAL_STORAGE))
|
||||
downloadImage(hitTestResult.getExtra());
|
||||
else {
|
||||
urlToDownload = hitTestResult.getExtra();
|
||||
PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST, Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
urlToDownload = hitTestResult.getExtra();
|
||||
String content = binding.itemWebview.getItemContent();
|
||||
|
||||
Pattern p = Pattern.compile("(<img.*src=\"" + urlToDownload + "\".*>)");
|
||||
Matcher m = p.matcher(content);
|
||||
if (m.matches()) {
|
||||
Pattern p2 = Pattern.compile("<img.*(title|alt)=\"(.*?)\".*>");
|
||||
Matcher m2 = p2.matcher(content);
|
||||
if (m2.matches()) {
|
||||
imageTitle = m2.group(2);
|
||||
} else {
|
||||
imageTitle = "";
|
||||
}
|
||||
}
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(urlToDownload)
|
||||
.content(imageTitle)
|
||||
.show();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unexpected value: " + position);
|
||||
}
|
||||
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
downloadImage(urlToDownload);
|
||||
} else {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) {
|
||||
Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission),
|
||||
getString(R.string.try_again),
|
||||
v -> PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE));
|
||||
} else {
|
||||
Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission),
|
||||
getString(R.string.permissions), v -> {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", getPackageName(), null));
|
||||
startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void downloadImage(String url) {
|
||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
|
||||
.setTitle(getString(R.string.download_image))
|
||||
.setMimeType("image/png")
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "image.png");
|
||||
|
||||
request.allowScanningByMediaScanner();
|
||||
|
||||
DownloadManager downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
|
||||
downloadManager.enqueue(request);
|
||||
}
|
||||
|
||||
private void shareImage(String url) {
|
||||
KoinJavaComponent.<GlideRequests>get(GlideRequests.class)
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.load(url)
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
try {
|
||||
Uri uri = viewModel.saveImageInCache(resource, ItemActivity.this);
|
||||
Intent intent = ShareCompat.IntentBuilder.from(ItemActivity.this)
|
||||
.setType("image/png")
|
||||
.setStream(uri)
|
||||
.setChooserTitle(R.string.share_image)
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
|
||||
startActivity(intent);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
// not useful
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package com.readrops.app.item;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
import com.readrops.db.queries.ItemSelectionQueryBuilder;
|
||||
|
||||
import org.koin.core.parameter.ParametersHolderKt;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
|
||||
public class ItemViewModel extends ViewModel {
|
||||
|
||||
private final Database database;
|
||||
private Account account;
|
||||
|
||||
public ItemViewModel(@NonNull Database database) {
|
||||
this.database = database;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public LiveData<ItemWithFeed> getItemById(int id) {
|
||||
return database.itemDao().getItemById(ItemSelectionQueryBuilder.buildQuery(id,
|
||||
account.getConfig().getUseSeparateState()));
|
||||
}
|
||||
|
||||
public Completable setStarState(Item item) {
|
||||
ARepository repository = KoinJavaComponent.get(ARepository.class, null,
|
||||
() -> ParametersHolderKt.parametersOf(account));
|
||||
|
||||
return repository.setItemStarState(item);
|
||||
}
|
||||
|
||||
public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException {
|
||||
File imagesFolder = new File(context.getCacheDir().getAbsolutePath(), "images");
|
||||
|
||||
if (!imagesFolder.exists())
|
||||
imagesFolder.mkdirs();
|
||||
|
||||
File image = new File(imagesFolder, "shared_image.png");
|
||||
OutputStream stream = new FileOutputStream(image);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
|
||||
|
||||
stream.flush();
|
||||
stream.close();
|
||||
|
||||
return FileProvider.getUriForFile(context, context.getPackageName(), image);
|
||||
}
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package com.readrops.app.item
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.webkit.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.databinding.ActivityWebViewBinding
|
||||
import com.readrops.app.utils.ReadropsKeys
|
||||
import com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR
|
||||
|
||||
class WebViewActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityWebViewBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityWebViewBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.activityWebViewToolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
title = ""
|
||||
|
||||
val actionBarColor = intent.getIntExtra(ACTION_BAR_COLOR, ContextCompat.getColor(this, R.color.colorPrimary))
|
||||
supportActionBar?.setBackgroundDrawable(ColorDrawable(actionBarColor))
|
||||
setWebViewSettings()
|
||||
|
||||
with(binding) {
|
||||
activityWebViewSwipe.setOnRefreshListener { binding.webView.reload() }
|
||||
activityWebViewProgress.progressTintList = ColorStateList.valueOf(actionBarColor)
|
||||
activityWebViewProgress.max = 100
|
||||
|
||||
val url: String = intent.getStringExtra(ReadropsKeys.WEB_URL)!!
|
||||
webView.loadUrl(url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun setWebViewSettings() {
|
||||
val settings: WebSettings = binding.webView.settings
|
||||
|
||||
settings.javaScriptEnabled = true
|
||||
settings.setSupportZoom(true)
|
||||
|
||||
binding.webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
binding.webView.loadUrl(request?.url.toString())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
with(binding) {
|
||||
activityWebViewSwipe.isRefreshing = false
|
||||
activityWebViewProgress.progress = 0
|
||||
activityWebViewProgress.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
super.onPageStarted(view, url, favicon)
|
||||
}
|
||||
}
|
||||
|
||||
binding.webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
||||
setTitle(title)
|
||||
supportActionBar?.subtitle = Uri.parse(view?.url).host
|
||||
|
||||
super.onReceivedTitle(view, title)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
with(binding) {
|
||||
activityWebViewProgress.progress = newProgress
|
||||
if (newProgress == 100) activityWebViewProgress.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
||||
super.onProgressChanged(view, newProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (binding.webView.canGoBack())
|
||||
binding.webView.goBack()
|
||||
else
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (binding.webView.canGoBack())
|
||||
binding.webView.goBack()
|
||||
else
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
R.id.web_view_refresh -> {
|
||||
binding.webView.reload()
|
||||
}
|
||||
R.id.web_view_share -> {
|
||||
shareLink()
|
||||
}
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun shareLink() {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, binding.webView.url.toString())
|
||||
}
|
||||
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_url)))
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.webview_menu, menu)
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -1,412 +0,0 @@
|
|||
package com.readrops.app.itemslist;
|
||||
|
||||
import static com.readrops.app.utils.Utils.drawableWithColor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.mikepenz.fastadapter.FastAdapter;
|
||||
import com.mikepenz.fastadapter.expandable.ExpandableExtension;
|
||||
import com.mikepenz.fastadapter.listeners.ClickEventHook;
|
||||
import com.mikepenz.fastadapter.select.SelectExtension;
|
||||
import com.mikepenz.materialdrawer.AccountHeader;
|
||||
import com.mikepenz.materialdrawer.AccountHeaderBuilder;
|
||||
import com.mikepenz.materialdrawer.Drawer;
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder;
|
||||
import com.mikepenz.materialdrawer.holder.ImageHolder;
|
||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DrawerManager {
|
||||
|
||||
public static final int ARTICLES_ITEM_ID = -5;
|
||||
public static final int READ_LATER_ID = -6;
|
||||
public static final int STARS_ID = -10;
|
||||
public static final int ADD_ACCOUNT_ID = -4;
|
||||
public static final int ABOUT_ID = -7;
|
||||
public static final int SETTINGS_ID = -8;
|
||||
public static final int ACCOUNT_SETTINGS_ID = -9;
|
||||
|
||||
private Activity activity;
|
||||
private Toolbar toolbar;
|
||||
private Drawer drawer;
|
||||
private FastAdapter<IDrawerItem> adapter;
|
||||
|
||||
private AccountHeader header;
|
||||
private Drawer.OnDrawerItemClickListener listener;
|
||||
private AccountHeader.OnAccountHeaderListener headerListener;
|
||||
|
||||
public DrawerManager(Activity activity, Toolbar toolbar, Drawer.OnDrawerItemClickListener listener) {
|
||||
this.activity = activity;
|
||||
this.listener = listener;
|
||||
this.toolbar = toolbar;
|
||||
}
|
||||
|
||||
public void setHeaderListener(AccountHeader.OnAccountHeaderListener headerListener) {
|
||||
this.headerListener = headerListener;
|
||||
}
|
||||
|
||||
public Drawer buildDrawer(List<Account> accounts, int currentAccountId) {
|
||||
createAccountHeader(accounts, currentAccountId);
|
||||
|
||||
drawer = new DrawerBuilder()
|
||||
.withActivity(activity)
|
||||
.withToolbar(toolbar)
|
||||
.withAccountHeader(header)
|
||||
.withSelectedItem(DrawerManager.ARTICLES_ITEM_ID)
|
||||
.withOnDrawerItemClickListener(listener)
|
||||
.build();
|
||||
|
||||
adapter = drawer.getAdapter();
|
||||
buildFastAdapter();
|
||||
|
||||
addDefaultPlaces();
|
||||
|
||||
return drawer;
|
||||
}
|
||||
|
||||
public void buildFastAdapter() {
|
||||
// Folder click
|
||||
adapter.withEventHook(new ClickEventHook<IDrawerItem>() {
|
||||
@Override
|
||||
public void onClick(@NonNull View v, int position, @NonNull FastAdapter<IDrawerItem> fastAdapter, @NonNull IDrawerItem item) {
|
||||
SelectExtension selectExtension = adapter.getExtension(SelectExtension.class);
|
||||
|
||||
selectExtension.deselect(selectExtension.getSelections());
|
||||
|
||||
if (!item.isSelected()) {
|
||||
selectExtension.select(position);
|
||||
}
|
||||
|
||||
listener.onItemClick(v, position, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<View> onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) {
|
||||
CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder;
|
||||
|
||||
return Arrays.asList(new View[]{
|
||||
expandableViewHolder.itemView.findViewById(R.id.expandable_item_container),
|
||||
expandableViewHolder.itemView.findViewById(R.id.material_drawer_icon),
|
||||
expandableViewHolder.itemView.findViewById(R.id.material_drawer_name),
|
||||
expandableViewHolder.itemView.findViewById(R.id.material_drawer_description)
|
||||
}.clone());
|
||||
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Expandable click
|
||||
adapter.withEventHook(new ClickEventHook<IDrawerItem>() {
|
||||
@Override
|
||||
public void onClick(@NonNull View v, int position, @NonNull FastAdapter<IDrawerItem> fastAdapter, @NonNull IDrawerItem item) {
|
||||
ExpandableExtension expandableExtension = adapter.getExtension(ExpandableExtension.class);
|
||||
|
||||
expandableExtension.toggleExpandable(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<View> onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) {
|
||||
CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder;
|
||||
|
||||
return Arrays.asList(new View[]{
|
||||
expandableViewHolder.badge,
|
||||
expandableViewHolder.badgeContainer,
|
||||
expandableViewHolder.arrow,
|
||||
expandableViewHolder.itemView.findViewById(R.id.material_drawer_arrow_container)
|
||||
}.clone());
|
||||
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void updateDrawer(Map<Folder, List<Feed>> folderListMap) {
|
||||
drawer.removeAllItems();
|
||||
drawer.removeAllStickyFooterItems();
|
||||
|
||||
addDefaultPlaces();
|
||||
|
||||
Map<SecondaryDrawerItem, Feed> feedsWithoutFolder = new HashMap<>();
|
||||
boolean hideFeeds = SharedPreferencesManager
|
||||
.readBoolean(SharedPreferencesManager.SharedPrefKey.HIDE_FEEDS);
|
||||
|
||||
for (Map.Entry<Folder, List<Feed>> entry : folderListMap.entrySet()) {
|
||||
Folder folder = entry.getKey();
|
||||
if (folder != null) {
|
||||
CustomExpandableBadgeDrawerItem badgeDrawerItem = new CustomExpandableBadgeDrawerItem()
|
||||
.withIdentifier(folder.getId() * 1000L) // to avoid any id conflict with other items
|
||||
.withName(folder.getName())
|
||||
.withIcon(R.drawable.ic_folder_grey);
|
||||
|
||||
List<IDrawerItem> secondaryDrawerItems = new ArrayList<>();
|
||||
int expandableUnreadCount = 0;
|
||||
|
||||
for (Feed feed : entry.getValue()) {
|
||||
expandableUnreadCount += feed.getUnreadCount();
|
||||
|
||||
SecondaryDrawerItem secondaryDrawerItem = createSecondaryItem(feed);
|
||||
|
||||
if (hideFeeds) {
|
||||
if (feed.getUnreadCount() > 0) {
|
||||
secondaryDrawerItems.add(secondaryDrawerItem);
|
||||
}
|
||||
} else {
|
||||
secondaryDrawerItems.add(secondaryDrawerItem);
|
||||
}
|
||||
|
||||
loadItemIcon(secondaryDrawerItem, feed);
|
||||
}
|
||||
|
||||
boolean showItem;
|
||||
if (hideFeeds) {
|
||||
showItem = expandableUnreadCount > 0;
|
||||
} else {
|
||||
showItem = true;
|
||||
}
|
||||
|
||||
if (!secondaryDrawerItems.isEmpty() && showItem) {
|
||||
badgeDrawerItem.withSubItems(secondaryDrawerItems);
|
||||
badgeDrawerItem.withBadge(String.valueOf(expandableUnreadCount));
|
||||
drawer.addItem(badgeDrawerItem);
|
||||
}
|
||||
} else { // no folder case, items to add after the folders
|
||||
for (Feed feed : folderListMap.get(folder)) {
|
||||
SecondaryDrawerItem secondaryItem = createSecondaryItem(feed);
|
||||
feedsWithoutFolder.put(secondaryItem, feed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// work-around as MaterialDrawer doesn't accept an item list
|
||||
for (Map.Entry<SecondaryDrawerItem, Feed> entry : feedsWithoutFolder.entrySet()) {
|
||||
drawer.addItem(entry.getKey());
|
||||
loadItemIcon(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void createAccountHeader(List<Account> accounts, int currentAccountId) {
|
||||
ProfileDrawerItem[] profileItems = new ProfileDrawerItem[accounts.size()];
|
||||
|
||||
for (int i = 0; i < accounts.size(); i++) {
|
||||
Account account = accounts.get(i);
|
||||
|
||||
// if currentAccount > 0, it means that the current account is no longer
|
||||
if (account.isCurrentAccount() && currentAccountId == 0)
|
||||
currentAccountId = account.getId();
|
||||
|
||||
ProfileDrawerItem profileItem = createProfileItem(account);
|
||||
profileItems[i] = profileItem;
|
||||
}
|
||||
|
||||
header = new AccountHeaderBuilder()
|
||||
.withActivity(activity)
|
||||
.addProfiles(profileItems)
|
||||
.withDividerBelowHeader(false)
|
||||
.withAlternativeProfileHeaderSwitching(true)
|
||||
.withCurrentProfileHiddenInList(true)
|
||||
.withTextColorRes(R.color.colorBackground)
|
||||
.withHeaderBackground(R.drawable.header_background)
|
||||
.withHeaderBackgroundScaleType(ImageView.ScaleType.CENTER_CROP)
|
||||
.withOnAccountHeaderListener(headerListener)
|
||||
.build();
|
||||
|
||||
addProfileSettingItems();
|
||||
|
||||
header.setActiveProfile(currentAccountId);
|
||||
}
|
||||
|
||||
private ProfileDrawerItem createProfileItem(Account account) {
|
||||
return new ProfileDrawerItem()
|
||||
.withIcon(account.getAccountType().getIconRes())
|
||||
.withName(account.getDisplayedName())
|
||||
.withEmail(account.getAccountName())
|
||||
.withIdentifier(account.getId());
|
||||
}
|
||||
|
||||
private SecondaryDrawerItem createSecondaryItem(Feed feed) {
|
||||
int color = feed.getTextColor();
|
||||
|
||||
return new SecondaryDrawerItem()
|
||||
.withName(feed.getName())
|
||||
.withBadge(String.valueOf(feed.getUnreadCount()))
|
||||
.withIcon(color != 0 ? drawableWithColor(color) : drawableWithColor(activity.getResources().getColor(R.color.colorPrimary)))
|
||||
.withIdentifier(feed.getId());
|
||||
}
|
||||
|
||||
private void loadItemIcon(SecondaryDrawerItem secondaryDrawerItem, Feed feed) {
|
||||
Glide.with(activity)
|
||||
.load(feed.getIconUrl())
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(new CustomTarget<Drawable>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
|
||||
drawer.updateIcon(secondaryDrawerItem.getIdentifier(), new ImageHolder(resource));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
// no need of this method
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addDefaultPlaces() {
|
||||
PrimaryDrawerItem articles = new PrimaryDrawerItem()
|
||||
.withName(R.string.articles)
|
||||
.withIcon(R.drawable.ic_rss_feed_grey)
|
||||
.withSelectable(true)
|
||||
.withIdentifier(ARTICLES_ITEM_ID);
|
||||
|
||||
PrimaryDrawerItem toReadLater = new PrimaryDrawerItem()
|
||||
.withName(R.string.read_later)
|
||||
.withIcon(R.drawable.ic_read_later)
|
||||
.withSelectable(true)
|
||||
.withIdentifier(READ_LATER_ID);
|
||||
|
||||
PrimaryDrawerItem favorites = new PrimaryDrawerItem()
|
||||
.withName(R.string.favorites)
|
||||
.withIcon(R.drawable.ic_star)
|
||||
.withSelectable(true)
|
||||
.withIdentifier(STARS_ID);
|
||||
|
||||
PrimaryDrawerItem aboutItem = new PrimaryDrawerItem()
|
||||
.withName(R.string.about)
|
||||
.withIcon(R.drawable.ic_about_grey)
|
||||
.withSelectable(false)
|
||||
.withIdentifier(ABOUT_ID);
|
||||
|
||||
PrimaryDrawerItem settingsItem = new PrimaryDrawerItem()
|
||||
.withName(R.string.settings)
|
||||
.withIcon(R.drawable.ic_settings)
|
||||
.withSelectable(false)
|
||||
.withIdentifier(SETTINGS_ID);
|
||||
|
||||
drawer.addStickyFooterItem(settingsItem);
|
||||
drawer.addStickyFooterItem(aboutItem);
|
||||
|
||||
drawer.addItem(articles);
|
||||
drawer.addItem(favorites);
|
||||
drawer.addItem(toReadLater);
|
||||
drawer.addItem(new DividerDrawerItem());
|
||||
}
|
||||
|
||||
private void addProfileSettingItems() {
|
||||
ProfileSettingDrawerItem accountSettingsItem = new ProfileSettingDrawerItem()
|
||||
.withName(R.string.account_settings)
|
||||
.withIcon(R.drawable.ic_settings)
|
||||
.withIdentifier(ACCOUNT_SETTINGS_ID);
|
||||
|
||||
ProfileSettingDrawerItem addAccountSettingsItem = new ProfileSettingDrawerItem()
|
||||
.withName(R.string.add_account)
|
||||
.withIcon(R.drawable.ic_add_account_grey)
|
||||
.withIdentifier(ADD_ACCOUNT_ID);
|
||||
|
||||
header.addProfiles(accountSettingsItem, addAccountSettingsItem);
|
||||
}
|
||||
|
||||
public void addAccount(Account account, boolean currentProfile) {
|
||||
ProfileDrawerItem profileItem = createProfileItem(account);
|
||||
|
||||
header.addProfiles(profileItem);
|
||||
|
||||
if (currentProfile)
|
||||
header.setActiveProfile(profileItem.getIdentifier());
|
||||
}
|
||||
|
||||
public void setAccount(int accountId) {
|
||||
header.setActiveProfile(accountId);
|
||||
}
|
||||
|
||||
public void updateHeader(List<Account> accounts) {
|
||||
header.clear();
|
||||
addProfileSettingItems();
|
||||
|
||||
for (Account account : accounts) {
|
||||
addAccount(account, account.isCurrentAccount());
|
||||
}
|
||||
}
|
||||
|
||||
public int getNumberOfProfiles() {
|
||||
List<IProfile> profiles = header.getProfiles();
|
||||
|
||||
int number = 0;
|
||||
for (IProfile profile : profiles) {
|
||||
if (profile instanceof ProfileDrawerItem)
|
||||
number++;
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
public void resetItems() {
|
||||
drawer.removeAllItems();
|
||||
drawer.removeAllStickyFooterItems();
|
||||
addDefaultPlaces();
|
||||
}
|
||||
|
||||
public void disableAccountSelection() {
|
||||
List<IProfile> profiles = header.getProfiles();
|
||||
|
||||
for (IProfile profile : profiles) {
|
||||
if (profile.getIdentifier() != header.getActiveProfile().getIdentifier() && !(profile instanceof ProfileSettingDrawerItem)) {
|
||||
profile.withSelectable(false);
|
||||
header.updateProfile(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void enableAccountSelection() {
|
||||
List<IProfile> profiles = header.getProfiles();
|
||||
|
||||
for (IProfile profile : profiles) {
|
||||
if (profile.getIdentifier() != header.getActiveProfile().getIdentifier() && !(profile instanceof ProfileSettingDrawerItem)) {
|
||||
profile.withSelectable(true);
|
||||
header.updateProfile(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDrawerSelection(long identifier) {
|
||||
drawer.setSelection(identifier);
|
||||
}
|
||||
|
||||
public long getCurrentSelection() {
|
||||
return drawer.getCurrentSelection();
|
||||
}
|
||||
}
|
|
@ -1,868 +0,0 @@
|
|||
package com.readrops.app.itemslist;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
|
||||
import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
|
||||
import static com.readrops.app.utils.ReadropsKeys.SYNCING;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.mikepenz.aboutlibraries.Libs;
|
||||
import com.mikepenz.aboutlibraries.LibsBuilder;
|
||||
import com.mikepenz.aboutlibraries.LibsConfiguration;
|
||||
import com.mikepenz.aboutlibraries.entity.Library;
|
||||
import com.mikepenz.materialdrawer.Drawer;
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.account.AccountTypeListActivity;
|
||||
import com.readrops.app.account.AccountViewModel;
|
||||
import com.readrops.app.addfeed.AddFeedActivity;
|
||||
import com.readrops.app.databinding.ActivityMainBinding;
|
||||
import com.readrops.app.item.ItemActivity;
|
||||
import com.readrops.app.settings.SettingsActivity;
|
||||
import com.readrops.app.utils.GlideRequests;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem;
|
||||
import com.readrops.app.utils.customviews.ReadropsItemTouchCallback;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.filters.MainFilter;
|
||||
import com.readrops.db.filters.ListSortType;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.CompletableObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener,
|
||||
ReadropsItemTouchCallback.SwipeCallback, ActionMode.Callback {
|
||||
|
||||
public static final String TAG = MainActivity.class.getSimpleName();
|
||||
|
||||
public static final int ADD_FEED_REQUEST = 1;
|
||||
public static final int MANAGE_ACCOUNT_REQUEST = 2;
|
||||
public static final int ITEM_REQUEST = 3;
|
||||
public static final int ADD_ACCOUNT_REQUEST = 4;
|
||||
public static final int SETTINGS_REQUEST = 5;
|
||||
|
||||
private ActivityMainBinding binding;
|
||||
private MainItemListAdapter adapter;
|
||||
|
||||
private Drawer drawer;
|
||||
|
||||
private PagedList<ItemWithFeed> allItems;
|
||||
|
||||
private MainViewModel viewModel;
|
||||
private DrawerManager drawerManager;
|
||||
|
||||
private int feedCount;
|
||||
private int feedNb;
|
||||
private boolean scrollToTop;
|
||||
private boolean allItemsSelected;
|
||||
private boolean updating;
|
||||
|
||||
private ActionMode actionMode;
|
||||
private Disposable syncDisposable;
|
||||
|
||||
private ItemWithFeed selectedItemWithFeed;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
setTheme(R.style.AppTheme_NoActionBar);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// surely a better way to do this, but hopefully this code will be replaced with jetpack compose
|
||||
AccountViewModel accountViewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
|
||||
int accountCount = accountViewModel.getAccountCount()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
|
||||
if (accountCount == 0) {
|
||||
Intent intent = new Intent(getApplicationContext(), AccountTypeListActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
|
||||
setContentView(binding.getRoot());
|
||||
setSupportActionBar(binding.toolbarMain);
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener(this);
|
||||
|
||||
feedCount = 0;
|
||||
initRecyclerView();
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, MainViewModel.class);
|
||||
|
||||
viewModel.getItemsWithFeed().observe(this, itemWithFeeds -> {
|
||||
allItems = itemWithFeeds;
|
||||
|
||||
if (!itemWithFeeds.isEmpty())
|
||||
binding.emptyListLayout.setVisibility(View.GONE);
|
||||
else
|
||||
binding.emptyListLayout.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!binding.swipeRefreshLayout.isRefreshing())
|
||||
adapter.submitList(itemWithFeeds);
|
||||
});
|
||||
|
||||
drawerManager = new DrawerManager(this, binding.toolbarMain, (view, position, drawerItem) -> {
|
||||
handleDrawerClick(drawerItem);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
drawerManager.setHeaderListener((view, profile, current) -> {
|
||||
if (!current) {
|
||||
int id = (int) profile.getIdentifier();
|
||||
|
||||
switch (id) {
|
||||
case DrawerManager.ADD_ACCOUNT_ID:
|
||||
Intent intent = new Intent(this, AccountTypeListActivity.class);
|
||||
intent.putExtra(FROM_MAIN_ACTIVITY, true);
|
||||
startActivityForResult(intent, ADD_ACCOUNT_REQUEST);
|
||||
break;
|
||||
case DrawerManager.ACCOUNT_SETTINGS_ID:
|
||||
Intent intent1 = new Intent(this, SettingsActivity.class);
|
||||
intent1.putExtra(SETTINGS,
|
||||
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
|
||||
intent1.putExtra(ACCOUNT, viewModel.getCurrentAccount());
|
||||
startActivity(intent1);
|
||||
break;
|
||||
default:
|
||||
if (!updating) {
|
||||
viewModel.setCurrentAccount(id);
|
||||
updateDrawerFeeds();
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
intent.putExtra(SETTINGS,
|
||||
SettingsActivity.SettingsKey.ACCOUNT_SETTINGS.ordinal());
|
||||
intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
|
||||
startActivityForResult(intent, MANAGE_ACCOUNT_REQUEST);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
Account currentAccount = getIntent().getParcelableExtra(ACCOUNT);
|
||||
WeakReference<Account> accountWeakReference = new WeakReference<>(currentAccount);
|
||||
|
||||
viewModel.getAllAccounts().observe(this, accounts -> {
|
||||
getAccountCredentials(accounts);
|
||||
viewModel.setAccounts(accounts);
|
||||
|
||||
// the activity was just opened
|
||||
if (drawer == null) {
|
||||
int currentAccountId = 0;
|
||||
if (getIntent().hasExtra(ACCOUNT_ID)) { // coming from a notification
|
||||
currentAccountId = getIntent().getIntExtra(ACCOUNT_ID, 1);
|
||||
viewModel.setCurrentAccount(currentAccountId);
|
||||
}
|
||||
|
||||
drawer = drawerManager.buildDrawer(accounts, currentAccountId);
|
||||
drawer.setSelection(DrawerManager.ARTICLES_ITEM_ID);
|
||||
updateDrawerFeeds();
|
||||
|
||||
openItemActivity(getIntent());
|
||||
} else if (accounts.size() < drawerManager.getNumberOfProfiles() && !accounts.isEmpty()) {
|
||||
drawerManager.updateHeader(accounts);
|
||||
updateDrawerFeeds();
|
||||
} else if (accounts.isEmpty()) {
|
||||
Intent intent = new Intent(this, AccountTypeListActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
if (accountWeakReference.get() != null && !accountWeakReference.get().isLocal()) {
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
onRefresh();
|
||||
accountWeakReference.clear();
|
||||
} else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING)) {
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
onRefresh();
|
||||
savedInstanceState.clear();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
openItemActivity(intent);
|
||||
}
|
||||
|
||||
private void openItemActivity(Intent intent) {
|
||||
if (intent.hasExtra(ITEM_ID) && intent.hasExtra(IMAGE_URL)) {
|
||||
Intent itemIntent = new Intent(this, ItemActivity.class);
|
||||
itemIntent.putExtras(intent);
|
||||
itemIntent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
|
||||
|
||||
startActivity(itemIntent);
|
||||
|
||||
Item item = new Item();
|
||||
item.setId(intent.getIntExtra(ITEM_ID, 0));
|
||||
item.setRead(true);
|
||||
|
||||
viewModel.setItemReadState(item)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDrawerClick(IDrawerItem drawerItem) {
|
||||
if (drawerItem instanceof PrimaryDrawerItem) {
|
||||
drawer.closeDrawer();
|
||||
int id = (int) drawerItem.getIdentifier();
|
||||
|
||||
switch (id) {
|
||||
default:
|
||||
case DrawerManager.ARTICLES_ITEM_ID:
|
||||
viewModel.setFilterType(MainFilter.ALL);
|
||||
scrollToTop = true;
|
||||
viewModel.invalidate();
|
||||
setTitle(R.string.articles);
|
||||
break;
|
||||
case DrawerManager.READ_LATER_ID:
|
||||
//viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER);
|
||||
viewModel.invalidate();
|
||||
setTitle(R.string.read_later);
|
||||
break;
|
||||
case DrawerManager.STARS_ID:
|
||||
viewModel.setFilterType(MainFilter.STARS);
|
||||
viewModel.invalidate();
|
||||
setTitle(R.string.favorites);
|
||||
break;
|
||||
case DrawerManager.ABOUT_ID:
|
||||
startAboutActivity();
|
||||
break;
|
||||
case DrawerManager.SETTINGS_ID:
|
||||
Intent intent = new Intent(getApplication(), SettingsActivity.class);
|
||||
intent.putExtra(SETTINGS,
|
||||
SettingsActivity.SettingsKey.SETTINGS.ordinal());
|
||||
startActivityForResult(intent, SETTINGS_REQUEST);
|
||||
break;
|
||||
}
|
||||
} else if (drawerItem instanceof SecondaryDrawerItem) {
|
||||
drawer.closeDrawer();
|
||||
|
||||
viewModel.setFilterFeedId((int) drawerItem.getIdentifier());
|
||||
viewModel.setFilterType(MainFilter.ALL);
|
||||
viewModel.invalidate();
|
||||
setTitle(((SecondaryDrawerItem) drawerItem).getName().getText());
|
||||
} else if (drawerItem instanceof CustomExpandableBadgeDrawerItem) {
|
||||
drawer.closeDrawer();
|
||||
|
||||
viewModel.setFilerFolderId((int) (drawerItem.getIdentifier() / 1000));
|
||||
viewModel.setFilterType(MainFilter.ALL);
|
||||
viewModel.invalidate();
|
||||
setTitle(((CustomExpandableBadgeDrawerItem) drawerItem).getName().getText());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDrawerFeeds() {
|
||||
viewModel.getFoldersWithFeeds()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<Map<Folder, List<Feed>>>() {
|
||||
@Override
|
||||
public void onSuccess(Map<Folder, List<Feed>> folderListHashMap) {
|
||||
drawerManager.updateDrawer(folderListHashMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Utils.showSnackbar(binding.mainRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (drawer.isDrawerOpen())
|
||||
drawer.closeDrawer();
|
||||
else
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
private void initRecyclerView() {
|
||||
ViewPreloadSizeProvider preloadSizeProvider = new ViewPreloadSizeProvider();
|
||||
adapter = new MainItemListAdapter(KoinJavaComponent.get(GlideRequests.class), preloadSizeProvider);
|
||||
adapter.setOnItemClickListener(new MainItemListAdapter.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(ItemWithFeed itemWithFeed, int position) {
|
||||
if (actionMode == null) {
|
||||
Intent intent = new Intent(getApplicationContext(), ItemActivity.class);
|
||||
|
||||
intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId());
|
||||
intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink());
|
||||
intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
|
||||
|
||||
startActivityForResult(intent, ITEM_REQUEST);
|
||||
|
||||
itemWithFeed.getItem().setRead(true);
|
||||
viewModel.setItemReadState(itemWithFeed)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
|
||||
adapter.notifyItemChanged(position, itemWithFeed);
|
||||
updateDrawerFeeds();
|
||||
} else {
|
||||
adapter.toggleSelection(position);
|
||||
int selectionSize = adapter.getSelection().size();
|
||||
|
||||
if (selectionSize > 0)
|
||||
actionMode.setTitle(String.valueOf(selectionSize));
|
||||
else
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemLongClick(ItemWithFeed itemWithFeed, int position) {
|
||||
if (actionMode != null || binding.swipeRefreshLayout.isRefreshing())
|
||||
return;
|
||||
|
||||
selectedItemWithFeed = itemWithFeed;
|
||||
adapter.toggleSelection(position);
|
||||
|
||||
actionMode = startActionMode(MainActivity.this);
|
||||
actionMode.setTitle(String.valueOf(adapter.getSelection().size()));
|
||||
}
|
||||
});
|
||||
|
||||
RecyclerViewPreloader<String> preloader = new RecyclerViewPreloader<String>(Glide.with(this), adapter, preloadSizeProvider, 10);
|
||||
binding.itemsRecyclerView.addOnScrollListener(preloader);
|
||||
|
||||
binding.itemsRecyclerView.addRecyclerListener(viewHolder -> {
|
||||
MainItemListAdapter.ItemViewHolder vh = (MainItemListAdapter.ItemViewHolder) viewHolder;
|
||||
KoinJavaComponent.<GlideRequests>get(GlideRequests.class).clear(vh.getItemImage());
|
||||
});
|
||||
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
binding.itemsRecyclerView.setLayoutManager(layoutManager);
|
||||
|
||||
DividerItemDecoration decoration = new DividerItemDecoration(this, layoutManager.getOrientation());
|
||||
binding.itemsRecyclerView.addItemDecoration(decoration);
|
||||
|
||||
binding.itemsRecyclerView.setAdapter(adapter);
|
||||
|
||||
|
||||
Drawable readLater = ContextCompat.getDrawable(this, R.drawable.ic_read_later).mutate();
|
||||
DrawableCompat.setTint(readLater, ContextCompat.getColor(this, android.R.color.white));
|
||||
|
||||
new ItemTouchHelper(new ReadropsItemTouchCallback(this,
|
||||
new ReadropsItemTouchCallback.Config.Builder()
|
||||
.swipeDirs(ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT)
|
||||
.swipeCallback(this)
|
||||
.leftDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read_later, readLater)
|
||||
.rightDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read, null)
|
||||
.build()))
|
||||
.attachToRecyclerView(binding.itemsRecyclerView);
|
||||
|
||||
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
if (scrollToTop) {
|
||||
binding.itemsRecyclerView.scrollToPosition(0);
|
||||
scrollToTop = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
if (scrollToTop) {
|
||||
binding.itemsRecyclerView.scrollToPosition(0);
|
||||
scrollToTop = false;
|
||||
} else
|
||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
|
||||
}
|
||||
});
|
||||
|
||||
binding.itemsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy > 0) {
|
||||
binding.addFeedFab.hide();
|
||||
} else {
|
||||
binding.addFeedFab.show();
|
||||
}
|
||||
|
||||
int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
|
||||
if (firstVisibleItemPosition - 2 >= 0) {
|
||||
Item item = adapter.getItemWithFeed(firstVisibleItemPosition - 2).getItem();
|
||||
|
||||
// Might be better to have a global variable updated when going back from settings
|
||||
if (!item.isRead() && SharedPreferencesManager.readBoolean(SharedPreferencesManager
|
||||
.SharedPrefKey.MARK_ITEMS_READ_ON_SCROLL)) {
|
||||
item.setRead(!item.isRead());
|
||||
|
||||
viewModel.setItemReadState(item)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwipe(@NotNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
Item item = adapter.getItemWithFeed(viewHolder.getBindingAdapterPosition()).getItem();
|
||||
|
||||
if (direction == ItemTouchHelper.LEFT) { // set item read state
|
||||
item.setRead(!item.isRead());
|
||||
|
||||
viewModel.setItemReadState(item)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
|
||||
} else { // set item read it later state
|
||||
item.setReadItLater(!item.isReadItLater());
|
||||
|
||||
viewModel.setItemReadItLater(item.isReadItLater(), item.getId())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
|
||||
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
binding.swipeRefreshLayout.setEnabled(false);
|
||||
|
||||
actionMode.getMenuInflater().inflate(R.menu.item_list_contextual_menu, menu);
|
||||
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.primary_dark));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
|
||||
menu.findItem(R.id.item_mark_read).setVisible(!selectedItemWithFeed.getItem().isRead());
|
||||
menu.findItem(R.id.item_mark_unread).setVisible(selectedItemWithFeed.getItem().isRead());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
|
||||
int itemId = menuItem.getItemId();
|
||||
|
||||
if (itemId == R.id.item_mark_read) {
|
||||
setReadState(true);
|
||||
} else if (itemId == R.id.item_mark_unread) {
|
||||
setReadState(false);
|
||||
} else if (itemId == R.id.item_select_all) {
|
||||
if (allItemsSelected) {
|
||||
adapter.unselectAll();
|
||||
allItemsSelected = false;
|
||||
actionMode.finish();
|
||||
} else {
|
||||
adapter.selectAll();
|
||||
allItemsSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
mode.finish();
|
||||
actionMode = null;
|
||||
|
||||
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
|
||||
binding.swipeRefreshLayout.setEnabled(true);
|
||||
|
||||
adapter.clearSelection();
|
||||
}
|
||||
|
||||
private void setReadState(boolean read) {
|
||||
if (allItemsSelected) {
|
||||
viewModel.setAllItemsReadState(read)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
|
||||
allItemsSelected = false;
|
||||
} else {
|
||||
viewModel.setItemsReadState(adapter.getSelectedItems(), read)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
adapter.updateSelection(read);
|
||||
updateDrawerFeeds();
|
||||
actionMode.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh() {
|
||||
Log.d(TAG, "syncing started");
|
||||
drawerManager.disableAccountSelection();
|
||||
updating = true;
|
||||
|
||||
if (viewModel.isAccountLocal()) {
|
||||
viewModel.getFeedCount()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableSingleObserver<Integer>() {
|
||||
@Override
|
||||
public void onSuccess(@NonNull Integer integer) {
|
||||
feedNb = integer;
|
||||
sync(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
Utils.showSnackbar(binding.mainRoot, e.getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sync(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void openAddFeedActivity(View view) {
|
||||
Intent intent = new Intent(this, AddFeedActivity.class);
|
||||
intent.putExtra(ACCOUNT_ID, viewModel.getCurrentAccount().getId());
|
||||
startActivityForResult(intent, ADD_FEED_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == ADD_FEED_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
List<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
|
||||
|
||||
if (feeds != null && !feeds.isEmpty() && viewModel.isAccountLocal()) {
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
feedNb = feeds.size();
|
||||
sync(feeds);
|
||||
}
|
||||
|
||||
} else if (requestCode == MANAGE_ACCOUNT_REQUEST || requestCode == SETTINGS_REQUEST) {
|
||||
updateDrawerFeeds();
|
||||
|
||||
} else if (requestCode == ADD_ACCOUNT_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
Account newAccount = data.getParcelableExtra(ACCOUNT);
|
||||
|
||||
if (newAccount != null) {
|
||||
// get credentials before creating the repository
|
||||
if (!newAccount.isLocal()) {
|
||||
getAccountCredentials(Collections.singletonList(newAccount));
|
||||
}
|
||||
|
||||
viewModel.addAccount(newAccount);
|
||||
adapter.clearData();
|
||||
|
||||
// start syncing only if the account is not local
|
||||
if (!viewModel.isAccountLocal()) {
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
drawerManager.resetItems();
|
||||
drawerManager.addAccount(newAccount, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void sync(@Nullable List<Feed> feeds) {
|
||||
viewModel.sync(feeds, feed -> {
|
||||
if (viewModel.isAccountLocal() && feedNb > 0) {
|
||||
binding.syncProgressTextView.setText(getString(R.string.updating_feed, feed.getName()));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
binding.syncProgressBar.setProgress((feedCount * 100) / feedNb, true);
|
||||
} else
|
||||
binding.syncProgressBar.setProgress((feedCount * 100) / feedNb);
|
||||
}
|
||||
|
||||
feedCount++;
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new CompletableObserver() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
syncDisposable = d;
|
||||
|
||||
if (viewModel.isAccountLocal() && feedNb > 0) {
|
||||
binding.syncProgressLayout.setVisibility(View.VISIBLE);
|
||||
binding.syncProgressBar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
viewModel.invalidate();
|
||||
|
||||
if (viewModel.isAccountLocal() && feedNb > 0) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
binding.syncProgressBar.setProgress(100, true);
|
||||
else
|
||||
binding.syncProgressBar.setProgress(100);
|
||||
|
||||
binding.syncProgressLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.swipeRefreshLayout.setRefreshing(false);
|
||||
|
||||
scrollToTop = true;
|
||||
adapter.submitList(allItems);
|
||||
|
||||
drawerManager.enableAccountSelection();
|
||||
updateDrawerFeeds(); // update drawer after syncing feeds
|
||||
updating = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
binding.swipeRefreshLayout.setRefreshing(false);
|
||||
binding.syncProgressLayout.setVisibility(View.GONE);
|
||||
|
||||
Utils.showSnackbar(binding.mainRoot, e.getMessage());
|
||||
drawerManager.enableAccountSelection();
|
||||
updating = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.item_list_menu, menu);
|
||||
|
||||
MenuItem articlesItem = menu.findItem(R.id.item_filter_read_items);
|
||||
articlesItem.setChecked(viewModel.showReadItems());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
|
||||
if (itemId == R.id.item_filter_read_items) {
|
||||
if (item.isChecked()) {
|
||||
item.setChecked(false);
|
||||
viewModel.setShowReadItems(false);
|
||||
SharedPreferencesManager.writeValue(
|
||||
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false);
|
||||
} else {
|
||||
item.setChecked(true);
|
||||
viewModel.setShowReadItems(true);
|
||||
SharedPreferencesManager.writeValue(
|
||||
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true);
|
||||
}
|
||||
|
||||
viewModel.invalidate();
|
||||
return true;
|
||||
} else if (itemId == R.id.item_sort) {
|
||||
displayFilterDialog();
|
||||
return true;
|
||||
} else if (itemId == R.id.start_sync) {
|
||||
if (!viewModel.isAccountLocal()) {
|
||||
binding.swipeRefreshLayout.setRefreshing(true);
|
||||
}
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void displayFilterDialog() {
|
||||
int index = viewModel.getSortType() == ListSortType.OLDEST_TO_NEWEST ? 1 : 0;
|
||||
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.filter)
|
||||
.items(R.array.filter_items)
|
||||
.itemsCallbackSingleChoice(index, (dialog, itemView, which, text) -> {
|
||||
String[] items = getResources().getStringArray(R.array.filter_items);
|
||||
|
||||
if (text.toString().equals(items[0]))
|
||||
viewModel.setSortType(ListSortType.NEWEST_TO_OLDEST);
|
||||
else
|
||||
viewModel.setSortType(ListSortType.OLDEST_TO_NEWEST);
|
||||
|
||||
scrollToTop = true;
|
||||
viewModel.invalidate();
|
||||
return true;
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void getAccountCredentials(List<Account> accounts) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getLogin() == null)
|
||||
account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
|
||||
|
||||
if (account.getPassword() == null)
|
||||
account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
|
||||
}
|
||||
}
|
||||
|
||||
private void startAboutActivity() {
|
||||
Libs.ActivityStyle activityStyle;
|
||||
int uiMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
|
||||
if (uiMode == Configuration.UI_MODE_NIGHT_YES) {
|
||||
activityStyle = Libs.ActivityStyle.DARK;
|
||||
} else {
|
||||
activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR;
|
||||
}
|
||||
|
||||
new LibsBuilder()
|
||||
.withAboutIconShown(true)
|
||||
.withAboutVersionShown(true)
|
||||
.withAboutAppName(getString(R.string.app_name))
|
||||
.withAboutDescription(getString(R.string.app_description))
|
||||
.withLicenseShown(true)
|
||||
.withLicenseDialog(false)
|
||||
.withActivityTitle(getString(R.string.about))
|
||||
.withActivityStyle(activityStyle)
|
||||
.withFields(R.string.class.getFields())
|
||||
.withAboutSpecial1(getString(R.string.source_code))
|
||||
.withAboutSpecial2(getString(R.string.changelog))
|
||||
.withListener(new LibsConfiguration.LibsListener() {
|
||||
@Override
|
||||
public void onIconClicked(View v) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLibraryAuthorClicked(View v, Library library) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLibraryContentClicked(View v, Library library) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLibraryBottomClicked(View v, Library library) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onExtraClicked(View v, Libs.SpecialButton specialButton) {
|
||||
if (v.getId() == R.id.aboutSpecial1) {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.app_url))));
|
||||
} else if (v.getId() == R.id.aboutSpecial2) {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.changelog_url))));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onIconLongClicked(View v) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLibraryAuthorLongClicked(View v, Library library) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLibraryContentLongClicked(View v, Library library) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLibraryBottomLongClicked(View v, Library library) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.start(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (syncDisposable != null && !syncDisposable.isDisposed())
|
||||
syncDisposable.dispose();
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
if (binding.swipeRefreshLayout.isRefreshing())
|
||||
outState.putBoolean(SYNCING, true);
|
||||
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
}
|
|
@ -1,372 +0,0 @@
|
|||
package com.readrops.app.itemslist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.paging.PagedListAdapter;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.bumptech.glide.request.transition.DrawableCrossFadeFactory;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.readrops.api.utils.DateUtils;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.databinding.ListItemBinding;
|
||||
import com.readrops.app.utils.GlideRequests;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MainItemListAdapter extends PagedListAdapter<ItemWithFeed, MainItemListAdapter.ItemViewHolder> implements ListPreloader.PreloadModelProvider<String> {
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
private OnItemClickListener listener;
|
||||
private ViewPreloadSizeProvider preloadSizeProvider;
|
||||
|
||||
private LinkedHashSet<Integer> selection;
|
||||
|
||||
public MainItemListAdapter(GlideRequests glideRequests, ViewPreloadSizeProvider preloadSizeProvider) {
|
||||
super(DIFF_CALLBACK);
|
||||
|
||||
this.glideRequests = glideRequests;
|
||||
this.preloadSizeProvider = preloadSizeProvider;
|
||||
selection = new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
private static final DiffUtil.ItemCallback<ItemWithFeed> DIFF_CALLBACK = new DiffUtil.ItemCallback<ItemWithFeed>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull ItemWithFeed item, @NonNull ItemWithFeed t1) {
|
||||
return item.getItem().getId() == t1.getItem().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull ItemWithFeed itemWithFeed, @NonNull ItemWithFeed t1) {
|
||||
Item oldItem = itemWithFeed.getItem();
|
||||
Item newItem = t1.getItem();
|
||||
|
||||
boolean folder = false;
|
||||
if (itemWithFeed.getFolder() != null && t1.getFolder() != null)
|
||||
folder = itemWithFeed.getFolder().getName().equals(t1.getFolder().getName());
|
||||
|
||||
return oldItem.getTitle().equals(newItem.getTitle()) &&
|
||||
itemWithFeed.getFeedName().equals(t1.getFeedName()) &&
|
||||
folder &&
|
||||
oldItem.isRead() == newItem.isRead() &&
|
||||
oldItem.isReadItLater() == newItem.isReadItLater() &&
|
||||
itemWithFeed.getColor() == t1.getColor() &&
|
||||
itemWithFeed.getBgColor() == t1.getBgColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChangePayload(@NonNull ItemWithFeed oldItem, @NonNull ItemWithFeed newItem) {
|
||||
return newItem;
|
||||
}
|
||||
};
|
||||
|
||||
private static final DrawableCrossFadeFactory FADE_FACTORY = new DrawableCrossFadeFactory.Builder().setCrossFadeEnabled(true).build();
|
||||
|
||||
private static final RequestOptions REQUEST_OPTIONS = new RequestOptions().transform(new CenterCrop(), new RoundedCorners(16));
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
ListItemBinding binding = ListItemBinding.inflate(LayoutInflater.from(viewGroup.getContext()));
|
||||
|
||||
ItemViewHolder viewHolder = new ItemViewHolder(binding);
|
||||
preloadSizeProvider.setView(binding.itemImage);
|
||||
|
||||
return viewHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (!payloads.isEmpty()) {
|
||||
ItemWithFeed itemWithFeed = (ItemWithFeed) payloads.get(0);
|
||||
|
||||
holder.bind(itemWithFeed);
|
||||
holder.applyColors(itemWithFeed);
|
||||
|
||||
if (itemWithFeed.getFolder() != null)
|
||||
holder.binding.itemFolderName.setText(itemWithFeed.getFolder().getName());
|
||||
else
|
||||
holder.binding.itemFolderName.setText(R.string.no_folder);
|
||||
|
||||
holder.setReadState(itemWithFeed.getItem().isRead());
|
||||
holder.setSelected(selection.contains(position));
|
||||
} else
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder viewHolder, int i) {
|
||||
ItemWithFeed itemWithFeed = getItem(i);
|
||||
if (itemWithFeed == null)
|
||||
return;
|
||||
|
||||
viewHolder.bind(itemWithFeed);
|
||||
viewHolder.setImages(itemWithFeed);
|
||||
viewHolder.applyColors(itemWithFeed);
|
||||
|
||||
int minutes = (int) Math.round(itemWithFeed.getItem().getReadTime());
|
||||
if (minutes < 1)
|
||||
viewHolder.binding.itemReadtime.setText(R.string.read_time_lower_than_1);
|
||||
else if (minutes > 1)
|
||||
viewHolder.binding.itemReadtime.setText(viewHolder.itemView.getContext().
|
||||
getString(R.string.read_time, String.valueOf(minutes)));
|
||||
else
|
||||
viewHolder.binding.itemReadtime.setText(R.string.read_time_one_minute);
|
||||
|
||||
if (itemWithFeed.getFolder() != null)
|
||||
viewHolder.binding.itemFolderName.setText(itemWithFeed.getFolder().getName());
|
||||
else
|
||||
viewHolder.binding.itemFolderName.setText(R.string.no_folder);
|
||||
|
||||
viewHolder.setReadState(itemWithFeed.getItem().isRead());
|
||||
viewHolder.setSelected(selection.contains(viewHolder.getAdapterPosition()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return getItem(position).getItem().getId();
|
||||
}
|
||||
|
||||
public void toggleSelection(int position) {
|
||||
if (selection.contains(position))
|
||||
selection.remove(position);
|
||||
else
|
||||
selection.add(position);
|
||||
|
||||
notifyItemChanged(position, getItem(position));
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
LinkedHashSet<Integer> localSelection = new LinkedHashSet<>(selection);
|
||||
selection.clear();
|
||||
|
||||
for (int position : localSelection) {
|
||||
notifyItemChanged(position, getItem(position));
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Integer> getSelection() {
|
||||
return selection;
|
||||
}
|
||||
|
||||
public void updateSelection(boolean read) {
|
||||
for (int position : selection) {
|
||||
ItemWithFeed itemWithFeed = getItem(position);
|
||||
itemWithFeed.getItem().setRead(read);
|
||||
notifyItemChanged(position, itemWithFeed);
|
||||
}
|
||||
}
|
||||
|
||||
public void selectAll() {
|
||||
selection.clear();
|
||||
for (int i = 0; i < getItemCount(); i++) {
|
||||
selection.add(i);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void unselectAll() {
|
||||
selection.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<ItemWithFeed> getSelectedItems() {
|
||||
List<ItemWithFeed> items = new ArrayList<>();
|
||||
|
||||
for (int i : selection) {
|
||||
items.add(getItem(i));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public void clearData() {
|
||||
submitList(null);
|
||||
}
|
||||
|
||||
public ItemWithFeed getItemWithFeed(int i) {
|
||||
return getItem(i);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<String> getPreloadItems(int position) {
|
||||
if (getItem(position).getItem().getHasImage()) {
|
||||
String url = getItem(position).getItem().getImageLink();
|
||||
return Collections.singletonList(url);
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull String url) {
|
||||
return glideRequests
|
||||
.load(url)
|
||||
.centerCrop()
|
||||
.apply(REQUEST_OPTIONS)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.transition(DrawableTransitionOptions.withCrossFade(FADE_FACTORY));
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
void onItemClick(ItemWithFeed itemWithFeed, int position);
|
||||
|
||||
void onItemLongClick(ItemWithFeed itemWithFeed, int position);
|
||||
}
|
||||
|
||||
public void setOnItemClickListener(OnItemClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public class ItemViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private ListItemBinding binding;
|
||||
private View[] alphaViews;
|
||||
|
||||
ItemViewHolder(ListItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
|
||||
itemView.setOnClickListener((view -> {
|
||||
int position = getAdapterPosition();
|
||||
|
||||
if (listener != null && position != RecyclerView.NO_POSITION)
|
||||
listener.onItemClick(getItem(position), position);
|
||||
}));
|
||||
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
int position = getAdapterPosition();
|
||||
|
||||
if (listener != null && position != RecyclerView.NO_POSITION)
|
||||
listener.onItemLongClick(getItem(position), position);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
alphaViews = new View[]{
|
||||
binding.itemDate,
|
||||
binding.itemFolderName,
|
||||
binding.itemFeedIcon,
|
||||
binding.itemFeedName,
|
||||
binding.itemDescription,
|
||||
binding.itemTitle,
|
||||
binding.itemImage,
|
||||
binding.itemReadtimeLayout
|
||||
};
|
||||
}
|
||||
|
||||
private void bind(ItemWithFeed itemWithFeed) {
|
||||
Item item = itemWithFeed.getItem();
|
||||
|
||||
binding.itemTitle.setText(item.getTitle());
|
||||
binding.itemDate.setText(DateUtils.formattedDateByLocal(item.getPubDate()));
|
||||
binding.itemFeedName.setText(itemWithFeed.getFeedName());
|
||||
|
||||
if (item.getCleanDescription() != null) {
|
||||
binding.itemDescription.setVisibility(View.VISIBLE);
|
||||
binding.itemDescription.setText(item.getCleanDescription());
|
||||
} else {
|
||||
binding.itemDescription.setVisibility(View.GONE);
|
||||
if (itemWithFeed.getItem().getHasImage())
|
||||
binding.itemTitle.setMaxLines(4);
|
||||
}
|
||||
}
|
||||
|
||||
private void setImages(ItemWithFeed itemWithFeed) {
|
||||
if (itemWithFeed.getItem().getHasImage()) {
|
||||
binding.itemImage.setVisibility(View.VISIBLE);
|
||||
|
||||
glideRequests
|
||||
.load(itemWithFeed.getItem().getImageLink())
|
||||
.centerCrop()
|
||||
.apply(REQUEST_OPTIONS)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.transition(DrawableTransitionOptions.withCrossFade(FADE_FACTORY))
|
||||
.into(binding.itemImage);
|
||||
} else
|
||||
binding.itemImage.setVisibility(View.GONE);
|
||||
|
||||
if (itemWithFeed.getFeedIconUrl() != null) {
|
||||
glideRequests.
|
||||
load(itemWithFeed.getFeedIconUrl())
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.ic_rss_feed_grey)
|
||||
.into(binding.itemFeedIcon);
|
||||
} else
|
||||
binding.itemFeedIcon.setImageResource(R.drawable.ic_rss_feed_grey);
|
||||
}
|
||||
|
||||
private void applyColors(ItemWithFeed itemWithFeed) {
|
||||
Resources resources = itemView.getResources();
|
||||
|
||||
if (itemWithFeed.getBgColor() != 0) {
|
||||
binding.itemFeedName.setTextColor(itemWithFeed.getBgColor());
|
||||
Utils.setDrawableColor(binding.itemDate.getBackground(), itemWithFeed.getBgColor());
|
||||
|
||||
} else if (itemWithFeed.getColor() != 0) {
|
||||
binding.itemFeedName.setTextColor(itemWithFeed.getColor());
|
||||
Utils.setDrawableColor(binding.itemDate.getBackground(), itemWithFeed.getColor());
|
||||
|
||||
} else if (itemWithFeed.getBgColor() == 0 && itemWithFeed.getColor() == 0) {
|
||||
binding.itemFeedName.setTextColor(resources.getColor(android.R.color.tab_indicator_text));
|
||||
Utils.setDrawableColor(binding.itemDate.getBackground(),
|
||||
ContextCompat.getColor(itemView.getContext(), R.color.colorPrimary));
|
||||
}
|
||||
}
|
||||
|
||||
private void setReadState(boolean isRead) {
|
||||
float alpha = isRead ? 0.5f : 1.0f;
|
||||
for (View view : alphaViews) {
|
||||
view.setAlpha(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSelected(boolean selected) {
|
||||
Context context = itemView.getContext();
|
||||
TypedValue outValue = new TypedValue();
|
||||
|
||||
if (selected) {
|
||||
context.getTheme().resolveAttribute(
|
||||
android.R.attr.colorControlHighlight, outValue, true);
|
||||
} else {
|
||||
context.getTheme().resolveAttribute(
|
||||
android.R.attr.selectableItemBackground, outValue, true);
|
||||
}
|
||||
|
||||
itemView.setBackgroundResource(outValue.resourceId);
|
||||
}
|
||||
|
||||
public ImageView getItemImage() {
|
||||
return binding.itemImage;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,266 +0,0 @@
|
|||
package com.readrops.app.itemslist;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
|
||||
import com.readrops.app.repositories.ARepository;
|
||||
import com.readrops.app.repositories.FeedUpdate;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.RoomFactoryWrapper;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.filters.MainFilter;
|
||||
import com.readrops.db.filters.ListSortType;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
import com.readrops.db.queries.ItemsQueryBuilder;
|
||||
import com.readrops.db.queries.QueryFilters;
|
||||
|
||||
import org.koin.core.parameter.ParametersHolderKt;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class MainViewModel extends ViewModel {
|
||||
|
||||
private final MediatorLiveData<PagedList<ItemWithFeed>> itemsWithFeed;
|
||||
private LiveData<PagedList<ItemWithFeed>> lastFetch;
|
||||
private ARepository repository;
|
||||
private final Database database;
|
||||
|
||||
private final QueryFilters queryFilters;
|
||||
|
||||
private Account currentAccount;
|
||||
private List<Account> accounts;
|
||||
|
||||
public MainViewModel(@NonNull Database database) {
|
||||
this.database = database;
|
||||
itemsWithFeed = new MediatorLiveData<>();
|
||||
|
||||
queryFilters = new QueryFilters();
|
||||
/* queryFilters.setShowReadItems(SharedPreferencesManager.readBoolean(
|
||||
SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));*/
|
||||
}
|
||||
|
||||
//region main query
|
||||
|
||||
private void setRepository() {
|
||||
repository = KoinJavaComponent.get(ARepository.class, null,
|
||||
() -> ParametersHolderKt.parametersOf(currentAccount));
|
||||
}
|
||||
|
||||
private void buildPagedList() {
|
||||
if (lastFetch != null) {
|
||||
itemsWithFeed.removeSource(lastFetch);
|
||||
}
|
||||
|
||||
DataSource.Factory<Integer, ItemWithFeed> items;
|
||||
items = database.itemDao().selectAll(ItemsQueryBuilder.INSTANCE.buildItemsQuery(queryFilters, currentAccount.getConfig().getUseSeparateState()));
|
||||
|
||||
lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items),
|
||||
new PagedList.Config.Builder()
|
||||
.setPageSize(100)
|
||||
.setPrefetchDistance(150)
|
||||
.setEnablePlaceholders(false)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
itemsWithFeed.addSource(lastFetch, itemsWithFeed::setValue);
|
||||
}
|
||||
|
||||
public void invalidate() {
|
||||
buildPagedList();
|
||||
}
|
||||
|
||||
public void setShowReadItems(boolean showReadItems) {
|
||||
//queryFilters.setShowReadItems(showReadItems);
|
||||
}
|
||||
|
||||
public boolean showReadItems() {
|
||||
return queryFilters.getShowReadItems();
|
||||
}
|
||||
|
||||
public void setFilterType(MainFilter filterType) {
|
||||
//queryFilters.setMainFilter(filterType);
|
||||
}
|
||||
|
||||
public MainFilter getFilterType() {
|
||||
return queryFilters.getMainFilter();
|
||||
}
|
||||
|
||||
public void setSortType(ListSortType sortType) {
|
||||
//queryFilters.setSortType(sortType);
|
||||
}
|
||||
|
||||
public ListSortType getSortType() {
|
||||
return queryFilters.getSortType();
|
||||
}
|
||||
|
||||
public void setFilterFeedId(int filterFeedId) {
|
||||
//queryFilters.setFilterFeedId(filterFeedId);
|
||||
}
|
||||
|
||||
public void setFilerFolderId(int folderId) {
|
||||
//queryFilters.setFilterFolderId(folderId);
|
||||
}
|
||||
|
||||
public MediatorLiveData<PagedList<ItemWithFeed>> getItemsWithFeed() {
|
||||
return itemsWithFeed;
|
||||
}
|
||||
|
||||
public Completable sync(List<Feed> feeds, FeedUpdate update) {
|
||||
itemsWithFeed.removeSource(lastFetch);
|
||||
|
||||
// get current viewed feed
|
||||
if (feeds == null && queryFilters.getMainFilter() == MainFilter.ALL) {
|
||||
return Single.<Feed>create(emitter -> emitter.onSuccess(database.feedDao()
|
||||
.getFeedById(queryFilters.getFilterFeedId())))
|
||||
.flatMapCompletable(feed -> repository.sync(Collections.singletonList(feed), update));
|
||||
}
|
||||
|
||||
return repository.sync(feeds, update);
|
||||
}
|
||||
|
||||
public Single<Integer> getFeedCount() {
|
||||
return repository.getFeedCount(currentAccount.getId());
|
||||
}
|
||||
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return repository.getFoldersWithFeeds();
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Account
|
||||
|
||||
public LiveData<List<Account>> getAllAccounts() {
|
||||
return database.accountDao().selectAllAsync();
|
||||
}
|
||||
|
||||
private Completable deselectOldCurrentAccount(int accountId) {
|
||||
return Completable.create(emitter -> {
|
||||
database.accountDao().deselectOldCurrentAccount(accountId);
|
||||
emitter.onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
private Account getAccount(int id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getId() == id)
|
||||
return account;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void addAccount(Account account) {
|
||||
accounts.add(account);
|
||||
setCurrentAccount(account);
|
||||
}
|
||||
|
||||
public Account getCurrentAccount() {
|
||||
return currentAccount;
|
||||
}
|
||||
|
||||
public void setCurrentAccount(Account currentAccount) {
|
||||
this.currentAccount = currentAccount;
|
||||
setRepository();
|
||||
//queryFilters.setAccountId(currentAccount.getId());
|
||||
buildPagedList();
|
||||
|
||||
// set the new account as the current one
|
||||
Completable setCurrentAccount = Completable.create(emitter -> {
|
||||
database.accountDao().setCurrentAccount(currentAccount.getId());
|
||||
emitter.onComplete();
|
||||
});
|
||||
|
||||
Completable.concat(Arrays.asList(setCurrentAccount, deselectOldCurrentAccount(currentAccount.getId())))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
public void setCurrentAccount(int id) {
|
||||
setCurrentAccount(getAccount(id));
|
||||
}
|
||||
|
||||
|
||||
public void setAccounts(List<Account> accounts) {
|
||||
this.accounts = accounts;
|
||||
|
||||
boolean currentAccountExists = false;
|
||||
|
||||
for (Account account1 : accounts) {
|
||||
if (account1.isCurrentAccount()) {
|
||||
currentAccount = account1;
|
||||
currentAccountExists = true;
|
||||
|
||||
setRepository();
|
||||
//queryFilters.setAccountId(currentAccount.getId());
|
||||
buildPagedList();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentAccountExists && !accounts.isEmpty()) {
|
||||
setCurrentAccount(accounts.get(0));
|
||||
accounts.get(0).setCurrentAccount(true);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAccountLocal() {
|
||||
return currentAccount.isLocal();
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region Item read state
|
||||
|
||||
public Completable setItemReadState(ItemWithFeed itemWithFeed) {
|
||||
return repository.setItemReadState(itemWithFeed.getItem());
|
||||
}
|
||||
|
||||
public Completable setItemReadState(Item item) {
|
||||
return repository.setItemReadState(item);
|
||||
}
|
||||
|
||||
public Completable setItemsReadState(List<ItemWithFeed> items, boolean read) {
|
||||
List<Completable> completableList = new ArrayList<>();
|
||||
|
||||
for (ItemWithFeed itemWithFeed : items) {
|
||||
itemWithFeed.getItem().setRead(read);
|
||||
completableList.add(setItemReadState(itemWithFeed));
|
||||
}
|
||||
|
||||
return Completable.concat(completableList);
|
||||
}
|
||||
|
||||
public Completable setAllItemsReadState(boolean read) {
|
||||
if (queryFilters.getMainFilter() == MainFilter.ALL)
|
||||
return repository.setAllFeedItemsReadState(queryFilters.getFilterFeedId(), read);
|
||||
else
|
||||
return repository.setAllItemsReadState(read);
|
||||
}
|
||||
|
||||
public Completable setItemReadItLater(boolean readLater, int itemId) {
|
||||
return database.itemDao().setReadItLater(readLater, itemId);
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
package com.readrops.app.notifications
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.settings.SettingsActivity
|
||||
import com.readrops.app.databinding.ActivityNotificationPermissionBinding
|
||||
import com.readrops.app.utils.ReadropsKeys
|
||||
import com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID
|
||||
import com.readrops.app.utils.SharedPreferencesManager
|
||||
import com.readrops.app.utils.Utils
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.account.Account
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.koin.androidx.viewmodel.ext.android.getViewModel
|
||||
|
||||
class NotificationPermissionActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityNotificationPermissionBinding
|
||||
private lateinit var viewModel: NotificationPermissionViewModel
|
||||
private var adapter: NotificationPermissionListAdapter? = null
|
||||
|
||||
private var isFirstCheck = true
|
||||
private var feedStateChanged = false
|
||||
private var feeds = listOf<Feed>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityNotificationPermissionBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
setTitle(R.string.notifications)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val accountId = intent.getIntExtra(ACCOUNT_ID, 0)
|
||||
|
||||
viewModel = getViewModel<NotificationPermissionViewModel>()
|
||||
viewModel.getAccount(accountId).observe(this, Observer { account ->
|
||||
viewModel.account = account
|
||||
|
||||
if (adapter == null) {
|
||||
// execute the method only once
|
||||
setupUI(account)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupUI(account: Account) {
|
||||
binding.notifPermissionAccountSwitch.isChecked = account.isNotificationsEnabled
|
||||
binding.notifPermissionAccountSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
account.isNotificationsEnabled = isChecked
|
||||
binding.notifPermissionFeedsSwitch.isEnabled = isChecked
|
||||
|
||||
adapter?.enableAll = isChecked
|
||||
adapter?.notifyDataSetChanged()
|
||||
|
||||
viewModel.setAccountNotificationsState(isChecked)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError { Utils.showSnackbar(binding.root, it.message) }
|
||||
.subscribe()
|
||||
|
||||
if (isChecked) displayAutoSynchroPopup()
|
||||
}
|
||||
|
||||
binding.notifPermissionFeedsSwitch.isEnabled = account.isNotificationsEnabled
|
||||
binding.notifPermissionFeedsSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (canUpdateAllFeedsPermissions(isChecked)) {
|
||||
viewModel.setAllFeedsNotificationState(isChecked)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError { Utils.showSnackbar(binding.root, it.message) }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
if (isFirstCheck) isFirstCheck = false
|
||||
if (feedStateChanged) feedStateChanged = false
|
||||
}
|
||||
|
||||
adapter = NotificationPermissionListAdapter(account.isNotificationsEnabled) { feed ->
|
||||
feedStateChanged = true
|
||||
|
||||
viewModel.setFeedNotificationState(feed)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnError { Utils.showSnackbar(binding.root, it.message) }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
binding.notifPermissionAccountList.layoutManager = LinearLayoutManager(this)
|
||||
binding.notifPermissionAccountList.adapter = adapter
|
||||
|
||||
viewModel.getFeedsWithNotifPermission().observe(this, Observer { newFeeds ->
|
||||
feeds = newFeeds
|
||||
|
||||
binding.notifPermissionFeedsSwitch.isChecked = newFeeds.all { it.isNotificationEnabled }
|
||||
adapter?.submitList(newFeeds)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform if is possible to update all feeds notifications permissions in the same time.
|
||||
* The method takes into account the following states :
|
||||
* - first check : when opening the activity with all feeds permissions enabled,
|
||||
* the enable all feeds permissions switch will be checked but the request mustn't be executed
|
||||
* - feed state : if all feeds permissions are enabled and a feed permission is disabled,
|
||||
* the enable all feeds permissions switch will be unchecked but the request mustn't be executed as only one feed permission is disabled
|
||||
* - all feeds permissions switch checked : if the setOnCheckedChangeListener method is triggered because all feeds permissions were enabled,
|
||||
* do not execute the request as it would be pointless
|
||||
*/
|
||||
private fun canUpdateAllFeedsPermissions(isChecked: Boolean): Boolean {
|
||||
return (!isFirstCheck || !feeds.all { it.isNotificationEnabled }) &&
|
||||
(!feedStateChanged || (isChecked && !feeds.all { it.isNotificationEnabled }))
|
||||
}
|
||||
|
||||
private fun displayAutoSynchroPopup() {
|
||||
val autoSynchroValue = SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.AUTO_SYNCHRO)
|
||||
|
||||
if (autoSynchroValue.toFloat() <= 0) {
|
||||
MaterialDialog.Builder(this)
|
||||
.title(R.string.auto_synchro_disabled)
|
||||
.content(R.string.enable_auto_synchro_text)
|
||||
.positiveText(R.string.open)
|
||||
.neutralText(R.string.cancel)
|
||||
.onPositive { _, _ ->
|
||||
val intent = Intent(this, SettingsActivity::class.java).apply {
|
||||
putExtra(ReadropsKeys.SETTINGS, SettingsActivity.SettingsKey.SETTINGS.ordinal)
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package com.readrops.app.notifications
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.databinding.NotificationPermissionLayoutBinding
|
||||
import com.readrops.app.utils.GlideRequests
|
||||
import com.readrops.db.entities.Feed
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class NotificationPermissionListAdapter(var enableAll: Boolean, val listener: (feed: Feed) -> Unit) :
|
||||
ListAdapter<Feed, NotificationPermissionListAdapter.NotificationPermissionViewHolder>(DIFF_CALLBACK), KoinComponent {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationPermissionViewHolder {
|
||||
val binding = NotificationPermissionLayoutBinding.inflate(LayoutInflater.from(parent.context))
|
||||
|
||||
return NotificationPermissionViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NotificationPermissionViewHolder, position: Int) {
|
||||
val feed = getItem(position)
|
||||
|
||||
holder.binding.notificationFeedName.text = feed.name
|
||||
holder.binding.notificationSwitch.isChecked = feed.isNotificationEnabled
|
||||
|
||||
holder.binding.notificationSwitch.isEnabled = enableAll
|
||||
|
||||
holder.itemView.setOnClickListener { if (enableAll) listener(getItem(position)) }
|
||||
|
||||
get<GlideRequests>()
|
||||
.load(feed.iconUrl)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.ic_rss_feed_grey)
|
||||
.into(holder.binding.notificationFeedIcon)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NotificationPermissionViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isNotEmpty()) {
|
||||
val feed = payloads.first() as Feed
|
||||
holder.binding.notificationSwitch.isChecked = feed.isNotificationEnabled
|
||||
} else onBindViewHolder(holder, position)
|
||||
}
|
||||
|
||||
inner class NotificationPermissionViewHolder(val binding: NotificationPermissionLayoutBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
companion object {
|
||||
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Feed>() {
|
||||
override fun areItemsTheSame(oldItem: Feed, newItem: Feed): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Feed, newItem: Feed): Boolean {
|
||||
return oldItem.isNotificationEnabled == newItem.isNotificationEnabled
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: Feed, newItem: Feed): Any? {
|
||||
return newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package com.readrops.app.notifications
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.account.Account
|
||||
import io.reactivex.Completable
|
||||
|
||||
class NotificationPermissionViewModel(val database: Database) : ViewModel() {
|
||||
|
||||
var account: Account? = null
|
||||
|
||||
fun getAccount(accountId: Int): LiveData<Account> = database.accountDao().selectAsync(accountId)
|
||||
|
||||
fun getFeedsWithNotifPermission(): LiveData<List<Feed>> = database.feedDao()
|
||||
.getFeedsForNotifPermission(account?.id!!)
|
||||
|
||||
fun setAccountNotificationsState(enabled: Boolean): Completable = database.accountDao()
|
||||
.updateNotificationState(account?.id!!, enabled)
|
||||
|
||||
fun setFeedNotificationState(feed: Feed): Completable = database.feedDao()
|
||||
.updateFeedNotificationState(feed.id, !feed.isNotificationEnabled)
|
||||
|
||||
fun setAllFeedsNotificationState(enabled: Boolean): Completable = database.feedDao()
|
||||
.updateAllFeedsNotificationState(account?.id!!, enabled)
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package com.readrops.app.notifications.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.readrops.app.R
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.app.utils.GlideRequests
|
||||
import com.readrops.app.utils.Utils
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
/**
|
||||
* Simple class to get synchro notification content (title, content and largeIcon) according to some rules
|
||||
*/
|
||||
class SyncResultAnalyser(val context: Context, private val syncResults: Map<Account, SyncResult>, val database: Database) : KoinComponent {
|
||||
|
||||
private val notifContent = SyncResultNotifContent()
|
||||
|
||||
fun getSyncNotifContent(): SyncResultNotifContent {
|
||||
if (newItemsInMultipleAccounts()) { // new items from several accounts
|
||||
var itemCount = 0
|
||||
val feeds = database.feedDao().selectFromIdList(getFeedsIdsForNewItems(syncResults))
|
||||
|
||||
syncResults.values.forEach { syncResult ->
|
||||
itemCount += syncResult.items.filter { isFeedNotificationEnabledForItem(feeds, it) }.size
|
||||
}
|
||||
|
||||
notifContent.title = context.getString(R.string.new_items, itemCount.toString())
|
||||
} else { // new items from only one account
|
||||
val syncResultMap = syncResults.filterValues { it.items.isNotEmpty() }
|
||||
|
||||
if (syncResultMap.values.isNotEmpty()) {
|
||||
val syncResult = syncResultMap.values.first()
|
||||
val account = syncResultMap.keys.first()
|
||||
val feedsIdsForNewItems = getFeedsIdsForNewItems(syncResult)
|
||||
|
||||
notifContent.accountId = account.id
|
||||
|
||||
if (account.isNotificationsEnabled) {
|
||||
val feeds = database.feedDao().selectFromIdList(feedsIdsForNewItems)
|
||||
|
||||
val items = syncResult.items.filter { isFeedNotificationEnabledForItem(feeds, it) }
|
||||
val itemCount = items.size
|
||||
|
||||
// new items from several feeds from one account
|
||||
if (feedsIdsForNewItems.size > 1 && itemCount > 1) {
|
||||
notifContent.title = account.accountName
|
||||
notifContent.content = context.getString(R.string.new_items, itemCount.toString())
|
||||
notifContent.largeIcon = Utils.getBitmapFromDrawable(ContextCompat.getDrawable(context, account.accountType!!.iconRes))
|
||||
} else if (feedsIdsForNewItems.size == 1) // new items from only one feed from one account
|
||||
oneFeedCase(feedsIdsForNewItems.first(), syncResult.items)
|
||||
else if (itemCount == 1)
|
||||
oneFeedCase(items.first().feedId.toLong(), items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notifContent
|
||||
}
|
||||
|
||||
private fun oneFeedCase(feedId: Long, items: List<Item>) {
|
||||
val feed = database.feedDao().getFeedById(feedId.toInt())
|
||||
|
||||
if (feed.isNotificationEnabled) {
|
||||
notifContent.title = feed?.name
|
||||
|
||||
feed?.iconUrl?.let {
|
||||
val target = get<GlideRequests>()
|
||||
.asBitmap()
|
||||
.load(it)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.submit()
|
||||
|
||||
notifContent.largeIcon = target.get()
|
||||
}
|
||||
|
||||
if (items.size == 1) {
|
||||
val item = database.itemDao().selectByRemoteId(items.first().remoteId!!,
|
||||
items.first().feedId)
|
||||
notifContent.content = item.title
|
||||
notifContent.item = item
|
||||
} else notifContent.content = context.getString(R.string.new_items, items.size.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun newItemsInMultipleAccounts(): Boolean {
|
||||
val itemsNotEmptyByAccount = mutableListOf<Boolean>()
|
||||
|
||||
for ((account, syncResult) in syncResults) {
|
||||
if (account.isNotificationsEnabled) itemsNotEmptyByAccount += syncResult.items.isNotEmpty()
|
||||
}
|
||||
|
||||
// return true it there is at least two true booleans in the list
|
||||
return itemsNotEmptyByAccount.groupingBy { it }.eachCount()[true] ?: 0 > 1
|
||||
}
|
||||
|
||||
private fun getFeedsIdsForNewItems(syncResult: SyncResult): List<Long> {
|
||||
val feedsIds = mutableListOf<Long>()
|
||||
|
||||
syncResult.items.forEach {
|
||||
if (it.feedId.toLong() !in feedsIds)
|
||||
feedsIds += it.feedId.toLong()
|
||||
}
|
||||
|
||||
return feedsIds
|
||||
}
|
||||
|
||||
private fun getFeedsIdsForNewItems(syncResults: Map<Account, SyncResult>): List<Long> {
|
||||
val feedsIds = mutableListOf<Long>()
|
||||
|
||||
syncResults.values.forEach { feedsIds += getFeedsIdsForNewItems(it) }
|
||||
return feedsIds
|
||||
}
|
||||
|
||||
private fun isFeedNotificationEnabledForItem(feeds: List<Feed>, item: Item): Boolean =
|
||||
feeds.find { it.id == item.feedId }?.isNotificationEnabled!!
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package com.readrops.app.notifications.sync
|
||||
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import com.readrops.db.entities.account.AccountType
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class SyncResultDebugData {
|
||||
|
||||
companion object : KoinComponent {
|
||||
|
||||
@TestOnly
|
||||
fun oneAccountOneFeedOneItem(): Map<Account, SyncResult> {
|
||||
val database = get<Database>()
|
||||
val account1 = database.accountDao().select(2)
|
||||
|
||||
|
||||
val item = database.itemDao().select(5000)
|
||||
// database.feedDao().updateNotificationState(item.feedId, false).subscribe()
|
||||
|
||||
return mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, SyncResult().apply { items = mutableListOf(item) })
|
||||
}
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
fun oneAccountOneFeedMultipleItems(): Map<Account, SyncResult> {
|
||||
val account1 = Account().apply {
|
||||
id = 1
|
||||
accountType = AccountType.FRESHRSS
|
||||
isNotificationsEnabled = true
|
||||
}
|
||||
|
||||
val database = get<Database>()
|
||||
val item = database.itemDao().select(5055)
|
||||
database.feedDao().updateFeedNotificationState(item.feedId, false).subscribe()
|
||||
|
||||
val item2 = database.itemDao().select(5056)
|
||||
|
||||
return mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, SyncResult().apply { items = listOf(item, item2) })
|
||||
}
|
||||
}
|
||||
|
||||
@TestOnly
|
||||
fun oneAccountMultipleFeeds(): Map<Account, SyncResult> {
|
||||
val account1 = Account().apply {
|
||||
accountName = "Test account"
|
||||
id = 1
|
||||
accountType = AccountType.FRESHRSS
|
||||
isNotificationsEnabled = true
|
||||
}
|
||||
|
||||
val item1 = Item().apply {
|
||||
id = 1
|
||||
title = "oneAccountMultipleFeeds"
|
||||
feedId = 1
|
||||
}
|
||||
|
||||
val item2 = Item().apply {
|
||||
id = 2
|
||||
title = "oneAccountMultipleFeeds"
|
||||
feedId = 2
|
||||
}
|
||||
|
||||
return mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, SyncResult().apply { items = mutableListOf(item1, item2) })
|
||||
}
|
||||
}
|
||||
|
||||
fun multipleAccounts(): Map<Account, SyncResult> {
|
||||
val account1 = Account().apply {
|
||||
id = 1
|
||||
accountType = AccountType.FRESHRSS
|
||||
isNotificationsEnabled = true
|
||||
}
|
||||
|
||||
val account2 = Account().apply {
|
||||
id = 2
|
||||
accountType = AccountType.LOCAL
|
||||
isNotificationsEnabled = true
|
||||
}
|
||||
|
||||
val item = Item().apply {
|
||||
id = 1
|
||||
title = "multipleAccountsCase"
|
||||
feedId = 90
|
||||
}
|
||||
|
||||
return mutableMapOf<Account, SyncResult>().apply {
|
||||
put(account1, SyncResult().apply { items = mutableListOf(item) })
|
||||
put(account2, SyncResult().apply { items = mutableListOf(item) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package com.readrops.app.notifications.sync
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.readrops.db.entities.Item
|
||||
|
||||
class SyncResultNotifContent {
|
||||
var title: String? = null
|
||||
var content: String? = null
|
||||
var largeIcon: Bitmap? = null
|
||||
var item: Item? = null
|
||||
var accountId: Int? = null
|
||||
}
|
|
@ -1,211 +0,0 @@
|
|||
package com.readrops.app.notifications.sync
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.ReadropsApp
|
||||
import com.readrops.app.itemslist.MainActivity
|
||||
import com.readrops.app.repositories.ARepository
|
||||
import com.readrops.app.utils.ReadropsKeys
|
||||
import com.readrops.app.utils.SharedPreferencesManager
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Item
|
||||
import com.readrops.db.entities.account.Account
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters), KoinComponent {
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
private val database = get<Database>()
|
||||
|
||||
override fun doWork(): Result {
|
||||
var result = Result.success()
|
||||
val syncResults = mutableMapOf<Account, SyncResult>()
|
||||
|
||||
try {
|
||||
val accounts = database.accountDao().selectAll()
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setContentTitle(applicationContext.getString(R.string.auto_synchro))
|
||||
.setProgress(0, 0, true)
|
||||
.setSmallIcon(R.drawable.ic_notif)
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
accounts.forEach {
|
||||
notificationBuilder.setContentText(it.accountName)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
it.login = SharedPreferencesManager.readString(it.loginKey)
|
||||
it.password = SharedPreferencesManager.readString(it.passwordKey)
|
||||
|
||||
val repository = get<ARepository>(parameters = { parametersOf(it) })
|
||||
|
||||
disposable = repository.sync(null, null)
|
||||
.doOnError { throwable ->
|
||||
result = Result.failure()
|
||||
Log.e(TAG, throwable.message!!, throwable)
|
||||
}
|
||||
.subscribe()
|
||||
|
||||
if (repository.syncResult != null) syncResults[it] = repository.syncResult
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.message!!)
|
||||
result = Result.failure()
|
||||
} finally {
|
||||
notificationManager.cancel(SYNC_NOTIFICATION_ID)
|
||||
displaySyncResultNotif(syncResults)
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
super.onStopped()
|
||||
|
||||
disposable?.dispose()
|
||||
notificationManager.cancel(SYNC_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
private fun displaySyncResultNotif(syncResults: Map<Account, SyncResult>) {
|
||||
val notifContent = SyncResultAnalyser(applicationContext, syncResults, database)
|
||||
.getSyncNotifContent()
|
||||
|
||||
if (notifContent.title != null) {
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
if (notifContent.item != null) {
|
||||
putExtra(ReadropsKeys.ITEM_ID, notifContent.item?.id)
|
||||
putExtra(ReadropsKeys.IMAGE_URL, notifContent.item?.imageLink)
|
||||
|
||||
if (notifContent.accountId != null) putExtra(ReadropsKeys.ACCOUNT_ID, notifContent.accountId!!)
|
||||
}
|
||||
}
|
||||
|
||||
val intentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setContentTitle(notifContent.title)
|
||||
.setContentText(notifContent.content)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(notifContent.content))
|
||||
.setSmallIcon(R.drawable.ic_notif)
|
||||
.setContentIntent(PendingIntent.getActivity(applicationContext, 0,
|
||||
intent, intentFlag))
|
||||
.setAutoCancel(true)
|
||||
|
||||
notifContent.item?.let {
|
||||
val feed = database.feedDao().getFeedById(it.feedId)
|
||||
|
||||
notificationBuilder.addAction(buildReadlaterAction(it))
|
||||
.addAction(buildMarkAsRead(it))
|
||||
.setColor(if (feed.backgroundColor != 0) feed.backgroundColor else feed.textColor)
|
||||
}
|
||||
|
||||
notifContent.largeIcon?.let {
|
||||
notificationBuilder.setLargeIcon(it)
|
||||
}
|
||||
|
||||
notificationManager.notify(SYNC_RESULT_NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun buildReadlaterAction(item: Item): NotificationCompat.Action {
|
||||
val broadcastIntent = Intent(applicationContext, ReadLaterReceiver::class.java).apply {
|
||||
putExtra(ReadropsKeys.ITEM_ID, item.id)
|
||||
}
|
||||
|
||||
val intentFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
}
|
||||
|
||||
return NotificationCompat.Action.Builder(R.drawable.ic_read_later, applicationContext.getString(R.string.read_later),
|
||||
PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, intentFlag))
|
||||
.setAllowGeneratedReplies(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildMarkAsRead(item: Item): NotificationCompat.Action {
|
||||
val broadcastIntent = Intent(applicationContext, MarkReadReceiver::class.java).apply {
|
||||
putExtra(ReadropsKeys.ITEM_ID, item.id)
|
||||
}
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
NotificationCompat.Action.Builder(R.drawable.ic_read, applicationContext.getString(R.string.read),
|
||||
PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, PendingIntent.FLAG_IMMUTABLE))
|
||||
.setAllowGeneratedReplies(false)
|
||||
.build()
|
||||
} else {
|
||||
NotificationCompat.Action.Builder(R.drawable.ic_read, applicationContext.getString(R.string.read),
|
||||
PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, PendingIntent.FLAG_IMMUTABLE))
|
||||
.setAllowGeneratedReplies(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class MarkReadReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!!
|
||||
|
||||
with(get<Database>()) {
|
||||
itemDao().setReadState(itemId, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
with(NotificationManagerCompat.from(context!!)) {
|
||||
cancel(SYNC_RESULT_NOTIFICATION_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReadLaterReceiver : BroadcastReceiver(), KoinComponent {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!!
|
||||
|
||||
with(get<Database>()) {
|
||||
val item = itemDao().select(itemId)
|
||||
item.isReadItLater = !item.isReadItLater
|
||||
|
||||
itemDao().setReadItLater(item.isReadItLater, itemId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
with(NotificationManagerCompat.from(context!!)) {
|
||||
cancel(SYNC_RESULT_NOTIFICATION_ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = SyncWorker::class.java.simpleName
|
||||
private const val SYNC_NOTIFICATION_ID = 2
|
||||
const val SYNC_RESULT_NOTIFICATION_ID = 3
|
||||
}
|
||||
}
|
|
@ -1,207 +0,0 @@
|
|||
package com.readrops.app.repositories;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.services.Credentials;
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.utils.AuthInterceptor;
|
||||
import com.readrops.app.addfeed.FeedInsertionResult;
|
||||
import com.readrops.app.addfeed.ParsingResult;
|
||||
import com.readrops.app.utils.feedscolors.FeedColorsKt;
|
||||
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.ItemState;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public abstract class ARepository {
|
||||
|
||||
protected Context context;
|
||||
protected Database database;
|
||||
protected Account account;
|
||||
|
||||
protected SyncResult syncResult;
|
||||
|
||||
protected ARepository(Database database, @NonNull Context context, @Nullable Account account) {
|
||||
this.context = context;
|
||||
this.database = database;
|
||||
this.account = account;
|
||||
|
||||
setCredentials(account);
|
||||
}
|
||||
|
||||
protected void setCredentials(@Nullable Account account) {
|
||||
KoinJavaComponent.<AuthInterceptor>get(AuthInterceptor.class)
|
||||
.setCredentials(account != null && !account.isLocal() ? Credentials.toCredentials(account) : null);
|
||||
}
|
||||
|
||||
public abstract Completable login(Account account, boolean insert);
|
||||
|
||||
public abstract Completable sync(@Nullable List<Feed> feeds, @Nullable FeedUpdate update);
|
||||
|
||||
public abstract Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results);
|
||||
|
||||
public Completable insertOPMLFoldersAndFeeds(Map<Folder, List<Feed>> foldersAndFeeds) {
|
||||
List<Completable> completableList = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<Folder, List<Feed>> entry : foldersAndFeeds.entrySet()) {
|
||||
Folder folder = entry.getKey();
|
||||
folder.setAccountId(account.getId());
|
||||
|
||||
Completable completable = Single.<Integer>create(emitter -> {
|
||||
Folder dbFolder = database.folderDao().getFolderByName(folder.getName(), account.getId());
|
||||
|
||||
if (dbFolder != null)
|
||||
emitter.onSuccess(dbFolder.getId());
|
||||
else
|
||||
emitter.onSuccess((int) database.folderDao().compatInsert(folder));
|
||||
}).flatMap(folderId -> {
|
||||
List<Feed> feeds = entry.getValue();
|
||||
for (Feed feed : feeds) {
|
||||
feed.setFolderId(folderId);
|
||||
}
|
||||
|
||||
List<ParsingResult> parsingResults = ParsingResult.toParsingResults(feeds);
|
||||
return addFeeds(parsingResults);
|
||||
}).flatMapCompletable(feedInsertionResults -> Completable.complete());
|
||||
|
||||
completableList.add(completable);
|
||||
}
|
||||
|
||||
return Completable.concat(completableList);
|
||||
}
|
||||
|
||||
public Completable updateFeed(Feed feed) {
|
||||
return Completable.create(emitter -> {
|
||||
database.feedDao().updateFeedFields(feed.getId(), feed.getName(), feed.getUrl(), feed.getFolderId());
|
||||
emitter.onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
public Completable deleteFeed(Feed feed) {
|
||||
return database.feedDao().delete(feed);
|
||||
}
|
||||
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return database.folderDao().insert(folder);
|
||||
}
|
||||
|
||||
public Completable updateFolder(Folder folder) {
|
||||
return database.folderDao().update(folder);
|
||||
}
|
||||
|
||||
public Completable deleteFolder(Folder folder) {
|
||||
return database.folderDao().delete(folder);
|
||||
}
|
||||
|
||||
public Completable setItemReadState(Item item) {
|
||||
if (account.getConfig().getUseSeparateState()) {
|
||||
return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), true)
|
||||
.andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(),
|
||||
item.isStarred(), item.getRemoteId(), account.getId())));
|
||||
} else if (account.isLocal()) {
|
||||
return database.itemDao().setReadState(item.getId(), item.isRead());
|
||||
} else { // nextcloud case
|
||||
return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), false)
|
||||
.andThen(database.itemDao().setReadState(item.getId(), item.isRead()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Completable setAllItemsReadState(boolean read) {
|
||||
if (account.isLocal()) { // TODO see if it's possible to implement for others accounts
|
||||
return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId());
|
||||
} else {
|
||||
return Completable.complete();
|
||||
}
|
||||
}
|
||||
|
||||
public Completable setAllFeedItemsReadState(int feedId, boolean read) {
|
||||
if (account.isLocal()) {
|
||||
return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0);
|
||||
} else {
|
||||
return Completable.complete();
|
||||
}
|
||||
}
|
||||
|
||||
public Completable setItemStarState(Item item) {
|
||||
if (account.getConfig().getUseSeparateState()) {
|
||||
return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), true)
|
||||
.andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(),
|
||||
item.isStarred(), item.getRemoteId(), account.getId())));
|
||||
} else if (account.isLocal()) {
|
||||
return database.itemDao().setStarState(item.getId(), item.isRead());
|
||||
} else { // nextcloud case
|
||||
return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), false)
|
||||
.andThen(database.itemDao().setStarState(item.getId(), item.isStarred()));
|
||||
}
|
||||
}
|
||||
|
||||
public Single<Integer> getFeedCount(int accountId) {
|
||||
return database.feedDao().getFeedCount(accountId);
|
||||
}
|
||||
|
||||
public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
|
||||
return Single.create(emitter -> {
|
||||
List<Folder> folders = database.folderDao().getFolders(account.getId());
|
||||
Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Comparator.nullsLast(Folder::compareTo));
|
||||
|
||||
for (Folder folder : folders) {
|
||||
List<Feed> feeds = database.feedDao().getFeedsByFolder(folder.getId());
|
||||
|
||||
for (Feed feed : feeds) {
|
||||
int unreadCount = database.itemDao().getUnreadCount(feed.getId());
|
||||
feed.setUnreadCount(unreadCount);
|
||||
}
|
||||
|
||||
foldersWithFeeds.put(folder, feeds);
|
||||
}
|
||||
|
||||
// feeds without folder
|
||||
List<Feed> feedsWithoutFolder = database.feedDao().getFeedsWithoutFolder(account.getId());
|
||||
for (Feed feed : feedsWithoutFolder) {
|
||||
feed.setUnreadCount(database.itemDao().getUnreadCount(feed.getId()));
|
||||
}
|
||||
|
||||
foldersWithFeeds.put(null, feedsWithoutFolder);
|
||||
|
||||
emitter.onSuccess(foldersWithFeeds);
|
||||
});
|
||||
}
|
||||
|
||||
protected void setFeedColors(Feed feed) {
|
||||
FeedColorsKt.setFeedColors(feed);
|
||||
database.feedDao().updateColors(feed.getId(),
|
||||
feed.getTextColor(), feed.getBackgroundColor());
|
||||
}
|
||||
|
||||
protected void setFeedsColors(List<Feed> feeds) {
|
||||
Intent intent = new Intent(context, FeedsColorsIntentService.class);
|
||||
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
|
||||
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public SyncResult getSyncResult() {
|
||||
return syncResult;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package com.readrops.app.repositories
|
||||
|
||||
import com.readrops.db.entities.Feed
|
||||
|
||||
interface FeedUpdate {
|
||||
|
||||
fun onNext(feed: Feed)
|
||||
|
||||
}
|
|
@ -1,301 +0,0 @@
|
|||
package com.readrops.app.repositories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.TimingLogger;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.services.SyncType;
|
||||
import com.readrops.api.services.freshrss.FreshRSSDataSource;
|
||||
import com.readrops.api.services.freshrss.FreshRSSSyncData;
|
||||
import com.readrops.app.addfeed.FeedInsertionResult;
|
||||
import com.readrops.app.addfeed.ParsingResult;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.ItemState;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.ItemReadStarState;
|
||||
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public class FreshRSSRepository extends ARepository {
|
||||
|
||||
private static final String TAG = FreshRSSRepository.class.getSimpleName();
|
||||
|
||||
private final FreshRSSDataSource dataSource;
|
||||
|
||||
public FreshRSSRepository(FreshRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
|
||||
super(database, context, account);
|
||||
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable login(Account account, boolean insert) {
|
||||
setCredentials(account);
|
||||
|
||||
return dataSource.login(account.getLogin(), account.getPassword())
|
||||
.flatMap(token -> {
|
||||
account.setToken(token);
|
||||
setCredentials(account);
|
||||
|
||||
return dataSource.getWriteToken();
|
||||
})
|
||||
.flatMap(writeToken -> {
|
||||
account.setWriteToken(writeToken);
|
||||
|
||||
return dataSource.getUserInfo();
|
||||
})
|
||||
.flatMapCompletable(userInfo -> {
|
||||
account.setDisplayedName(userInfo.getUserName());
|
||||
|
||||
if (insert) {
|
||||
return database.accountDao().insert(account)
|
||||
.flatMapCompletable(id -> {
|
||||
account.setId(id.intValue());
|
||||
|
||||
return Completable.complete();
|
||||
});
|
||||
}
|
||||
|
||||
return Completable.complete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable sync(@Nullable List<Feed> feeds, @Nullable FeedUpdate update) {
|
||||
FreshRSSSyncData syncData = new FreshRSSSyncData();
|
||||
SyncType syncType;
|
||||
|
||||
if (account.getLastModified() != 0) {
|
||||
syncType = SyncType.CLASSIC_SYNC;
|
||||
syncData.setLastModified(account.getLastModified());
|
||||
} else
|
||||
syncType = SyncType.INITIAL_SYNC;
|
||||
|
||||
long newLastModified = DateTime.now().getMillis() / 1000L;
|
||||
TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer");
|
||||
|
||||
return Single.<FreshRSSSyncData>create(emitter -> {
|
||||
List<ItemReadStarState> itemStateChanges = database
|
||||
.itemStateChangesDao()
|
||||
.getItemStateChanges(account.getId());
|
||||
|
||||
syncData.setReadIds(itemStateChanges.stream()
|
||||
.filter(it -> it.getReadChange() && it.getRead())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
syncData.setUnreadIds(itemStateChanges.stream()
|
||||
.filter(it -> it.getReadChange() && !it.getRead())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
syncData.setStarredIds(itemStateChanges.stream()
|
||||
.filter(it -> it.getStarChange() && it.getStarred())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
syncData.setUnstarredIds(itemStateChanges.stream()
|
||||
.filter(it -> it.getStarChange() && !it.getStarred())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
emitter.onSuccess(syncData);
|
||||
}).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken()))
|
||||
.flatMapCompletable(syncResult -> {
|
||||
logger.addSplit("server queries");
|
||||
|
||||
insertFolders(syncResult.getFolders());
|
||||
logger.addSplit("folders insertion");
|
||||
insertFeeds(syncResult.getFeeds());
|
||||
logger.addSplit("feeds insertion");
|
||||
|
||||
insertItems(syncResult.getItems(), false);
|
||||
logger.addSplit("items insertion");
|
||||
|
||||
insertItems(syncResult.getStarredItems(), true);
|
||||
logger.addSplit("starred items insertion");
|
||||
|
||||
insertItemsIds(syncResult.getUnreadIds(), syncResult.getReadIds(), syncResult.getStarredIds());
|
||||
logger.addSplit("insert and update items ids");
|
||||
|
||||
account.setLastModified(newLastModified);
|
||||
database.accountDao().updateLastModified(account.getId(), newLastModified);
|
||||
|
||||
database.itemStateChangesDao().resetStateChanges(account.getId());
|
||||
|
||||
logger.dumpToLog();
|
||||
|
||||
this.syncResult = syncResult;
|
||||
|
||||
return Completable.complete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
|
||||
List<Completable> completableList = new ArrayList<>();
|
||||
List<FeedInsertionResult> insertionResults = new ArrayList<>();
|
||||
|
||||
for (ParsingResult result : results) {
|
||||
completableList.add(dataSource.createFeed(account.getWriteToken(), result.getUrl())
|
||||
.doOnComplete(() -> {
|
||||
FeedInsertionResult feedInsertionResult = new FeedInsertionResult();
|
||||
feedInsertionResult.setParsingResult(result);
|
||||
insertionResults.add(feedInsertionResult);
|
||||
}).onErrorResumeNext(throwable -> {
|
||||
Log.d(TAG, throwable.getMessage());
|
||||
|
||||
FeedInsertionResult feedInsertionResult = new FeedInsertionResult();
|
||||
|
||||
feedInsertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.ERROR);
|
||||
feedInsertionResult.setParsingResult(result);
|
||||
insertionResults.add(feedInsertionResult);
|
||||
|
||||
return Completable.complete();
|
||||
}));
|
||||
}
|
||||
|
||||
return Completable.concat(completableList)
|
||||
.andThen(Single.just(insertionResults));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable updateFeed(Feed feed) {
|
||||
return Single.<Folder>create(emitter -> {
|
||||
Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
|
||||
emitter.onSuccess(folder);
|
||||
|
||||
}).flatMapCompletable(folder -> dataSource.updateFeed(account.getWriteToken(),
|
||||
feed.getUrl(), feed.getName(), folder == null ? null : folder.getRemoteId())
|
||||
.andThen(super.updateFeed(feed)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable deleteFeed(Feed feed) {
|
||||
return dataSource.deleteFeed(account.getWriteToken(), feed.getUrl())
|
||||
.andThen(super.deleteFeed(feed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
return dataSource.createFolder(account.getWriteToken(), folder.getName())
|
||||
.andThen(super.addFolder(folder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable updateFolder(Folder folder) {
|
||||
return dataSource.updateFolder(account.getWriteToken(), folder.getRemoteId(), folder.getName())
|
||||
.andThen(Completable.create(emitter -> {
|
||||
folder.setRemoteId("user/-/label/" + folder.getName());
|
||||
emitter.onComplete();
|
||||
}))
|
||||
.andThen(super.updateFolder(folder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable deleteFolder(Folder folder) {
|
||||
return dataSource.deleteFolder(account.getWriteToken(), folder.getRemoteId())
|
||||
.andThen(super.deleteFolder(folder));
|
||||
}
|
||||
|
||||
private void insertFeeds(List<Feed> freshRSSFeeds) {
|
||||
freshRSSFeeds.stream().forEach(feed -> feed.setAccountId(account.getId()));
|
||||
|
||||
List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(freshRSSFeeds, account);
|
||||
|
||||
if (!insertedFeedsIds.isEmpty()) {
|
||||
setFeedsColors(database.feedDao().selectFromIdList(insertedFeedsIds));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void insertFolders(List<Folder> freshRSSFolders) {
|
||||
freshRSSFolders.stream().forEach(folder -> folder.setAccountId(account.getId()));
|
||||
|
||||
database.folderDao().foldersUpsert(freshRSSFolders, account);
|
||||
}
|
||||
|
||||
private void insertItems(List<Item> items, boolean starredItems) {
|
||||
List<Item> itemsToInsert = new ArrayList<>();
|
||||
Map<String, Integer> itemsFeedsIds = new HashMap<>();
|
||||
|
||||
for (Item item : items) {
|
||||
Integer feedId;
|
||||
if (itemsFeedsIds.containsKey(item.getFeedRemoteId())) {
|
||||
feedId = itemsFeedsIds.get(item.getFeedRemoteId());
|
||||
} else {
|
||||
feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
|
||||
itemsFeedsIds.put(item.getFeedRemoteId(), feedId);
|
||||
}
|
||||
|
||||
item.setFeedId(feedId);
|
||||
if (item.getText() != null) {
|
||||
item.setReadTime(Utils.readTimeFromString(item.getText()));
|
||||
}
|
||||
|
||||
|
||||
// workaround to avoid inserting starred items coming from the main item call
|
||||
// as the API exclusion filter doesn't seem to work
|
||||
if (!starredItems) {
|
||||
if (!item.isStarred()) {
|
||||
itemsToInsert.add(item);
|
||||
}
|
||||
} else {
|
||||
itemsToInsert.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (!itemsToInsert.isEmpty()) {
|
||||
Collections.sort(itemsToInsert, Item::compareTo);
|
||||
database.itemDao().insert(itemsToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertItemsIds(List<String> unreadIds, List<String> readIds, List<String> starredIds) {
|
||||
database.itemStateDao().deleteItemsStates(account.getId());
|
||||
|
||||
database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> {
|
||||
boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1;
|
||||
if (starred) {
|
||||
starredIds.remove(id);
|
||||
}
|
||||
|
||||
return new ItemState(0, false, starred, id, account.getId());
|
||||
}
|
||||
).collect(Collectors.toList()));
|
||||
|
||||
database.itemStateDao().insertItemStates(readIds.stream().map(id -> {
|
||||
boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1;
|
||||
if (starred) {
|
||||
starredIds.remove(id);
|
||||
}
|
||||
|
||||
return new ItemState(0, true, starred, id, account.getId());
|
||||
}
|
||||
).collect(Collectors.toList()));
|
||||
|
||||
// insert starred items ids which are read
|
||||
if (!starredIds.isEmpty()) {
|
||||
database.itemStateDao().insertItemStates(starredIds.stream().map(id ->
|
||||
new ItemState(0, true, true, id, account.getId()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
package com.readrops.app.repositories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.localfeed.LocalRSSDataSource;
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.utils.ApiUtils;
|
||||
import com.readrops.api.utils.exceptions.ParseException;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.app.addfeed.FeedInsertionResult;
|
||||
import com.readrops.app.addfeed.ParsingResult;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import kotlin.Pair;
|
||||
import okhttp3.Headers;
|
||||
|
||||
public class LocalFeedRepository extends ARepository {
|
||||
|
||||
private static final String TAG = LocalFeedRepository.class.getSimpleName();
|
||||
|
||||
private LocalRSSDataSource dataSource;
|
||||
|
||||
public LocalFeedRepository(LocalRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
|
||||
super(database, context, account);
|
||||
|
||||
syncResult = new SyncResult();
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable login(Account account, boolean insert) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable sync(@Nullable List<Feed> feeds, FeedUpdate update) {
|
||||
return Completable.create(emitter -> {
|
||||
List<Feed> feedList;
|
||||
|
||||
if (feeds == null || feeds.isEmpty()) {
|
||||
feedList = database.feedDao().getFeeds(account.getId());
|
||||
} else {
|
||||
feedList = feeds;
|
||||
}
|
||||
|
||||
for (Feed feed : feedList) {
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
mainHandler.post(() -> update.onNext(feed));
|
||||
|
||||
try {
|
||||
Headers.Builder headers = new Headers.Builder();
|
||||
if (feed.getEtag() != null) {
|
||||
headers.add(ApiUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
|
||||
}
|
||||
if (feed.getLastModified() != null) {
|
||||
headers.add(ApiUtils.IF_MODIFIED_HEADER, feed.getLastModified());
|
||||
}
|
||||
|
||||
Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build());
|
||||
|
||||
if (pair != null) {
|
||||
insertNewItems(feed, pair.getSecond());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "sync: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
|
||||
return Single.create(emitter -> {
|
||||
List<FeedInsertionResult> insertionResults = new ArrayList<>();
|
||||
|
||||
for (ParsingResult parsingResult : results) {
|
||||
FeedInsertionResult insertionResult = new FeedInsertionResult();
|
||||
|
||||
try {
|
||||
Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(parsingResult.getUrl(),
|
||||
null);
|
||||
Feed feed = insertFeed(pair.getFirst(), parsingResult);
|
||||
|
||||
if (feed != null) {
|
||||
insertionResult.setFeed(feed);
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
|
||||
} catch (UnknownFormatException e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "addFeeds: " + e.getMessage());
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
|
||||
} finally {
|
||||
insertionResult.setParsingResult(parsingResult);
|
||||
insertionResults.add(insertionResult);
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onSuccess(insertionResults);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("SimplifyStreamApiCallChains")
|
||||
private void insertNewItems(Feed feed, List<Item> items) {
|
||||
database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), feed.getId());
|
||||
|
||||
Collections.sort(items, Item::compareTo);
|
||||
|
||||
int maxItems = Integer.parseInt(SharedPreferencesManager.readString(
|
||||
SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
|
||||
if (maxItems > 0 && items.size() > maxItems) {
|
||||
items = items.subList(items.size() - maxItems, items.size());
|
||||
}
|
||||
|
||||
items.stream().forEach(item -> item.setFeedId(feed.getId()));
|
||||
insertItems(items, feed);
|
||||
}
|
||||
|
||||
private Feed insertFeed(Feed feed, ParsingResult parsingResult) {
|
||||
feed.setFolderId(parsingResult.getFolderId());
|
||||
|
||||
if (database.feedDao().feedExists(feed.getUrl(), account.getId())) {
|
||||
return null; // feed already inserted
|
||||
}
|
||||
|
||||
setFeedColors(feed);
|
||||
feed.setAccountId(account.getId());
|
||||
|
||||
// we need empty headers to query the feed just after, without any 304 result
|
||||
feed.setEtag(null);
|
||||
feed.setLastModified(null);
|
||||
|
||||
feed.setId((int) (database.feedDao().compatInsert(feed)));
|
||||
return feed;
|
||||
}
|
||||
|
||||
private void insertItems(Collection<Item> items, Feed feed) {
|
||||
List<Item> itemsToInsert = new ArrayList<>();
|
||||
|
||||
for (Item dbItem : items) {
|
||||
if (!database.itemDao().itemExists(dbItem.getGuid(), feed.getAccountId())) {
|
||||
if (dbItem.getDescription() != null) {
|
||||
dbItem.setCleanDescription(Jsoup.parse(dbItem.getDescription()).text());
|
||||
}
|
||||
|
||||
if (dbItem.getContent() != null) {
|
||||
dbItem.setReadTime(Utils.readTimeFromString(dbItem.getContent()));
|
||||
} else if (dbItem.getDescription() != null) {
|
||||
dbItem.setReadTime(Utils.readTimeFromString(dbItem.getCleanDescription()));
|
||||
}
|
||||
|
||||
itemsToInsert.add(dbItem);
|
||||
}
|
||||
}
|
||||
|
||||
syncResult.getItems().addAll(itemsToInsert);
|
||||
database.itemDao().insert(itemsToInsert);
|
||||
}
|
||||
}
|
|
@ -1,352 +0,0 @@
|
|||
package com.readrops.app.repositories;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.util.Log;
|
||||
import android.util.TimingLogger;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.services.SyncResult;
|
||||
import com.readrops.api.services.SyncType;
|
||||
import com.readrops.api.services.nextcloudnews.NextNewsDataSource;
|
||||
import com.readrops.api.services.nextcloudnews.NextcloudNewsSyncData;
|
||||
import com.readrops.api.utils.exceptions.UnknownFormatException;
|
||||
import com.readrops.app.addfeed.FeedInsertionResult;
|
||||
import com.readrops.app.addfeed.ParsingResult;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.Database;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.Item;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.pojo.ItemReadStarState;
|
||||
|
||||
import org.joda.time.LocalDateTime;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class NextNewsRepository extends ARepository {
|
||||
|
||||
private static final String TAG = NextNewsRepository.class.getSimpleName();
|
||||
|
||||
private final NextNewsDataSource dataSource;
|
||||
|
||||
public NextNewsRepository(NextNewsDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
|
||||
super(database, context, account);
|
||||
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable login(Account account, boolean insert) {
|
||||
setCredentials(account);
|
||||
return Single.<String>create(emitter -> {
|
||||
OkHttpClient httpClient = KoinJavaComponent.get(OkHttpClient.class);
|
||||
|
||||
String displayName = dataSource.login(httpClient, account);
|
||||
emitter.onSuccess(displayName);
|
||||
}).flatMapCompletable(displayName -> {
|
||||
account.setDisplayedName(displayName);
|
||||
account.setCurrentAccount(true);
|
||||
|
||||
if (insert) {
|
||||
return database.accountDao().insert(account)
|
||||
.flatMapCompletable(id -> {
|
||||
account.setId(id.intValue());
|
||||
return Completable.complete();
|
||||
});
|
||||
}
|
||||
|
||||
return Completable.complete();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable sync(@Nullable List<Feed> feeds, @Nullable FeedUpdate update) {
|
||||
setCredentials(account);
|
||||
return Completable.create(emitter -> {
|
||||
try {
|
||||
long lastModified = LocalDateTime.now().toDateTime().getMillis();
|
||||
SyncType syncType;
|
||||
|
||||
if (account.getLastModified() != 0) {
|
||||
syncType = SyncType.CLASSIC_SYNC;
|
||||
} else {
|
||||
syncType = SyncType.INITIAL_SYNC;
|
||||
}
|
||||
|
||||
NextcloudNewsSyncData syncData = new NextcloudNewsSyncData();
|
||||
|
||||
/*if (syncType == SyncType.CLASSIC_SYNC) {
|
||||
syncData.setLastModified(account.getLastModified() / 1000L);
|
||||
|
||||
List<ItemReadStarState> itemStateChanges = database
|
||||
.itemStateChangesDao()
|
||||
.getNextcloudNewsStateChanges(account.getId());
|
||||
|
||||
syncData.setReadIds(itemStateChanges.stream()
|
||||
.filter(it -> it.getReadChange() && it.getRead())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
syncData.setUnreadIds(itemStateChanges.stream()
|
||||
.filter(it -> it.getReadChange() && !it.getRead())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
List<String> starredItemsIds = itemStateChanges.stream()
|
||||
.filter(it -> it.getStarChange() && it.getStarred())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!starredItemsIds.isEmpty()) {
|
||||
syncData.setStarredIds(database.itemDao().getStarChanges(starredItemsIds, account.getId()));
|
||||
}
|
||||
|
||||
List<String> unstarredItemsIds = itemStateChanges.stream()
|
||||
.filter(it -> it.getStarChange() && !it.getStarred())
|
||||
.map(ItemReadStarState::getRemoteId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!unstarredItemsIds.isEmpty()) {
|
||||
syncData.setUnstarredIds(database.itemDao().getStarChanges(unstarredItemsIds, account.getId()));
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase());
|
||||
SyncResult result = dataSource.sync(syncType, syncData);
|
||||
timings.addSplit("server queries");
|
||||
|
||||
if (!result.isError()) {
|
||||
syncResult = new SyncResult();
|
||||
|
||||
insertFolders(result.getFolders());
|
||||
timings.addSplit("insert folders");
|
||||
|
||||
insertFeeds(result.getFeeds(), false);
|
||||
timings.addSplit("insert feeds");
|
||||
|
||||
boolean initialSync = syncType == SyncType.INITIAL_SYNC;
|
||||
insertItems(result.getItems(), initialSync);
|
||||
timings.addSplit("insert items");
|
||||
|
||||
insertItems(result.getStarredItems(), initialSync);
|
||||
timings.dumpToLog();
|
||||
|
||||
account.setLastModified(lastModified);
|
||||
database.accountDao().updateLastModified(account.getId(), lastModified);
|
||||
|
||||
database.itemStateChangesDao().resetStateChanges(account.getId());
|
||||
|
||||
emitter.onComplete();
|
||||
} else {
|
||||
emitter.onError(new Throwable());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "sync: " + e.getMessage());
|
||||
emitter.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
|
||||
setCredentials(account);
|
||||
return Single.create(emitter -> {
|
||||
List<FeedInsertionResult> feedInsertionResults = new ArrayList<>();
|
||||
|
||||
for (ParsingResult result : results) {
|
||||
FeedInsertionResult insertionResult = new FeedInsertionResult();
|
||||
|
||||
try {
|
||||
List<Feed> nextNewsFeeds = dataSource.createFeed(result.getUrl(), 0);
|
||||
|
||||
if (nextNewsFeeds != null) {
|
||||
List<Feed> newFeeds = insertFeeds(nextNewsFeeds, true);
|
||||
// there is always only one object in the list, see nextcloud news dataSource doc
|
||||
insertionResult.setFeed(newFeeds.get(0));
|
||||
} else
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
|
||||
|
||||
insertionResult.setParsingResult(result);
|
||||
} catch (Exception e) {
|
||||
if (e instanceof IOException)
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
|
||||
else if (e instanceof UnknownFormatException)
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
|
||||
else if (e instanceof SQLiteConstraintException)
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.DB_ERROR);
|
||||
else
|
||||
insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
|
||||
}
|
||||
|
||||
feedInsertionResults.add(insertionResult);
|
||||
}
|
||||
|
||||
emitter.onSuccess(feedInsertionResults);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable updateFeed(Feed feed) {
|
||||
setCredentials(account);
|
||||
return Completable.create(emitter -> {
|
||||
Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
|
||||
|
||||
if (folder != null)
|
||||
feed.setRemoteFolderId(folder.getRemoteId());
|
||||
else
|
||||
feed.setRemoteFolderId(String.valueOf(0)); // 0 for no folder
|
||||
|
||||
try {
|
||||
if (dataSource.renameFeed(feed) && dataSource.changeFeedFolder(feed)) {
|
||||
emitter.onComplete();
|
||||
} else
|
||||
emitter.onError(new Exception("Unknown error when updating feed"));
|
||||
} catch (Exception e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
}).andThen(super.updateFeed(feed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable deleteFeed(Feed feed) {
|
||||
setCredentials(account);
|
||||
return Completable.create(emitter -> {
|
||||
try {
|
||||
if (dataSource.deleteFeed(Integer.parseInt(feed.getRemoteId()))) {
|
||||
emitter.onComplete();
|
||||
} else
|
||||
emitter.onError(new Exception("Unknown error"));
|
||||
} catch (Exception e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
|
||||
emitter.onComplete();
|
||||
}).andThen(super.deleteFeed(feed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<Long> addFolder(Folder folder) {
|
||||
setCredentials(account);
|
||||
return Single.<Folder>create(emitter -> {
|
||||
try {
|
||||
List<Folder> folders = dataSource.createFolder(folder);
|
||||
|
||||
if (folders != null) {
|
||||
Folder nextNewsFolder = folders.get(0); // always only one item returned by the server, see doc
|
||||
folder.setRemoteId(nextNewsFolder.getRemoteId());
|
||||
|
||||
emitter.onSuccess(folder);
|
||||
} else
|
||||
emitter.onError(new Exception("Unknown error"));
|
||||
} catch (Exception e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
}).flatMap(folder1 -> database.folderDao().insert(folder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable updateFolder(Folder folder) {
|
||||
setCredentials(account);
|
||||
return Completable.create(emitter -> {
|
||||
try {
|
||||
if (dataSource.renameFolder(folder)) {
|
||||
emitter.onComplete();
|
||||
} else
|
||||
emitter.onError(new Exception("Unknown error"));
|
||||
|
||||
} catch (Exception e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
|
||||
emitter.onComplete();
|
||||
}).andThen(super.updateFolder(folder));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Completable deleteFolder(Folder folder) {
|
||||
setCredentials(account);
|
||||
return Completable.create(emitter -> {
|
||||
try {
|
||||
if (dataSource.deleteFolder(folder)) {
|
||||
emitter.onComplete();
|
||||
} else
|
||||
emitter.onError(new Exception("Unknown error"));
|
||||
|
||||
} catch (Exception e) {
|
||||
emitter.onError(e);
|
||||
}
|
||||
|
||||
emitter.onComplete();
|
||||
}).andThen(super.deleteFolder(folder));
|
||||
}
|
||||
|
||||
private List<Feed> insertFeeds(List<Feed> nextNewsFeeds, boolean newFeeds) {
|
||||
for (Feed nextNewsFeed : nextNewsFeeds) {
|
||||
nextNewsFeed.setAccountId(account.getId());
|
||||
}
|
||||
|
||||
List<Long> insertedFeedsIds;
|
||||
if (newFeeds) {
|
||||
insertedFeedsIds = database.feedDao().insert(nextNewsFeeds);
|
||||
} else {
|
||||
insertedFeedsIds = database.feedDao().feedsUpsert(nextNewsFeeds, account);
|
||||
}
|
||||
|
||||
List<Feed> insertedFeeds = new ArrayList<>();
|
||||
if (!insertedFeedsIds.isEmpty()) {
|
||||
insertedFeeds.addAll(database.feedDao().selectFromIdList(insertedFeedsIds));
|
||||
setFeedsColors(insertedFeeds);
|
||||
}
|
||||
|
||||
return insertedFeeds;
|
||||
}
|
||||
|
||||
private void insertFolders(List<Folder> nextNewsFolders) {
|
||||
for (Folder folder : nextNewsFolders) {
|
||||
folder.setAccountId(account.getId());
|
||||
}
|
||||
|
||||
database.folderDao().foldersUpsert(nextNewsFolders, account);
|
||||
}
|
||||
|
||||
private void insertItems(List<Item> items, boolean initialSync) {
|
||||
List<Item> itemsToInsert = new ArrayList<>();
|
||||
|
||||
for (Item item : items) {
|
||||
int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
|
||||
|
||||
//if the item already exists, update only its read state
|
||||
if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(item.getRemoteId()), feedId)) {
|
||||
database.itemDao().setReadAndStarState(item.getRemoteId(), item.isRead(), item.isStarred());
|
||||
continue;
|
||||
}
|
||||
|
||||
item.setFeedId(feedId);
|
||||
item.setReadTime(Utils.readTimeFromString(item.getContent()));
|
||||
|
||||
itemsToInsert.add(item);
|
||||
}
|
||||
|
||||
if (!itemsToInsert.isEmpty()) {
|
||||
syncResult.setItems(itemsToInsert);
|
||||
|
||||
Collections.sort(itemsToInsert, Item::compareTo);
|
||||
database.itemDao().insert(itemsToInsert);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,325 +0,0 @@
|
|||
package com.readrops.app.settings;
|
||||
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static com.readrops.app.utils.OPMLHelper.OPEN_OPML_FILE_REQUEST;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
|
||||
import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.readrops.api.opml.OPMLParser;
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.ReadropsApp;
|
||||
import com.readrops.app.account.AccountViewModel;
|
||||
import com.readrops.app.account.AddAccountActivity;
|
||||
import com.readrops.app.feedsfolders.ManageFeedsFoldersActivity;
|
||||
import com.readrops.app.notifications.NotificationPermissionActivity;
|
||||
import com.readrops.app.utils.FileUtils;
|
||||
import com.readrops.app.utils.OPMLHelper;
|
||||
import com.readrops.app.utils.PermissionManager;
|
||||
import com.readrops.app.utils.SharedPreferencesManager;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.entities.Feed;
|
||||
import com.readrops.db.entities.Folder;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
import com.readrops.db.entities.account.AccountType;
|
||||
|
||||
import org.koin.android.compat.ViewModelCompat;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.observers.DisposableCompletableObserver;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
|
||||
/**
|
||||
* A simple {@link Fragment} subclass.
|
||||
*/
|
||||
public class AccountSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
private static final String TAG = AccountSettingsFragment.class.getSimpleName();
|
||||
|
||||
private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
|
||||
|
||||
private Account account;
|
||||
private AccountViewModel viewModel;
|
||||
|
||||
public AccountSettingsFragment() {
|
||||
|
||||
}
|
||||
|
||||
public static AccountSettingsFragment newInstance(Account account) {
|
||||
AccountSettingsFragment fragment = new AccountSettingsFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putParcelable(ACCOUNT, account);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.acount_preferences);
|
||||
|
||||
account = getArguments().getParcelable(ACCOUNT);
|
||||
|
||||
Preference feedsFoldersPref = findPreference("feeds_folders_key");
|
||||
Preference credentialsPref = findPreference("credentials_key");
|
||||
Preference deleteAccountPref = findPreference("delete_account_key");
|
||||
Preference opmlPref = findPreference("opml_import_export");
|
||||
Preference notificationPref = findPreference("notifications");
|
||||
|
||||
if (account.is(AccountType.LOCAL))
|
||||
credentialsPref.setVisible(false);
|
||||
|
||||
if (!account.is(AccountType.LOCAL))
|
||||
opmlPref.setVisible(false);
|
||||
|
||||
feedsFoldersPref.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(getContext(), ManageFeedsFoldersActivity.class);
|
||||
intent.putExtra(ACCOUNT, account);
|
||||
startActivity(intent);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
credentialsPref.setOnPreferenceClickListener(preference -> {
|
||||
if (!account.isLocal()) {
|
||||
Intent intent = new Intent(getContext(), AddAccountActivity.class);
|
||||
intent.putExtra(EDIT_ACCOUNT, account);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
deleteAccountPref.setOnPreferenceClickListener(preference -> {
|
||||
deleteAccount();
|
||||
return true;
|
||||
});
|
||||
|
||||
opmlPref.setOnPreferenceClickListener(preference -> {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.items(R.array.opml_import_export)
|
||||
.itemsCallback(((dialog, itemView, position, text) -> openOPMLMode(position)))
|
||||
.show();
|
||||
return true;
|
||||
});
|
||||
|
||||
notificationPref.setOnPreferenceClickListener(preference -> {
|
||||
Intent intent = new Intent(getContext(), NotificationPermissionActivity.class);
|
||||
intent.putExtra(ACCOUNT_ID, account.getId());
|
||||
|
||||
startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
|
||||
viewModel.setAccount(account);
|
||||
}
|
||||
|
||||
private void deleteAccount() {
|
||||
new MaterialDialog.Builder(getContext())
|
||||
.title(R.string.delete_account_question)
|
||||
.positiveText(R.string.validate)
|
||||
.negativeText(R.string.cancel)
|
||||
.onPositive(((dialog, which) -> {
|
||||
SharedPreferencesManager.remove(account.getLoginKey());
|
||||
SharedPreferencesManager.remove(account.getPasswordKey());
|
||||
|
||||
viewModel.delete(account)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
getActivity().finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Utils.showSnackbar(getView(), e.getMessage());
|
||||
}
|
||||
});
|
||||
}))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openOPMLMode(int position) {
|
||||
if (position == 0) {
|
||||
OPMLHelper.openFileIntent(this);
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||
if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
exportAsOPMLFile();
|
||||
} else {
|
||||
requestExternalStoragePermission();
|
||||
}
|
||||
} else {
|
||||
exportAsOPMLFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region opml import
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
|
||||
Uri uri = data.getData();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.opml_processing)
|
||||
.content(R.string.operation_takes_time)
|
||||
.progress(true, 100)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
try {
|
||||
parseOPMLFile(uri, dialog);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.d(TAG, e.getMessage());
|
||||
displayErrorMessage();
|
||||
}
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
private void parseOPMLFile(Uri uri, MaterialDialog dialog) throws FileNotFoundException {
|
||||
viewModel.parseOPMLFile(uri, getContext())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new DisposableCompletableObserver() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
dialog.dismiss();
|
||||
|
||||
displayErrorMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void displayErrorMessage() {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.processing_file_failed)
|
||||
.neutralText(R.string.cancel)
|
||||
.iconRes(R.drawable.ic_error)
|
||||
.show();
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region opml export
|
||||
|
||||
private void exportAsOPMLFile() {
|
||||
String fileName = "subscriptions.opml";
|
||||
|
||||
try {
|
||||
String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/x-opml", outputStream -> {
|
||||
Map<Folder, List<Feed>> folderListMap = viewModel.getFoldersWithFeeds()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.blockingGet();
|
||||
|
||||
|
||||
/*OPMLParser.write(folderListMap, outputStream)
|
||||
.blockingAwait();*/
|
||||
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
|
||||
displayNotification(fileName, path);
|
||||
} catch (Exception e) {
|
||||
displayErrorMessage();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void displayNotification(String name, String absolutePath) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.parse(absolutePath), "text/plain");
|
||||
|
||||
int intentFlag;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
intentFlag = PendingIntent.FLAG_IMMUTABLE;
|
||||
} else {
|
||||
intentFlag = PendingIntent.FLAG_IMMUTABLE;
|
||||
}
|
||||
|
||||
Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.opml_export))
|
||||
.setContentText(name)
|
||||
.setSmallIcon(R.drawable.ic_notif)
|
||||
.setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, intentFlag))
|
||||
.setAutoCancel(true)
|
||||
.build();
|
||||
|
||||
NotificationManagerCompat manager = NotificationManagerCompat.from(getContext());
|
||||
manager.notify(2, notification);
|
||||
}
|
||||
|
||||
private void requestExternalStoragePermission() {
|
||||
PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == WRITE_EXTERNAL_STORAGE_REQUEST) {
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
|
||||
if (shouldShowRequestPermissionRationale(permissions[0])) {
|
||||
Utils.showSnackBarWithAction(getView(), getString(R.string.external_storage_opml_export),
|
||||
getString(R.string.try_again), v -> requestExternalStoragePermission());
|
||||
} else {
|
||||
Utils.showSnackBarWithAction(getView(), getString(R.string.external_storage_opml_export),
|
||||
getString(R.string.permissions), v -> {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
||||
intent.setData(Uri.fromParts("package", getContext().getPackageName(), null));
|
||||
getContext().startActivity(intent);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
exportAsOPMLFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package com.readrops.app.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.db.entities.account.Account;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
|
||||
import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Account account = getIntent().getParcelableExtra(ACCOUNT);
|
||||
|
||||
SettingsKey settingsKey = SettingsKey.values()[getIntent().getIntExtra(SETTINGS, -1)];
|
||||
Fragment fragment = null;
|
||||
|
||||
switch (settingsKey) {
|
||||
case ACCOUNT_SETTINGS:
|
||||
fragment = AccountSettingsFragment.newInstance(account);
|
||||
setTitle(account.getAccountName());
|
||||
break;
|
||||
case SETTINGS:
|
||||
fragment = new SettingsFragment();
|
||||
setTitle(R.string.settings);
|
||||
break;
|
||||
}
|
||||
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings_activity_fragment, fragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public enum SettingsKey {
|
||||
ACCOUNT_SETTINGS,
|
||||
SETTINGS
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package com.readrops.app.settings;
|
||||
|
||||
import static com.readrops.app.utils.ReadropsKeys.FEEDS;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.notifications.sync.SyncWorker;
|
||||
import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
|
||||
import com.readrops.db.Database;
|
||||
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
Preference feedsColorsPreference = findPreference("reload_feeds_colors");
|
||||
Preference themePreference = findPreference("dark_theme");
|
||||
Preference synchroPreference = findPreference("auto_synchro");
|
||||
|
||||
|
||||
AtomicBoolean serviceStarted = new AtomicBoolean(false);
|
||||
feedsColorsPreference.setOnPreferenceClickListener(preference -> {
|
||||
Database database = KoinJavaComponent.get(Database.class);
|
||||
|
||||
database.feedDao().getAllFeeds().observe(getActivity(), feeds -> {
|
||||
if (!serviceStarted.get()) {
|
||||
Intent intent = new Intent(getContext(), FeedsColorsIntentService.class);
|
||||
intent.putParcelableArrayListExtra(FEEDS, new ArrayList<>(feeds));
|
||||
|
||||
getContext().startService(intent);
|
||||
serviceStarted.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
themePreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||
if (newValue.equals(getString(R.string.theme_value_light))) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
} else if (newValue.equals(getString(R.string.theme_value_dark))) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
} else {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
synchroPreference.setOnPreferenceChangeListener(((preference, newValue) -> {
|
||||
WorkManager workManager = WorkManager.getInstance(getContext());
|
||||
Pair<Integer, TimeUnit> interval = getWorkerInterval((String) newValue);
|
||||
|
||||
if (interval != null) {
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build();
|
||||
|
||||
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(SyncWorker.class, interval.first, interval.second)
|
||||
.addTag(SyncWorker.Companion.getTAG())
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(interval.first, interval.second)
|
||||
.build();
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(SyncWorker.Companion.getTAG(), ExistingPeriodicWorkPolicy.REPLACE, request);
|
||||
} else {
|
||||
workManager.cancelAllWorkByTag(SyncWorker.Companion.getTAG());
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Pair<Integer, TimeUnit> getWorkerInterval(String newValue) {
|
||||
int interval;
|
||||
TimeUnit timeUnit;
|
||||
|
||||
switch (newValue) {
|
||||
case "0.30":
|
||||
interval = 30;
|
||||
timeUnit = TimeUnit.MINUTES;
|
||||
break;
|
||||
case "1":
|
||||
interval = 1;
|
||||
timeUnit = TimeUnit.HOURS;
|
||||
break;
|
||||
case "2":
|
||||
interval = 2;
|
||||
timeUnit = TimeUnit.HOURS;
|
||||
break;
|
||||
case "3":
|
||||
interval = 3;
|
||||
timeUnit = TimeUnit.HOURS;
|
||||
break;
|
||||
case "6":
|
||||
interval = 6;
|
||||
timeUnit = TimeUnit.HOURS;
|
||||
break;
|
||||
case "12":
|
||||
interval = 12;
|
||||
timeUnit = TimeUnit.HOURS;
|
||||
break;
|
||||
case "24":
|
||||
interval = 1;
|
||||
timeUnit = TimeUnit.DAYS;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Pair<>(interval, timeUnit);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package com.readrops.app.utils
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
object FileUtils {
|
||||
|
||||
@JvmStatic
|
||||
fun writeDownloadFile(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
writeFileApi29(context, fileName, mimeType, listener)
|
||||
else
|
||||
writeFileApi28(fileName, listener)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun writeFileApi29(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String {
|
||||
val resolver = context.contentResolver
|
||||
val downloadsUri = MediaStore.Downloads
|
||||
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
|
||||
val fileDetails = ContentValues().apply {
|
||||
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.Downloads.IS_PENDING, 1)
|
||||
put(MediaStore.Downloads.MIME_TYPE, mimeType)
|
||||
}
|
||||
|
||||
val contentUri = resolver.insert(downloadsUri, fileDetails)
|
||||
|
||||
resolver.openOutputStream(contentUri!!)!!.use { stream ->
|
||||
try {
|
||||
listener(stream)
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
} finally {
|
||||
stream.flush()
|
||||
stream.close()
|
||||
}
|
||||
|
||||
fileDetails.put(MediaStore.Downloads.IS_PENDING, 0)
|
||||
resolver.update(contentUri, fileDetails, null, null)
|
||||
}
|
||||
|
||||
fileDetails.put(MediaStore.Downloads.IS_PENDING, 0)
|
||||
resolver.update(contentUri, fileDetails, null, null)
|
||||
|
||||
return contentUri.path!!
|
||||
}
|
||||
|
||||
private fun writeFileApi28(fileName: String, listener: (OutputStream) -> Unit): String {
|
||||
val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
|
||||
val file = File(filePath, fileName)
|
||||
|
||||
val outputStream = FileOutputStream(file)
|
||||
listener(outputStream)
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
return file.absolutePath
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
package com.readrops.app.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.api.localfeed.LocalRSSHelper;
|
||||
import com.readrops.api.utils.ApiUtils;
|
||||
import com.readrops.api.utils.AuthInterceptor;
|
||||
import com.readrops.app.addfeed.ParsingResult;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public final class HtmlParser {
|
||||
|
||||
private static final String TAG = HtmlParser.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Parse the html page to get all rss urls
|
||||
*
|
||||
* @param url url to request
|
||||
* @return a list of rss urls with their title
|
||||
*/
|
||||
public static List<ParsingResult> getFeedLink(String url) {
|
||||
List<ParsingResult> results = new ArrayList<>();
|
||||
|
||||
String head = getHTMLHeadFromUrl(url);
|
||||
if (head != null) {
|
||||
Document document = Jsoup.parse(head, url);
|
||||
|
||||
Elements elements = document.select("link");
|
||||
|
||||
for (Element element : elements) {
|
||||
String type = element.attributes().get("type");
|
||||
|
||||
if (LocalRSSHelper.isRSSType(type)) {
|
||||
String feedUrl = element.absUrl("href");
|
||||
String label = element.attributes().get("title");
|
||||
|
||||
results.add(new ParsingResult(feedUrl, label));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getFaviconLink(@NonNull String url) {
|
||||
String favUrl = null;
|
||||
|
||||
String head = getHTMLHeadFromUrl(url);
|
||||
if (head == null)
|
||||
return null;
|
||||
|
||||
Document document = Jsoup.parse(head, url);
|
||||
Elements elements = document.select("link");
|
||||
|
||||
for (Element element : elements) {
|
||||
if (element.attributes().get("rel").toLowerCase().contains("icon")) {
|
||||
favUrl = element.absUrl("href");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return favUrl;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getHTMLHeadFromUrl(@NonNull String url) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
Response response = KoinJavaComponent.<OkHttpClient>get(OkHttpClient.class)
|
||||
.newCall(new Request.Builder().url(url).build()).execute();
|
||||
KoinJavaComponent.<AuthInterceptor>get(AuthInterceptor.class).setCredentials(null);
|
||||
|
||||
if (response.header("Content-Type").contains(ApiUtils.HTML_CONTENT_TYPE)) {
|
||||
String body = response.body().string();
|
||||
String head = body.substring(body.indexOf("<head"), body.indexOf("</head>"));
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
Log.d(TAG, "parsing time : " + (end - start));
|
||||
|
||||
return head;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package com.readrops.app.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
object OPMLHelper {
|
||||
|
||||
const val OPEN_OPML_FILE_REQUEST = 1
|
||||
|
||||
@JvmStatic
|
||||
fun openFileIntent(activity: Activity) =
|
||||
activity.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST)
|
||||
|
||||
@JvmStatic
|
||||
fun openFileIntent(fragment: Fragment) =
|
||||
fragment.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST)
|
||||
|
||||
|
||||
private fun createIntent(): Intent {
|
||||
return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package com.readrops.app.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
object PermissionManager {
|
||||
|
||||
@JvmStatic
|
||||
fun isPermissionGranted(context: Context, permission: String): Boolean =
|
||||
ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
@JvmStatic
|
||||
fun requestPermissions(activity: Activity, requestCode: Int, vararg permissions: String) =
|
||||
ActivityCompat.requestPermissions(activity, permissions, requestCode)
|
||||
|
||||
@JvmStatic
|
||||
fun requestPermissions(fragment: Fragment, requestCode: Int, vararg permissions: String) =
|
||||
fragment.requestPermissions(permissions, requestCode)
|
||||
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package com.readrops.app.utils
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import java.io.InputStream
|
||||
|
||||
@GlideModule
|
||||
class ReadropsGlideModule : AppGlideModule(), KoinComponent {
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
val factory = OkHttpUrlLoader.Factory(get<OkHttpClient>())
|
||||
|
||||
glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory)
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package com.readrops.app.utils
|
||||
|
||||
object ReadropsKeys {
|
||||
|
||||
const val ACCOUNT = "ACCOUNT_KEY"
|
||||
const val ACCOUNT_ID = "ACCOUNT_ID"
|
||||
const val ACCOUNT_TYPE = "ACCOUNT_TYPE_KEY"
|
||||
const val EDIT_ACCOUNT = "EDIT_ACCOUNT"
|
||||
|
||||
const val FROM_MAIN_ACTIVITY = "FROM_MAIN_ACTIVITY_KEY"
|
||||
|
||||
const val ITEM_ID = "ITEM_ID_KEY"
|
||||
const val IMAGE_URL = "IMAGE_URL_KEY"
|
||||
|
||||
const val SYNCING = "SYNCING_KEY"
|
||||
|
||||
const val SETTINGS = "SETTINGS_KEY"
|
||||
|
||||
const val WEB_URL = "WEB_URL_KEY"
|
||||
const val ACTION_BAR_COLOR = "ACTION_BAR_COLOR_KEY"
|
||||
|
||||
const val FEEDS = "FEEDS"
|
||||
|
||||
const val STARRED_ITEM = "STARRED_ITEM"
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package com.readrops.app.utils;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
public final class SharedPreferencesManager {
|
||||
|
||||
public static void writeValue(String key, Object value) {
|
||||
SharedPreferences sharedPref = KoinJavaComponent.get(SharedPreferences.class);
|
||||
SharedPreferences.Editor editor = sharedPref.edit();
|
||||
|
||||
if (value instanceof Boolean)
|
||||
editor.putBoolean(key, (Boolean) value);
|
||||
else if (value instanceof String)
|
||||
editor.putString(key, (String) value);
|
||||
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static void writeValue(SharedPrefKey sharedPrefKey, Object value) {
|
||||
writeValue(sharedPrefKey.key, value);
|
||||
}
|
||||
|
||||
public static int readInt(SharedPrefKey sharedPrefKey) {
|
||||
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
|
||||
return sharedPreferences.getInt(sharedPrefKey.key, sharedPrefKey.getIntDefaultValue());
|
||||
}
|
||||
|
||||
public static boolean readBoolean(SharedPrefKey sharedPrefKey) {
|
||||
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
|
||||
return sharedPreferences.getBoolean(sharedPrefKey.key, sharedPrefKey.getBooleanDefaultValue());
|
||||
}
|
||||
|
||||
public static String readString(String key) {
|
||||
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
|
||||
return sharedPreferences.getString(key, null);
|
||||
}
|
||||
|
||||
public static String readString(SharedPrefKey sharedPrefKey) {
|
||||
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
|
||||
return sharedPreferences.getString(sharedPrefKey.key, sharedPrefKey.getStringDefaultValue());
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
|
||||
SharedPreferences.Editor editor = sharedPreferences.edit();
|
||||
|
||||
editor.remove(key);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public enum SharedPrefKey {
|
||||
SHOW_READ_ARTICLES("show_read_articles", false),
|
||||
ITEMS_TO_PARSE_MAX_NB("items_to_parse_max_nb", "20"),
|
||||
OPEN_ITEMS_IN("open_items_in", "0"),
|
||||
DARK_THEME("dark_theme", "false"),
|
||||
AUTO_SYNCHRO("auto_synchro", "0"),
|
||||
HIDE_FEEDS("hide_feeds", false),
|
||||
MARK_ITEMS_READ_ON_SCROLL("mark_items_read", false);
|
||||
|
||||
@NonNull
|
||||
private String key;
|
||||
@NonNull
|
||||
private Object defaultValue;
|
||||
|
||||
public boolean getBooleanDefaultValue() {
|
||||
return Boolean.valueOf(defaultValue.toString());
|
||||
}
|
||||
|
||||
public String getStringDefaultValue() {
|
||||
return (String) defaultValue;
|
||||
}
|
||||
|
||||
public int getIntDefaultValue() {
|
||||
return Integer.parseInt(defaultValue.toString());
|
||||
}
|
||||
|
||||
SharedPrefKey(@NonNull String key, @NonNull Object defaultValue) {
|
||||
this.key = key;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
package com.readrops.app.utils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import org.koin.java.KoinJavaComponent;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public final class Utils {
|
||||
|
||||
public static final String HTTP_PREFIX = "http://";
|
||||
|
||||
public static final String HTTPS_PREFIX = "https://";
|
||||
|
||||
private static final int AVERAGE_WORDS_PER_MINUTE = 250;
|
||||
|
||||
public static Bitmap getImageFromUrl(String url) {
|
||||
try {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
|
||||
Response response = KoinJavaComponent.<OkHttpClient>get(OkHttpClient.class).newCall(request).execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
return BitmapFactory.decodeStream(inputStream);
|
||||
} else
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return null; // no way to get the favicon
|
||||
}
|
||||
}
|
||||
|
||||
public static double readTimeFromString(String value) {
|
||||
int nbWords = value.split("\\s+").length;
|
||||
|
||||
return (double) nbWords / AVERAGE_WORDS_PER_MINUTE;
|
||||
}
|
||||
|
||||
public static String getCssColor(@ColorInt int color) {
|
||||
return String.format(Locale.US, "rgba(%d,%d,%d,%.2f)",
|
||||
Color.red(color),
|
||||
Color.green(color),
|
||||
Color.blue(color),
|
||||
Color.alpha(color) / 255.0);
|
||||
}
|
||||
|
||||
public static boolean isTypeImage(@NonNull String type) {
|
||||
return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
|
||||
|| type.equals("image/png");
|
||||
}
|
||||
|
||||
public static void setDrawableColor(Drawable drawable, @ColorInt int color) {
|
||||
drawable.mutate().setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
|
||||
}
|
||||
|
||||
public static Drawable drawableWithColor(@ColorInt int color) {
|
||||
ShapeDrawable drawable = new ShapeDrawable(new OvalShape());
|
||||
drawable.setIntrinsicWidth(50);
|
||||
drawable.setIntrinsicHeight(50);
|
||||
|
||||
drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
|
||||
public static void showSnackBarWithAction(View root, String message, String action, View.OnClickListener listener) {
|
||||
Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.setAction(action, listener);
|
||||
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
public static void showSnackbar(View root, String message) {
|
||||
Snackbar snackbar = Snackbar.make(root, message, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
public static Bitmap getBitmapFromDrawable(Drawable drawable) {
|
||||
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
|
||||
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
|
||||
return bitmap;
|
||||
|
||||
}
|
||||
|
||||
public static boolean isColorTooBright(@ColorInt int color) {
|
||||
return getColorLuma(color) > 210;
|
||||
}
|
||||
|
||||
public static boolean isColorTooDark(@ColorInt int color) {
|
||||
return getColorLuma(color) < 40;
|
||||
}
|
||||
|
||||
private static double getColorLuma(@ColorInt int color) {
|
||||
int r = (color >> 16) & 0xff;
|
||||
int g = (color >> 8) & 0xff;
|
||||
int b = (color >> 0) & 0xff;
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
package com.readrops.app.utils.customviews;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.LayoutRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.mikepenz.fastadapter.IClickable;
|
||||
import com.mikepenz.fastadapter.IItem;
|
||||
import com.mikepenz.fastadapter.listeners.OnClickListener;
|
||||
import com.mikepenz.iconics.IconicsDrawable;
|
||||
import com.mikepenz.materialdrawer.Drawer;
|
||||
import com.mikepenz.materialdrawer.holder.BadgeStyle;
|
||||
import com.mikepenz.materialdrawer.holder.ColorHolder;
|
||||
import com.mikepenz.materialdrawer.holder.StringHolder;
|
||||
import com.mikepenz.materialdrawer.icons.MaterialDrawerFont;
|
||||
import com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem;
|
||||
import com.mikepenz.materialdrawer.model.BaseViewHolder;
|
||||
import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable;
|
||||
import com.readrops.app.R;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This a simple modification of original ExpandableBadgeDrawerItem from MaterialDrawer lib to get two click events from an expandable drawer item
|
||||
*/
|
||||
public class CustomExpandableBadgeDrawerItem extends BaseDescribeableDrawerItem<CustomExpandableBadgeDrawerItem,
|
||||
CustomExpandableBadgeDrawerItem.ViewHolder>
|
||||
implements ColorfulBadgeable<CustomExpandableBadgeDrawerItem>, IClickable {
|
||||
|
||||
protected ColorHolder arrowColor;
|
||||
|
||||
protected int arrowRotationAngleStart = 0;
|
||||
|
||||
protected int arrowRotationAngleEnd = 180;
|
||||
|
||||
protected StringHolder mBadge;
|
||||
protected BadgeStyle mBadgeStyle = new BadgeStyle();
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return R.id.material_drawer_item_expandable_badge;
|
||||
}
|
||||
|
||||
@Override
|
||||
@LayoutRes
|
||||
public int getLayoutRes() {
|
||||
return R.layout.custom_expandable_drawer_item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(CustomExpandableBadgeDrawerItem.ViewHolder viewHolder, List payloads) {
|
||||
super.bindView(viewHolder, payloads);
|
||||
|
||||
Context ctx = viewHolder.itemView.getContext();
|
||||
//bind the basic view parts
|
||||
bindViewHelper(viewHolder);
|
||||
|
||||
//set the text for the badge or hide
|
||||
boolean badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge);
|
||||
//style the badge if it is visible
|
||||
if (true) {
|
||||
mBadgeStyle.style(viewHolder.badge, getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx)));
|
||||
viewHolder.badgeContainer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
viewHolder.badgeContainer.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
//define the typeface for our textViews
|
||||
if (getTypeface() != null) {
|
||||
viewHolder.badge.setTypeface(getTypeface());
|
||||
}
|
||||
|
||||
//make sure all animations are stopped
|
||||
if (viewHolder.arrow.getDrawable() instanceof IconicsDrawable) {
|
||||
((IconicsDrawable) viewHolder.arrow.getDrawable()).color(this.arrowColor != null ? this.arrowColor.color(ctx) : getIconColor(ctx));
|
||||
}
|
||||
viewHolder.arrow.clearAnimation();
|
||||
if (!isExpanded()) {
|
||||
viewHolder.arrow.setRotation(this.arrowRotationAngleStart);
|
||||
} else {
|
||||
viewHolder.arrow.setRotation(this.arrowRotationAngleEnd);
|
||||
}
|
||||
|
||||
//call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required)
|
||||
onPostBindView(this, viewHolder.itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomExpandableBadgeDrawerItem withOnDrawerItemClickListener(Drawer.OnDrawerItemClickListener onDrawerItemClickListener) {
|
||||
mOnDrawerItemClickListener = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawer.OnDrawerItemClickListener getOnDrawerItemClickListener() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomExpandableBadgeDrawerItem withBadge(StringHolder badge) {
|
||||
this.mBadge = badge;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomExpandableBadgeDrawerItem withBadge(String badge) {
|
||||
this.mBadge = new StringHolder(badge);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomExpandableBadgeDrawerItem withBadge(@StringRes int badgeRes) {
|
||||
this.mBadge = new StringHolder(badgeRes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomExpandableBadgeDrawerItem withBadgeStyle(BadgeStyle badgeStyle) {
|
||||
this.mBadgeStyle = badgeStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StringHolder getBadge() {
|
||||
return mBadge;
|
||||
}
|
||||
|
||||
public BadgeStyle getBadgeStyle() {
|
||||
return mBadgeStyle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder getViewHolder(View v) {
|
||||
return new ViewHolder(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IItem withOnItemPreClickListener(OnClickListener onItemPreClickListener) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnClickListener getOnPreItemClickListener() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IItem withOnItemClickListener(OnClickListener onItemClickListener) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnClickListener getOnItemClickListener() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class ViewHolder extends BaseViewHolder {
|
||||
public ImageView arrow;
|
||||
public View badgeContainer;
|
||||
public TextView badge;
|
||||
|
||||
public ViewHolder(View view) {
|
||||
super(view);
|
||||
badgeContainer = view.findViewById(R.id.material_drawer_badge_container);
|
||||
badge = view.findViewById(R.id.material_drawer_badge);
|
||||
arrow = view.findViewById(R.id.material_drawer_arrow);
|
||||
arrow.setImageDrawable(new IconicsDrawable(view.getContext(), MaterialDrawerFont.Icon.mdf_expand_more).sizeDp(16).paddingDp(2).color(Color.BLACK));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package com.readrops.app.utils.customviews
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.databinding.EmptyListViewBinding
|
||||
|
||||
/**
|
||||
* A simple custom view to display a empty list message
|
||||
*/
|
||||
class EmptyListView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
|
||||
|
||||
val binding: EmptyListViewBinding = EmptyListViewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
init {
|
||||
val attributes = context.obtainStyledAttributes(attrs, R.styleable.EmptyListView)
|
||||
binding.emptyListImage.setImageDrawable(attributes.getDrawable(R.styleable.EmptyListView_image))
|
||||
binding.emptyListText.text = attributes.getString(R.styleable.EmptyListView_text)
|
||||
|
||||
attributes.recycle()
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
package com.readrops.app.utils.customviews
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
|
||||
class ReadropsItemTouchCallback(private val context: Context, private val config: Config) :
|
||||
ItemTouchHelper.SimpleCallback(config.dragDirs, config.swipeDirs) {
|
||||
|
||||
private val iconHorizontalMargin = 40
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
config.moveCallback?.onMove()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
config.swipeCallback?.onSwipe(viewHolder, direction)
|
||||
}
|
||||
|
||||
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
|
||||
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
|
||||
|
||||
val background: ColorDrawable
|
||||
var icon: Drawable? = null
|
||||
val itemView: View = viewHolder.itemView
|
||||
var draw = true // variable used to draw under some conditions
|
||||
|
||||
// do not draw anymore if the view has reached the screen's left/right side
|
||||
if (abs(dX).toInt() == itemView.right) {
|
||||
draw = false
|
||||
} else if (abs(dX).toInt() == 0) {
|
||||
draw = true
|
||||
}
|
||||
|
||||
// left swipe
|
||||
if (dX > 0 && config.leftDraw != null && draw) {
|
||||
background = ColorDrawable(config.leftDraw.bgColor)
|
||||
background.setBounds(itemView.left, itemView.top, dX.toInt(), itemView.bottom)
|
||||
|
||||
icon = config.leftDraw.drawable
|
||||
?: ContextCompat.getDrawable(context, config.leftDraw.iconRes)!!
|
||||
val iconMargin = (itemView.height - icon.intrinsicHeight) / 2
|
||||
icon.setBounds(itemView.left + iconHorizontalMargin, itemView.top + iconMargin,
|
||||
itemView.left + iconHorizontalMargin + icon.intrinsicWidth, itemView.bottom - iconMargin)
|
||||
// right swipe
|
||||
} else if (dX < 0 && config.rightDraw != null && draw) {
|
||||
background = ColorDrawable(config.rightDraw.bgColor)
|
||||
background.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
|
||||
|
||||
icon = config.rightDraw.drawable
|
||||
?: ContextCompat.getDrawable(context, config.rightDraw.iconRes)!!
|
||||
val iconMargin = (itemView.height - icon.intrinsicHeight) / 2
|
||||
icon.setBounds(itemView.right - iconHorizontalMargin - icon.intrinsicWidth, itemView.top + iconMargin,
|
||||
itemView.right - iconHorizontalMargin, itemView.bottom - iconMargin)
|
||||
} else {
|
||||
background = ColorDrawable()
|
||||
}
|
||||
|
||||
background.draw(c)
|
||||
|
||||
if (dX > 0)
|
||||
c.clipRect(itemView.left, itemView.top, dX.toInt(), itemView.bottom)
|
||||
else if (dX < 0)
|
||||
c.clipRect(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
|
||||
|
||||
icon?.draw(c)
|
||||
}
|
||||
|
||||
override fun isItemViewSwipeEnabled(): Boolean {
|
||||
return config.swipeCallback != null
|
||||
}
|
||||
|
||||
override fun isLongPressDragEnabled(): Boolean {
|
||||
return config.moveCallback != null
|
||||
}
|
||||
|
||||
interface MoveCallback {
|
||||
fun onMove()
|
||||
}
|
||||
|
||||
interface SwipeCallback {
|
||||
fun onSwipe(viewHolder: RecyclerView.ViewHolder, direction: Int)
|
||||
}
|
||||
|
||||
class SwipeDraw(@ColorInt val bgColor: Int, @DrawableRes val iconRes: Int = 0, val drawable: Drawable?)
|
||||
|
||||
class Config(val dragDirs: Int = 0, val swipeDirs: Int = 0, val moveCallback: MoveCallback? = null,
|
||||
val swipeCallback: SwipeCallback? = null, val leftDraw: SwipeDraw? = null, val rightDraw: SwipeDraw? = null) {
|
||||
|
||||
private constructor(builder: Builder) : this(builder.dragDirs, builder.swipeDirs,
|
||||
builder.moveCallback, builder.swipeCallback, builder.leftDraw, builder.rightDraw)
|
||||
|
||||
class Builder {
|
||||
var dragDirs: Int = 0
|
||||
private set
|
||||
|
||||
var swipeDirs: Int = 0
|
||||
private set
|
||||
|
||||
var moveCallback: MoveCallback? = null
|
||||
private set
|
||||
|
||||
var swipeCallback: SwipeCallback? = null
|
||||
private set
|
||||
|
||||
var leftDraw: SwipeDraw? = null
|
||||
private set
|
||||
|
||||
var rightDraw: SwipeDraw? = null
|
||||
private set
|
||||
|
||||
fun dragDirs(dragDirs: Int) = apply { this.dragDirs = dragDirs }
|
||||
|
||||
fun swipeDirs(swipeDirs: Int) = apply { this.swipeDirs = swipeDirs }
|
||||
|
||||
fun moveCallback(moveCallback: MoveCallback) = apply { this.moveCallback = moveCallback }
|
||||
|
||||
fun swipeCallback(swipeCallback: SwipeCallback) = apply { this.swipeCallback = swipeCallback }
|
||||
|
||||
fun leftDraw(@ColorInt bgColor: Int, @DrawableRes iconRes: Int, @Nullable icon: Drawable? = null) = apply { leftDraw = SwipeDraw(bgColor, iconRes, icon) }
|
||||
|
||||
fun rightDraw(@ColorInt bgColor: Int, @DrawableRes iconRes: Int, @Nullable icon: Drawable? = null) = apply { this.rightDraw = SwipeDraw(bgColor, iconRes, icon) }
|
||||
|
||||
fun build(): Config {
|
||||
return Config(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,118 +0,0 @@
|
|||
package com.readrops.app.utils.customviews;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Base64;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.readrops.app.R;
|
||||
import com.readrops.app.utils.Utils;
|
||||
import com.readrops.db.pojo.ItemWithFeed;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.parser.Parser;
|
||||
import org.jsoup.select.Elements;
|
||||
|
||||
public class ReadropsWebView extends WebView {
|
||||
|
||||
private ItemWithFeed itemWithFeed;
|
||||
|
||||
@ColorInt
|
||||
private int textColor;
|
||||
@ColorInt
|
||||
private int backgroundColor;
|
||||
|
||||
public ReadropsWebView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
getColors(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setItem(ItemWithFeed itemWithFeed) {
|
||||
this.itemWithFeed = itemWithFeed;
|
||||
|
||||
String text = getText();
|
||||
String base64Content = null;
|
||||
|
||||
if (text != null)
|
||||
base64Content = Base64.encodeToString(text.getBytes(), Base64.NO_PADDING);
|
||||
|
||||
loadData(base64Content, "text/html; charset=utf-8", "base64");
|
||||
}
|
||||
|
||||
public String getItemContent() {
|
||||
String content = itemWithFeed.getItem().getContent();
|
||||
return content;
|
||||
}
|
||||
|
||||
private void getColors(Context context, AttributeSet attrs) {
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ReadropsWebView);
|
||||
textColor = typedArray.getColor(R.styleable.ReadropsWebView_textColor, 0);
|
||||
backgroundColor = typedArray.getColor(R.styleable.ReadropsWebView_backgroundColor, 0);
|
||||
|
||||
typedArray.recycle();
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
private void init() {
|
||||
if (!isInEditMode()) {
|
||||
WebSettings settings = getSettings();
|
||||
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setBuiltInZoomControls(true);
|
||||
settings.setDisplayZoomControls(false);
|
||||
}
|
||||
|
||||
setVerticalScrollBarEnabled(false);
|
||||
setBackgroundColor(backgroundColor);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getText() {
|
||||
if (itemWithFeed.getItem().getText() != null) {
|
||||
Document document;
|
||||
|
||||
if (itemWithFeed.getWebsiteUrl() != null)
|
||||
document = Jsoup.parse(Parser.unescapeEntities(itemWithFeed.getItem().getText(), false), itemWithFeed.getWebsiteUrl());
|
||||
else
|
||||
document = Jsoup.parse(Parser.unescapeEntities(itemWithFeed.getItem().getText(), false));
|
||||
|
||||
formatDocument(document);
|
||||
|
||||
int color = itemWithFeed.getColor() != 0 ? itemWithFeed.getColor() : getResources().getColor(R.color.colorPrimary);
|
||||
return getContext().getString(R.string.webview_html_template,
|
||||
Utils.getCssColor(itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() :
|
||||
color),
|
||||
Utils.getCssColor(this.textColor),
|
||||
Utils.getCssColor(backgroundColor),
|
||||
document.body().html());
|
||||
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
private void formatDocument(Document document) {
|
||||
Elements elements = document.select("figure,figcaption");
|
||||
for (Element element : elements) {
|
||||
element.unwrap();
|
||||
}
|
||||
|
||||
elements.clear();
|
||||
elements = document.select("div,span");
|
||||
|
||||
for (Element element : elements) {
|
||||
element.clearAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package com.readrops.app.utils.feedscolors
|
||||
|
||||
import androidx.palette.graphics.Palette
|
||||
import com.readrops.app.utils.HtmlParser
|
||||
import com.readrops.app.utils.Utils
|
||||
import com.readrops.db.entities.Feed
|
||||
|
||||
fun setFeedColors(feed: Feed) {
|
||||
getFaviconLink(feed)
|
||||
|
||||
if (feed.iconUrl != null) {
|
||||
val bitmap = Utils.getImageFromUrl(feed.iconUrl) ?: return
|
||||
val palette = Palette.from(bitmap).generate()
|
||||
|
||||
val dominantSwatch = palette.dominantSwatch
|
||||
feed.textColor = if (dominantSwatch != null && !Utils.isColorTooBright(dominantSwatch.rgb)
|
||||
&& !Utils.isColorTooDark(dominantSwatch.rgb)) {
|
||||
dominantSwatch.rgb
|
||||
} else 0
|
||||
|
||||
|
||||
val mutedSwatch = palette.mutedSwatch
|
||||
feed.backgroundColor = if (mutedSwatch != null && !Utils.isColorTooBright(mutedSwatch.rgb)
|
||||
&& !Utils.isColorTooDark(mutedSwatch.rgb)) {
|
||||
mutedSwatch.rgb
|
||||
} else 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getFaviconLink(feed: Feed) {
|
||||
feed.iconUrl = if (feed.iconUrl != null)
|
||||
feed.iconUrl
|
||||
else
|
||||
HtmlParser.getFaviconLink(feed.siteUrl!!)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package com.readrops.app.utils.feedscolors
|
||||
|
||||
import android.app.IntentService
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.ReadropsApp
|
||||
import com.readrops.app.utils.ReadropsKeys.FEEDS
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
|
||||
class FeedsColorsIntentService : IntentService("FeedsColorsIntentService"), KoinComponent {
|
||||
|
||||
override fun onHandleIntent(intent: Intent?) {
|
||||
val feeds: List<Feed> = intent!!.getParcelableArrayListExtra(FEEDS)!!
|
||||
val database = get<Database>()
|
||||
|
||||
val notificationBuilder = NotificationCompat.Builder(this, ReadropsApp.FEEDS_COLORS_CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.get_feeds_colors))
|
||||
.setProgress(feeds.size, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_notif)
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
startForeground(NOTIFICATION_ID, notificationBuilder.build())
|
||||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
|
||||
var feedsNb = 0
|
||||
feeds.forEach {
|
||||
notificationBuilder.setContentText(it.name)
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||
setFeedColors(it)
|
||||
|
||||
database.feedDao().updateColors(it.id, it.textColor, it.backgroundColor)
|
||||
notificationBuilder.setProgress(feeds.size, ++feedsNb, false)
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||
}
|
||||
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:color="@android:color/darker_gray" android:state_enabled="false" />
|
||||
<item android:color="@android:color/white" />
|
||||
</selector>
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@color/colorPrimaryDark" />
|
||||
|
||||
<item android:gravity="center" android:width="120dp" android:height="120dp">
|
||||
<bitmap
|
||||
android:gravity="fill_horizontal|fill_vertical"
|
||||
android:src="@drawable/logo" />
|
||||
</item>
|
||||
</layer-list>
|
Binary file not shown.
Before Width: | Height: | Size: 151 KiB |
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22 0.03,-1.99 4,-3.08 6,-3.08 1.99,0 5.97,1.09 6,3.08 -1.29,1.94 -3.5,3.22 -6,3.22z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM6,10L6,7L4,7v3L1,10v2h3v3h2v-3h3v-2L6,10zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#11FF00"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#727272"
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z" />
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM19,14h-3v3h-2v-3h-3v-2h3L14,9h2v3h3v2z"/>
|
||||
</vector>
|
|
@ -1,20 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M119.105,183.425C107.127,195.403 86.992,194.681 74.134,181.823C61.275,168.964 60.557,148.832 72.535,136.854C84.513,124.877 104.645,125.595 117.5,138.451C130.364,151.315 131.083,171.447 119.105,183.425" />
|
||||
<path
|
||||
android:fillColor="#727272"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M113.728,159.622C110.739,162.611 104.392,161.119 99.558,156.29C94.718,151.454 93.23,145.105 96.223,142.113C99.215,139.121 105.559,140.61 110.396,145.448C115.227,150.281 116.718,156.628 113.728,159.622" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M16.301,127.979a111.96,111.829 89.999,1 0,223.658 0a111.96,111.829 89.999,1 0,-223.658 0z"
|
||||
android:strokeWidth="29.11"
|
||||
android:strokeColor="#000000" />
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#727272"
|
||||
android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z" />
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,4L5,4c-1.11,0 -2,0.9 -2,2v12c0,1.1 0.89,2 2,2h4v-2L5,18L5,8h14v10h-4v2h4c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.89,-2 -2,-2zM12,10l-4,4h3v6h2v-6h3l-4,-4z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
|
||||
</vector>
|
|
@ -1,24 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="133dp"
|
||||
android:height="154dp"
|
||||
android:viewportWidth="133"
|
||||
android:viewportHeight="154">
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M66.5,87.5m-59,0a59,59 0,1 1,118 0a59,59 0,1 1,-118 0"
|
||||
android:strokeWidth="15"
|
||||
android:strokeColor="#727272" />
|
||||
<path
|
||||
android:fillColor="#727272"
|
||||
android:pathData="M112.968,33.469l10.536,-10.536l9.037,9.037l-10.536,10.536z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M66.5,44L66.5,88"
|
||||
android:strokeWidth="15"
|
||||
android:strokeColor="#727272" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M45.5,7.5L87.5,7.5"
|
||||
android:strokeWidth="15"
|
||||
android:strokeColor="#727272" />
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
</vector>
|
|
@ -1,6 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M6.18,17.82m-2.18,0a2.18,2.18 0,1 1,4.36 0a2.18,2.18 0,1 1,-4.36 0"/>
|
||||
<path android:fillColor="#FF000000" android:pathData="M4,4.44v2.83c7.03,0 12.73,5.7 12.73,12.73h2.83c0,-8.59 -6.97,-15.56 -15.56,-15.56zM4,10.1v2.83c3.9,0 7.07,3.17 7.07,7.07h2.83c0,-5.47 -4.43,-9.9 -9.9,-9.9z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M3,5h2L5,3c-1.1,0 -2,0.9 -2,2zM3,13h2v-2L3,11v2zM7,21h2v-2L7,19v2zM3,9h2L5,7L3,7v2zM13,3h-2v2h2L13,3zM19,3v2h2c0,-1.1 -0.9,-2 -2,-2zM5,21v-2L3,19c0,1.1 0.9,2 2,2zM3,17h2v-2L3,15v2zM9,3L7,3v2h2L9,3zM11,21h2v-2h-2v2zM19,13h2v-2h-2v2zM19,21c1.1,0 2,-0.9 2,-2h-2v2zM19,9h2L21,7h-2v2zM19,17h2v-2h-2v2zM15,21h2v-2h-2v2zM15,5h2L17,3h-2v2zM7,17h10L17,7L7,7v10zM9,9h6v6L9,15L9,9z"/>
|
||||
</vector>
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#727272"
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#727272"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,4L12,1L8,5l4,4L12,6c3.31,0 6,2.69 6,6 0,1.01 -0.25,1.97 -0.7,2.8l1.46,1.46C19.54,15.03 20,13.57 20,12c0,-4.42 -3.58,-8 -8,-8zM12,18c-3.31,0 -6,-2.69 -6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24,7.74C4.46,8.97 4,10.43 4,12c0,4.42 3.58,8 8,8v3l4,-4 -4,-4v3z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM12,17H6v-2h6V17zM15,13H9v-2h6V13zM18,9h-6V7h6V9z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#FF6200"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue