From 55d982dd22eeb78a6428f18055dfb116662fed46 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 20 Feb 2023 19:13:06 +0100 Subject: [PATCH 01/95] Update gradle and kotlin plugins --- api/build.gradle | 7 ++++--- api/src/debug/AndroidManifest.xml | 3 +-- api/src/main/AndroidManifest.xml | 3 +-- app/build.gradle | 7 ++++--- app/src/debug/AndroidManifest.xml | 3 +-- app/src/main/AndroidManifest.xml | 3 +-- build.gradle | 4 ++-- db/build.gradle | 7 ++++--- db/src/main/AndroidManifest.xml | 3 +-- db/src/main/java/com/readrops/db/Converters.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 11 files changed, 21 insertions(+), 23 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index 28a4906a..3b258810 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -12,9 +12,6 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - lintOptions { - abortOnError false - } sourceSets { androidTest.assets.srcDirs += files("$projectDir/androidTest/assets".toString()) @@ -39,6 +36,10 @@ android { kotlinOptions { jvmTarget = '1.8' } + lint { + abortOnError false + } + namespace 'com.readrops.api' } dependencies { diff --git a/api/src/debug/AndroidManifest.xml b/api/src/debug/AndroidManifest.xml index bc80eafd..dd871fc0 100644 --- a/api/src/debug/AndroidManifest.xml +++ b/api/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/api/src/main/AndroidManifest.xml b/api/src/main/AndroidManifest.xml index b1075664..0a0938ae 100644 --- a/api/src/main/AndroidManifest.xml +++ b/api/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - + diff --git a/app/build.gradle b/app/build.gradle index 48914c02..247458e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,9 +18,6 @@ android { testOptions { unitTests.returnDefaultValues = true } - lintOptions { - abortOnError false - } buildTypes { release { minifyEnabled true @@ -52,6 +49,10 @@ android { viewBinding true buildConfig true } + lint { + abortOnError false + } + namespace 'com.readrops.app' } dependencies { diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index a1d2a946..548b2db7 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -1,6 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> + xmlns:tools="http://schemas.android.com/tools"> diff --git a/build.gradle b/build.gradle index 2c6de9af..63ed0d02 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jacoco:org.jacoco.core:0.8.7" } diff --git a/db/build.gradle b/db/build.gradle index 646e0ba2..41690dc2 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -28,9 +28,6 @@ android { androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) } - lintOptions { - abortOnError false - } buildTypes { release { @@ -52,6 +49,10 @@ android { kotlinOptions { jvmTarget = '1.8' } + lint { + abortOnError false + } + namespace 'com.readrops.db' } // Needed for kapt starting with kotlin plugin 1.5 diff --git a/db/src/main/AndroidManifest.xml b/db/src/main/AndroidManifest.xml index aa1258a6..94cbbcfc 100644 --- a/db/src/main/AndroidManifest.xml +++ b/db/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/db/src/main/java/com/readrops/db/Converters.kt b/db/src/main/java/com/readrops/db/Converters.kt index 5291deeb..91b64589 100644 --- a/db/src/main/java/com/readrops/db/Converters.kt +++ b/db/src/main/java/com/readrops/db/Converters.kt @@ -4,7 +4,7 @@ import androidx.room.TypeConverter import com.readrops.db.entities.account.AccountType import org.joda.time.LocalDateTime -object Converters { +class Converters { @TypeConverter fun fromTimeStamp(value: Long): LocalDateTime { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0d90a675..7bd21ed8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip From 288b637da7b45faaf64369227d76bc80de120e86 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 21 Feb 2023 21:44:02 +0100 Subject: [PATCH 02/95] Integrate Compose into the project --- app/build.gradle | 18 ++++++++- .../com/readrops/app/ReadropsDebugApp.java | 2 +- app/src/main/AndroidManifest.xml | 24 ++++++++---- .../main/java/com/readrops/app/AppModule.kt | 6 +-- .../java/com/readrops/app/BaseActivity.kt | 37 +++++++++++++++++++ .../app/notifications/sync/SyncWorker.kt | 30 +++++++++++++-- .../app/settings/AccountSettingsFragment.java | 27 +++++++++----- build.gradle | 6 +-- 8 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/readrops/app/BaseActivity.kt diff --git a/app/build.gradle b/app/build.gradle index 247458e6..4978e99a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,10 +48,18 @@ android { buildFeatures { viewBinding true buildConfig true + compose true } + + composeOptions { + kotlinCompilerExtensionVersion = "1.4.0" + } + lint { abortOnError false } + + namespace 'com.readrops.app' } @@ -75,7 +83,7 @@ dependencies { 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.5.0" + implementation "androidx.work:work-runtime-ktx:2.8.0" implementation "androidx.fragment:fragment-ktx:1.3.5" implementation "androidx.browser:browser:1.3.0" @@ -104,4 +112,12 @@ dependencies { 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' + + def composeBom = platform('androidx.compose:compose-bom:2022.12.00') + implementation composeBom + androidTestImplementation composeBom + + implementation 'androidx.activity:activity-compose:1.5.1' + implementation 'androidx.compose.material:material' + } diff --git a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java index 2ef2a039..1eb6475d 100644 --- a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java +++ b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java @@ -24,7 +24,7 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi super.onCreate(); SoLoader.init(this, false); - initFlipper(); + //initFlipper(); } private void initFlipper() { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6301624c..420a509f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,12 +60,8 @@ android:name=".itemslist.MainActivity" android:label="@string/articles" android:launchMode="singleTask" - android:theme="@style/SplashTheme"> - - - - - + android:theme="@style/SplashTheme" + android:exported="true"> + android:parentActivityName=".itemslist.MainActivity" + android:exported="true"> - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index 8e9000b2..8a50624d 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -1,8 +1,6 @@ package com.readrops.app import androidx.preference.PreferenceManager -import com.chimerapps.niddler.core.AndroidNiddler -import com.chimerapps.niddler.core.Niddler import com.readrops.api.services.Credentials import com.readrops.app.account.AccountViewModel import com.readrops.app.addfeed.AddFeedsViewModel @@ -63,7 +61,7 @@ val appModule = module { single { PreferenceManager.getDefaultSharedPreferences(androidContext()) } - single { + /* single { val niddler = AndroidNiddler.Builder() .setNiddlerInformation(AndroidNiddler.fromApplication(get())) .setPort(0) @@ -73,5 +71,5 @@ val appModule = module { niddler.attachToApplication(get()) niddler.apply { start() } - } + }*/ } \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/BaseActivity.kt b/app/src/main/java/com/readrops/app/BaseActivity.kt new file mode 100644 index 00000000..426ac252 --- /dev/null +++ b/app/src/main/java/com/readrops/app/BaseActivity.kt @@ -0,0 +1,37 @@ +package com.readrops.app + +import android.os.Bundle +import android.os.PersistableBundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.ui.Modifier + +class BaseActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + + setContent { + MaterialTheme { + Scaffold { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues) + ) { + Row { + Text( + text = "Hello World", + ) + } + } + + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt index c91fa1a6..896e2cd2 100644 --- a/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt @@ -4,6 +4,7 @@ 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 @@ -95,13 +96,19 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex } } + 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, PendingIntent.FLAG_UPDATE_CURRENT)) + intent, intentFlag)) .setAutoCancel(true) notifContent.item?.let { @@ -126,8 +133,14 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex 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, PendingIntent.FLAG_UPDATE_CURRENT)) + PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, intentFlag)) .setAllowGeneratedReplies(false) .build() } @@ -137,10 +150,19 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex putExtra(ReadropsKeys.ITEM_ID, item.id) } - return NotificationCompat.Action.Builder(R.drawable.ic_read, applicationContext.getString(R.string.read), - PendingIntent.getBroadcast(applicationContext, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT)) + 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 { diff --git a/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java index f96ceb84..60ba3bb7 100644 --- a/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java +++ b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java @@ -1,6 +1,12 @@ 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; @@ -21,7 +27,6 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import com.afollestad.materialdialogs.MaterialDialog; -import com.readrops.app.utils.OPMLHelper; import com.readrops.api.opml.OPMLParser; import com.readrops.app.R; import com.readrops.app.ReadropsApp; @@ -30,6 +35,7 @@ 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; @@ -38,6 +44,8 @@ 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; @@ -47,14 +55,6 @@ import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.schedulers.Schedulers; import kotlin.Unit; -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 org.koin.android.compat.ViewModelCompat; - /** * A simple {@link Fragment} subclass. */ @@ -274,11 +274,18 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat { 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, PendingIntent.FLAG_UPDATE_CURRENT)) + .setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, intentFlag)) .setAutoCancel(true) .build(); diff --git a/build.gradle b/build.gradle index 63ed0d02..b8530fd1 100644 --- a/build.gradle +++ b/build.gradle @@ -30,10 +30,10 @@ allprojects { } ext { - compileSdkVersion = 30 + compileSdkVersion = 33 minSdkVersion = 21 - targetSdkVersion = 30 - buildToolsVersion = "30.0.3" + targetSdkVersion = 33 + buildToolsVersion = "33.0.2" koin_version = "3.1.2" } From c7d83362f96e78a2cb76b08459832d71aed69c83 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 25 Feb 2023 18:19:12 +0100 Subject: [PATCH 03/95] Add initial color and theming, far from being final --- .../java/com/readrops/app/compose/Color.kt | 68 ++++++++++++++ .../java/com/readrops/app/compose/Theme.kt | 90 +++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 app/src/main/java/com/readrops/app/compose/Color.kt create mode 100644 app/src/main/java/com/readrops/app/compose/Theme.kt diff --git a/app/src/main/java/com/readrops/app/compose/Color.kt b/app/src/main/java/com/readrops/app/compose/Color.kt new file mode 100644 index 00000000..11500362 --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/Color.kt @@ -0,0 +1,68 @@ +package com.readrops.app.compose + +import androidx.compose.ui.graphics.Color + +val md_theme_light_primary = Color(0xFF0062A2) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFD1E4FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001D35) +val md_theme_light_secondary = Color(0xFFA43D00) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFFFDBCD) +val md_theme_light_onSecondaryContainer = Color(0xFF360F00) +val md_theme_light_tertiary = Color(0xFF006D3D) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFF97F7B7) +val md_theme_light_onTertiaryContainer = Color(0xFF00210F) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFF8FDFF) +val md_theme_light_onBackground = Color(0xFF001F25) +val md_theme_light_surface = Color(0xFFF8FDFF) +val md_theme_light_onSurface = Color(0xFF001F25) +val md_theme_light_surfaceVariant = Color(0xFFDFE2EB) +val md_theme_light_onSurfaceVariant = Color(0xFF42474E) +val md_theme_light_outline = Color(0xFF73777F) +val md_theme_light_inverseOnSurface = Color(0xFFD6F6FF) +val md_theme_light_inverseSurface = Color(0xFF00363F) +val md_theme_light_inversePrimary = Color(0xFF9DCAFF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF0062A2) +val md_theme_light_outlineVariant = Color(0xFFC3C7CF) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFF9DCAFF) +val md_theme_dark_onPrimary = Color(0xFF003257) +val md_theme_dark_primaryContainer = Color(0xFF00497C) +val md_theme_dark_onPrimaryContainer = Color(0xFFD1E4FF) +val md_theme_dark_secondary = Color(0xFFFFB597) +val md_theme_dark_onSecondary = Color(0xFF581D00) +val md_theme_dark_secondaryContainer = Color(0xFF7D2D00) +val md_theme_dark_onSecondaryContainer = Color(0xFFFFDBCD) +val md_theme_dark_tertiary = Color(0xFF7BDA9C) +val md_theme_dark_onTertiary = Color(0xFF00391D) +val md_theme_dark_tertiaryContainer = Color(0xFF00522C) +val md_theme_dark_onTertiaryContainer = Color(0xFF97F7B7) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF001F25) +val md_theme_dark_onBackground = Color(0xFFA6EEFF) +val md_theme_dark_surface = Color(0xFF001F25) +val md_theme_dark_onSurface = Color(0xFFA6EEFF) +val md_theme_dark_surfaceVariant = Color(0xFF42474E) +val md_theme_dark_onSurfaceVariant = Color(0xFFC3C7CF) +val md_theme_dark_outline = Color(0xFF8D9199) +val md_theme_dark_inverseOnSurface = Color(0xFF001F25) +val md_theme_dark_inverseSurface = Color(0xFFA6EEFF) +val md_theme_dark_inversePrimary = Color(0xFF0062A2) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF9DCAFF) +val md_theme_dark_outlineVariant = Color(0xFF42474E) +val md_theme_dark_scrim = Color(0xFF000000) + + +val seed = Color(0xFF0072BC) diff --git a/app/src/main/java/com/readrops/app/compose/Theme.kt b/app/src/main/java/com/readrops/app/compose/Theme.kt new file mode 100644 index 00000000..7e2086fa --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/Theme.kt @@ -0,0 +1,90 @@ +package com.readrops.app.compose + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + + +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +fun ReadropsTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (!useDarkTheme) { + LightColors + } else { + DarkColors + } + + MaterialTheme( + colorScheme = colors, + content = content + ) +} \ No newline at end of file From c2585f2dac550ed539dcd8ac01a9ee3603e5b88c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 26 Feb 2023 16:18:30 +0100 Subject: [PATCH 04/95] Introduce base of new UI navigation --- app/build.gradle | 11 ++- .../java/com/readrops/app/BaseActivity.kt | 91 +++++++++++++++---- .../app/compose/account/AccountScreen.kt | 17 ++++ .../readrops/app/compose/feeds/FeedsScreen.kt | 18 ++++ .../readrops/app/compose/more/MoreScreen.kt | 18 ++++ .../app/compose/timelime/TimelineItem.kt | 71 +++++++++++++++ .../app/compose/timelime/TimelineScreen.kt | 18 ++++ app/src/main/res/drawable/ic_timeline.xml | 5 + 8 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/readrops/app/compose/account/AccountScreen.kt create mode 100644 app/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt create mode 100644 app/src/main/java/com/readrops/app/compose/more/MoreScreen.kt create mode 100644 app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt create mode 100644 app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt create mode 100644 app/src/main/res/drawable/ic_timeline.xml diff --git a/app/build.gradle b/app/build.gradle index 4978e99a..e295aa7a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -118,6 +118,15 @@ dependencies { androidTestImplementation composeBom implementation 'androidx.activity:activity-compose:1.5.1' - implementation 'androidx.compose.material:material' + implementation 'androidx.compose.material3:material3' + def voyager = "1.0.0-rc03" + implementation "cafe.adriel.voyager:voyager-navigator:$voyager" + implementation "cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyager" + implementation "cafe.adriel.voyager:voyager-tab-navigator:$voyager" + implementation "cafe.adriel.voyager:voyager-androidx:$voyager" + //implementation "cafe.adriel.voyager:voyager-koin:$voyager" + + debugImplementation "androidx.compose.ui:ui-tooling:1.3.3" + implementation "androidx.compose.ui:ui-tooling-preview:1.3.3" } diff --git a/app/src/main/java/com/readrops/app/BaseActivity.kt b/app/src/main/java/com/readrops/app/BaseActivity.kt index 426ac252..d5bc080e 100644 --- a/app/src/main/java/com/readrops/app/BaseActivity.kt +++ b/app/src/main/java/com/readrops/app/BaseActivity.kt @@ -1,35 +1,92 @@ package com.readrops.app import android.os.Bundle -import android.os.PersistableBundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.* import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.Navigator +import com.readrops.app.compose.ReadropsTheme +import com.readrops.app.compose.account.AccountScreen +import com.readrops.app.compose.feeds.FeedsScreen +import com.readrops.app.compose.more.MoreScreen +import com.readrops.app.compose.timelime.TimelineScreen class BaseActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { - super.onCreate(savedInstanceState, persistentState) + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) setContent { - MaterialTheme { - Scaffold { paddingValues -> - Box( - modifier = Modifier.padding(paddingValues) - ) { - Row { - Text( - text = "Hello World", - ) + ReadropsTheme { + Navigator(screen = TimelineScreen()) { navigator -> + Scaffold( + bottomBar = { + BottomAppBar { + NavigationBarItem( + selected = false, + onClick = { navigator.push(TimelineScreen()) }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_timeline), + contentDescription = null + ) + }, + label = { Text("Timeline") } + ) + + NavigationBarItem( + selected = false, + onClick = { navigator.push(FeedsScreen()) }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_rss_feed_grey), + contentDescription = null + ) + }, + label = { Text("Feeds") } + ) + + NavigationBarItem( + selected = false, + onClick = { navigator.push(AccountScreen()) }, + icon = { + Icon( + imageVector = Icons.Default.AccountBox, + contentDescription = null, + ) + }, + label = { Text("Account") } + ) + + NavigationBarItem( + selected = false, + onClick = { navigator.push(MoreScreen()) }, + icon = { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + ) + }, + label = { Text("More") } + ) + } + }, + ) { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues) + ) { + CurrentScreen() } } - } } } diff --git a/app/src/main/java/com/readrops/app/compose/account/AccountScreen.kt b/app/src/main/java/com/readrops/app/compose/account/AccountScreen.kt new file mode 100644 index 00000000..d5250459 --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/account/AccountScreen.kt @@ -0,0 +1,17 @@ +package com.readrops.app.compose.account + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen + +class AccountScreen : AndroidScreen() { + + @Composable + override fun Content() { + Column { + Text(text = "Account") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt b/app/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt new file mode 100644 index 00000000..d0f463fd --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt @@ -0,0 +1,18 @@ +package com.readrops.app.compose.feeds + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen + +class FeedsScreen : AndroidScreen() { + + @Composable + override fun Content() { + Column { + Text( + text = "Feeds" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/compose/more/MoreScreen.kt b/app/src/main/java/com/readrops/app/compose/more/MoreScreen.kt new file mode 100644 index 00000000..d3d7832e --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/more/MoreScreen.kt @@ -0,0 +1,18 @@ +package com.readrops.app.compose.more + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen + +class MoreScreen : AndroidScreen() { + + @Composable + override fun Content() { + Column { + Text( + text = "More" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt new file mode 100644 index 00000000..9a88202e --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -0,0 +1,71 @@ +package com.readrops.app.compose.timelime + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview +@Composable +fun TimelineItem() { + Card( + // elevation = 4.card, + modifier = Modifier.background(Color.White) + .padding(8.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(id = com.readrops.app.R.drawable.ic_rss_feed_grey), + contentDescription = null, + // modifier = Modifier.size((MaterialTheme.typography.subtitle2.fontSize.value * 1.5).dp) + ) + +// Spacer(Modifier.padding(4.dp)) + + Text( + text = "feed name", + //style = MaterialTheme.typography. + ) + } + + Text( + text = "Item date", + // style = MaterialTheme.typography.subtitle2 + ) + } + + Spacer(Modifier.size(8.dp)) + + Text( + text = "title example", + //style = MaterialTheme.typography.h5, + ) + + Spacer(Modifier.size(8.dp)) + + /* Image( + painter = painterResource(id = com.readrops.app.R.drawable.header_background), + contentDescription = null + )*/ + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt b/app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt new file mode 100644 index 00000000..e17b14f3 --- /dev/null +++ b/app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt @@ -0,0 +1,18 @@ +package com.readrops.app.compose.timelime + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen + + +class TimelineScreen : AndroidScreen() { + + @Composable + override fun Content() { + Column { + TimelineItem() + } + } + + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_timeline.xml b/app/src/main/res/drawable/ic_timeline.xml new file mode 100644 index 00000000..68d26c91 --- /dev/null +++ b/app/src/main/res/drawable/ic_timeline.xml @@ -0,0 +1,5 @@ + + + From 43a9ccb218f2d3e21a0ce14d75904e9693920f05 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 26 Feb 2023 22:07:43 +0100 Subject: [PATCH 05/95] Add a new temporary module to work on app module compose/kotlin migration --- app/src/main/AndroidManifest.xml | 11 -- appcompose/.gitignore | 1 + appcompose/build.gradle | 80 +++++++++ appcompose/proguard-rules.pro | 21 +++ .../app/compose/ExampleInstrumentedTest.kt | 24 +++ appcompose/src/main/AndroidManifest.xml | 26 +++ .../com/readrops/app/compose}/BaseActivity.kt | 3 +- .../java/com/readrops/app/compose/Color.kt | 0 .../java/com/readrops/app/compose/Theme.kt | 0 .../app/compose/account/AccountScreen.kt | 0 .../readrops/app/compose/feeds/FeedsScreen.kt | 0 .../readrops/app/compose/more/MoreScreen.kt | 0 .../app/compose/timelime/TimelineItem.kt | 6 +- .../app/compose/timelime/TimelineScreen.kt | 2 - .../drawable-v24/ic_launcher_foreground.xml | 30 ++++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++++ .../main/res/drawable/ic_rss_feed_grey.xml | 6 + .../src/main/res/drawable/ic_timeline.xml | 5 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../res/mipmap-anydpi-v33/ic_launcher.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../src/main/res/values-night/themes.xml | 16 ++ appcompose/src/main/res/values/colors.xml | 10 ++ appcompose/src/main/res/values/strings.xml | 3 + appcompose/src/main/res/values/themes.xml | 16 ++ .../readrops/app/compose/ExampleUnitTest.kt | 17 ++ settings.gradle | 1 + 37 files changed, 445 insertions(+), 19 deletions(-) create mode 100644 appcompose/.gitignore create mode 100644 appcompose/build.gradle create mode 100644 appcompose/proguard-rules.pro create mode 100644 appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt create mode 100644 appcompose/src/main/AndroidManifest.xml rename {app/src/main/java/com/readrops/app => appcompose/src/main/java/com/readrops/app/compose}/BaseActivity.kt (98%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/Color.kt (100%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/Theme.kt (100%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/account/AccountScreen.kt (100%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt (100%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/more/MoreScreen.kt (100%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt (93%) rename {app => appcompose}/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt (99%) create mode 100644 appcompose/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 appcompose/src/main/res/drawable/ic_launcher_background.xml create mode 100644 appcompose/src/main/res/drawable/ic_rss_feed_grey.xml create mode 100644 appcompose/src/main/res/drawable/ic_timeline.xml create mode 100644 appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 appcompose/src/main/res/mipmap-anydpi-v33/ic_launcher.xml create mode 100644 appcompose/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 appcompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 appcompose/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 appcompose/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 appcompose/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 appcompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 appcompose/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 appcompose/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 appcompose/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 appcompose/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 appcompose/src/main/res/values-night/themes.xml create mode 100644 appcompose/src/main/res/values/colors.xml create mode 100644 appcompose/src/main/res/values/strings.xml create mode 100644 appcompose/src/main/res/values/themes.xml create mode 100644 appcompose/src/test/java/com/readrops/app/compose/ExampleUnitTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 420a509f..fc9bca41 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -80,17 +80,6 @@ - - - - - - - - \ No newline at end of file diff --git a/appcompose/.gitignore b/appcompose/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/appcompose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/appcompose/build.gradle b/appcompose/build.gradle new file mode 100644 index 00000000..018dbc78 --- /dev/null +++ b/appcompose/build.gradle @@ -0,0 +1,80 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.readrops.app.compose' + compileSdk 33 + + defaultConfig { + applicationId "com.readrops.app.compose" + minSdk 21 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + debug { + minifyEnabled false + shrinkResources false + + testCoverageEnabled true + applicationIdSuffix ".debug" + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + buildFeatures { + buildConfig true + compose true + } + + composeOptions { + kotlinCompilerExtensionVersion = "1.4.0" + } +} + +dependencies { + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + def composeBom = platform('androidx.compose:compose-bom:2022.12.00') + implementation composeBom + androidTestImplementation composeBom + + implementation 'androidx.activity:activity-compose:1.5.1' + implementation 'androidx.compose.material3:material3' + + def voyager = "1.0.0-rc03" + implementation "cafe.adriel.voyager:voyager-navigator:$voyager" + implementation "cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyager" + implementation "cafe.adriel.voyager:voyager-tab-navigator:$voyager" + implementation "cafe.adriel.voyager:voyager-androidx:$voyager" + //implementation "cafe.adriel.voyager:voyager-koin:$voyager" + + debugImplementation "androidx.compose.ui:ui-tooling:1.3.3" + implementation "androidx.compose.ui:ui-tooling-preview:1.3.3" +} \ No newline at end of file diff --git a/appcompose/proguard-rules.pro b/appcompose/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/appcompose/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..831e9c8c --- /dev/null +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.readrops.app.compose + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.readrops.app.compose", appContext.packageName) + } +} \ No newline at end of file diff --git a/appcompose/src/main/AndroidManifest.xml b/appcompose/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1cbd94ad --- /dev/null +++ b/appcompose/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/readrops/app/BaseActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/BaseActivity.kt similarity index 98% rename from app/src/main/java/com/readrops/app/BaseActivity.kt rename to appcompose/src/main/java/com/readrops/app/compose/BaseActivity.kt index d5bc080e..5b687973 100644 --- a/app/src/main/java/com/readrops/app/BaseActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/BaseActivity.kt @@ -1,4 +1,4 @@ -package com.readrops.app +package com.readrops.app.compose import android.os.Bundle import androidx.activity.ComponentActivity @@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator -import com.readrops.app.compose.ReadropsTheme import com.readrops.app.compose.account.AccountScreen import com.readrops.app.compose.feeds.FeedsScreen import com.readrops.app.compose.more.MoreScreen diff --git a/app/src/main/java/com/readrops/app/compose/Color.kt b/appcompose/src/main/java/com/readrops/app/compose/Color.kt similarity index 100% rename from app/src/main/java/com/readrops/app/compose/Color.kt rename to appcompose/src/main/java/com/readrops/app/compose/Color.kt diff --git a/app/src/main/java/com/readrops/app/compose/Theme.kt b/appcompose/src/main/java/com/readrops/app/compose/Theme.kt similarity index 100% rename from app/src/main/java/com/readrops/app/compose/Theme.kt rename to appcompose/src/main/java/com/readrops/app/compose/Theme.kt diff --git a/app/src/main/java/com/readrops/app/compose/account/AccountScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreen.kt similarity index 100% rename from app/src/main/java/com/readrops/app/compose/account/AccountScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/account/AccountScreen.kt diff --git a/app/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt similarity index 100% rename from app/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt diff --git a/app/src/main/java/com/readrops/app/compose/more/MoreScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/more/MoreScreen.kt similarity index 100% rename from app/src/main/java/com/readrops/app/compose/more/MoreScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/more/MoreScreen.kt diff --git a/app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt similarity index 93% rename from app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt rename to appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index 9a88202e..55fe0f2e 100644 --- a/app/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -3,13 +3,11 @@ package com.readrops.app.compose.timelime import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Card -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -33,11 +31,11 @@ fun TimelineItem() { Row( verticalAlignment = Alignment.CenterVertically, ) { - Icon( + /* Icon( painter = painterResource(id = com.readrops.app.R.drawable.ic_rss_feed_grey), contentDescription = null, // modifier = Modifier.size((MaterialTheme.typography.subtitle2.fontSize.value * 1.5).dp) - ) + )*/ // Spacer(Modifier.padding(4.dp)) diff --git a/app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt similarity index 99% rename from app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt index e17b14f3..f7d9e9c1 100644 --- a/app/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt @@ -13,6 +13,4 @@ class TimelineScreen : AndroidScreen() { TimelineItem() } } - - } \ No newline at end of file diff --git a/appcompose/src/main/res/drawable-v24/ic_launcher_foreground.xml b/appcompose/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/appcompose/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/appcompose/src/main/res/drawable/ic_launcher_background.xml b/appcompose/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appcompose/src/main/res/drawable/ic_rss_feed_grey.xml b/appcompose/src/main/res/drawable/ic_rss_feed_grey.xml new file mode 100644 index 00000000..fedffd69 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_rss_feed_grey.xml @@ -0,0 +1,6 @@ + + + + diff --git a/appcompose/src/main/res/drawable/ic_timeline.xml b/appcompose/src/main/res/drawable/ic_timeline.xml new file mode 100644 index 00000000..68d26c91 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_timeline.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/appcompose/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/appcompose/src/main/res/mipmap-anydpi-v33/ic_launcher.xml b/appcompose/src/main/res/mipmap-anydpi-v33/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/appcompose/src/main/res/mipmap-anydpi-v33/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/appcompose/src/main/res/mipmap-hdpi/ic_launcher.webp b/appcompose/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/appcompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/appcompose/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 zcmV-Y3$650Nk&FW3jhFDMM6+kP&il$0000G0000-002h-06|PpNWB9900E$G+qN-D z+81ABX7q?;bwx%xBg?kcwr$(C-Tex-ZCkHUw(Y9#+`E5-zuONG5fgw~E2WDng@Bc@ z24xy+R1n%~6xI#u9vJ8zREI)sb<&Il(016}Z~V1n^PU3-_H17A*Bf^o)&{_uBv}Py zulRfeE8g(g6HFhk_?o_;0@tz?1I+l+Y#Q*;RVC?(ud`_cU-~n|AX-b`JHrOIqn(-t&rOg-o`#C zh0LPxmbOAEb;zHTu!R3LDh1QO zZTf-|lJNUxi-PpcbRjw3n~n-pG;$+dIF6eqM5+L();B2O2tQ~|p{PlpNcvDbd1l%c zLtXn%lu(3!aNK!V#+HNn_D3lp z2%l+hK-nsj|Bi9;V*WIcQRTt5j90A<=am+cc`J zTYIN|PsYAhJ|=&h*4wI4ebv-C=Be#u>}%m;a{IGmJDU`0snWS&$9zdrT(z8#{OZ_Y zxwJx!ZClUi%YJjD6Xz@OP8{ieyJB=tn?>zaI-4JN;rr`JQbb%y5h2O-?_V@7pG_+y z(lqAsqYr!NyVb0C^|uclHaeecG)Sz;WV?rtoqOdAAN{j%?Uo%owya(F&qps@Id|Of zo@~Y-(YmfB+chv^%*3g4k3R0WqvuYUIA+8^SGJ{2Bl$X&X&v02>+0$4?di(34{pt* zG=f#yMs@Y|b&=HyH3k4yP&goF2LJ#tBLJNNDo6lG06r}ghC-pC4Q*=x3;|+W04zte zAl>l4kzUBQFYF(E`KJy?ZXd1tnfbH+Z~SMmA21KokJNs#eqcXWKUIC>{TuoKe^vhF z);H)o`t9j~`$h1D`#bxe@E`oE`cM9w(@)5Bp8BNukIwM>wZHfd0S;5bcXA*5KT3bj zc&_~`&{z7u{Et!Z_k78H75gXf4g8<_ul!H$eVspPeU3j&&Au=2R*Zp#M9$9s;fqwgzfiX=E_?BwVcfx3tG9Q-+<5fw z%Hs64z)@Q*%s3_Xd5>S4dg$s>@rN^ixeVj*tqu3ZV)biDcFf&l?lGwsa zWj3rvK}?43c{IruV2L`hUU0t^MemAn3U~x3$4mFDxj=Byowu^Q+#wKRPrWywLjIAp z9*n}eQ9-gZmnd9Y0WHtwi2sn6n~?i#n9VN1B*074_VbZZ=WrpkMYr{RsI ztM_8X1)J*DZejxkjOTRJ&a*lrvMKBQURNP#K)a5wIitfu(CFYV4FT?LUB$jVwJSZz zNBFTWg->Yk0j&h3e*a5>B=-xM7dE`IuOQna!u$OoxLlE;WdrNlN)1 z7**de7-hZ!(%_ZllHBLg`Ir#|t>2$*xVOZ-ADZKTN?{(NUeLU9GbuG-+Axf*AZ-P1 z0ZZ*fx+ck4{XtFsbcc%GRStht@q!m*ImssGwuK+P@%gEK!f5dHymg<9nSCXsB6 zQ*{<`%^bxB($Z@5286^-A(tR;r+p7B%^%$N5h%lb*Vlz-?DL9x;!j<5>~kmXP$E}m zQV|7uv4SwFs0jUervsxVUm>&9Y3DBIzc1XW|CUZrUdb<&{@D5yuLe%Xniw^x&{A2s z0q1+owDSfc3Gs?ht;3jw49c#mmrViUfX-yvc_B*wY|Lo7; zGh!t2R#BHx{1wFXReX*~`NS-LpSX z#TV*miO^~B9PF%O0huw!1Zv>^d0G3$^8dsC6VI!$oKDKiXdJt{mGkyA`+Gwd4D-^1qtNTUK)`N*=NTG-6}=5k6suNfdLt*dt8D| z%H#$k)z#ZRcf|zDWB|pn<3+7Nz>?WW9WdkO5(a^m+D4WRJ9{wc>Y}IN)2Kbgn;_O? zGqdr&9~|$Y0tP=N(k7^Eu;iO*w+f%W`20BNo)=Xa@M_)+o$4LXJyiw{F?a633SC{B zl~9FH%?^Rm*LVz`lkULs)%idDX^O)SxQol(3jDRyBVR!7d`;ar+D7do)jQ}m`g$TevUD5@?*P8)voa?kEe@_hl{_h8j&5eB-5FrYW&*FHVt$ z$kRF9Nstj%KRzpjdd_9wO=4zO8ritN*NPk_9avYrsF(!4))tm{Ga#OY z(r{0buexOzu7+rw8E08Gxd`LTOID{*AC1m*6Nw@osfB%0oBF5sf<~wH1kL;sd zo)k6^VyRFU`)dt*iX^9&QtWbo6yE8XXH?`ztvpiOLgI3R+=MOBQ9=rMVgi<*CU%+d1PQQ0a1U=&b0vkF207%xU0ssI2 literal 0 HcmV?d00001 diff --git a/appcompose/src/main/res/mipmap-mdpi/ic_launcher.webp b/appcompose/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/appcompose/src/main/res/mipmap-xhdpi/ic_launcher.webp b/appcompose/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/appcompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/appcompose/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 zcmV-U53%r4Nk&FS4*&pHMM6+kP&il$0000G0001A003VA06|PpNSy@$00HoY|G(*G z+qV7x14$dSO^Re!iqt-AAIE9iwr$(CZQJL$blA4B`>;C3fBY6Q8_YSjb2%a=fc}4E zrSzssacq<^nmW|Rs93PJni30R<8w<(bK_$LO4L?!_OxLl$}K$MUEllnMK|rg=f3;y z*?;3j|Nh>)p0JQ3A~rf(MibH2r+)3cyV1qF&;8m{w-S*y+0mM){KTK^M5}ksc`qX3 zy>rf^b>~l>SSHds8(I@hz3&PD@LmEs4&prkT=BjsBCXTMhN$_)+kvnl0bLKW5rEsj z*d#KXGDB4P&>etx0X+`R19yC=LS)j!mgs5M0L~+o-T~Jl!p!AJxnGAhV%~rhYUL4hlWhgES3Kb5oA&X z{}?3OBSS-{!v$nCIGj->(-TAG)8LR{htr41^gxsT8yqt2@DEG6Yl`Uma3Nd4;YUoW zTbkYl3CMU5ypMF3EIkYmWL|*BknM`0+Kq6CpvO(y$#j94e+q{vI{Zp8cV_6RK!`&C zob$*5Q|$IZ09dW=L!V zw@#2wviu|<#3lgGE8GEhcx+zBt`} zOwP8j9X%^f7i_bth4PiJ$LYtFJSCN$3xwDN;8mr*B;CJwBP2G0TMq0uNt7S^DO_wE zepk!Wrn#Z#03j{`c*Rf~y3o7?J}w?tEELRUR2cgxB*Y{LzA#pxHgf}q?u5idu>077 zd^=p)`nA}6e`|@`p?u}YU66PP_MA}Zqqe!c{nK&z%Jwq1N4e_q<#4g^xaz=ao;u|6 zwpRcW2Lax=ZGbx=Q*HhlJ`Ns#Y*r0*%!T?P*TTiX;rb)$CGLz=rSUum$)3Qyv{BL2 zO*=OI2|%(Yz~`pNEOnLp>+?T@glq-DujlIp?hdJeZ7ctP4_OKx|5@EOps3rr(pWzg zK4d3&oN-X2qN(d_MkfwB4I)_)!I_6nj2iA9u^pQ{;GckGLxBGrJUM2Wdda!k)Y>lq zmjws>dVQ*vW9lvEMkiN3wE-__6OWD0txS&Qn0n22cyj4Q*8(nG4!G{6OOwNvsrPIL zCl-$W9UwkEUVuLwyD%|inbOF*xMODZ4VMEVAq_zUxZ+K#Gdqf!DW$5f)?7UNOFMz! zrB~tuu=6X2FE(p^iqgxr+?ZK;=yz`e;C$#_@D9Lj-+TDVOrva>(#*PVbaHO>A)mhl z07OJWCqYC60518$!&c`eNBcBW%GnfaQ*$eazV^2_AW?j)h;J1nUjN(I9=0+!RVx~% z3@Tf!P0TE+98jA?WceK-}A1% zW!K)lyKcGqy#M~})315-A#2NXQ`?6NR#Apo=S!oF=JfpX>iR*49ec{7AN$xxpK{D$ z2d%Fz&rdfSqourN$~Y^NFIMV1CZ?J*bMx~H3k&meGtH@q9ra2vZxmA$S(#jaaj-g4 ztJmxG+DLV<*q<|sDXPp$X>E)#S}Vm&sRaO5P&goh2><}FEdZSXDqsL$06sAkh(e+v zAsBhKSRexgwg6tIy~GFJzaTxXD(}|+0eOwFDA%rn`X;MVwDHT9=4=g%OaJ9s%3b9>9EUTnnp0t;2Zpa{*>mk~hZqItE_!dQ zOtC>8`$l|mV43Jbudf0N6&&X;{=z}Zi}d1`2qmJ}i|0*GsulD3>GgQXHN)pkR6sf1 z?5ZU%&xtL}oH;YiAA)d*^Ndw2T$+Mjuzyzz@-SM`9df7LqTxLuIwC~S0092~+=qYv z@*ja;?Wt!T!{U?c*Z0YtGe)XbI&y-?B&G2$`JDM)(dIV9G`Sc#6?sI60de6kv+)Qb zUW~2|WjvJq3TA8`0+sWA3zRhY9a~ow)O~&StBkG2{*{TGiY~S8ep{V&Vo2l<6LWsu z^#p0-v*t2?3&aA1)ozu|%efSR=XnpX$lvTeRdKlvM!@|pM5p2w3u-6 zU>}t2xiYLS+{|%C65AzX+23Mtlq?BS&YdYcYsVjoiE&rT>;Necn6l^K)T^lmE`5u{ zm1i+-a-gc;Z&v-{;8r)z6NYfBUv+=_L}ef}qa9FX01)+Aaf+;xj(mL6|JUzGJR1|fnanb%?BPPIp>SCjP|8qE5qJ{=n5ZGw?81z3(k;pzH%1CtlX50{E7h)$h{qGKfzC`e2o`*IqA#tjA z`Fz&^%$b9F*N`)U-#6>a)Z`55`$Dd0cfcs0$d13^ONrdCu9xcv_=n#WQo8stcz3jP9|2EvdI-RhJM3%Q%oM&!OlShM|0 z?gz?wHZSnm45njLtsz8PVT1S&jAlbKg5kVam$p16=EK@Sj4EP0OtH zmJDmdc^v)x>56Qg_wmYHz6h)>kl_h$>0@J!ypv%APmjZTAQVLy6Fu50RGY&JAVNhx zrF_qG6`x9MkT;1SFWo$)l{M$;3qUDn9JwE}z zRl#E_bDRJFii61kPgBybIgp8dNW!Cc1b*^YYk-#oWLJvtM_v^hQx~9?8LD4VFFxBF z3MlrsSC%f9Oupn*ctPL0U1fwfX?`tRhPD{PSLFPQOmIt$mDy0SgpNVvHS+f#Do>h1Gn?LZU9(KaN>Q_=Y*_T zvtD7%_u^^+{g`0VGzg(VZrpVQ6Ub5M=tI_p7T93R8@3Zulu3|#{iNcu!oiHxZ4Rf*( zfmiN$$ru(*_Zqn=`Gq#OuHRTSwp7uH_SokR&|)RuW5yo=Z|_4?qU-JU+tpt>!B&Is z@N(=SG;bpVc;AO@zbmMM zScqq1)b-ZQIrs={oD}|?6y{$HNB1U0^LsBh8JI&3!GBZxOXI<}&5-$lgkAaYqhOTb z?2vEnZ$-kk;*M_17(upJF3%+iH*s0-r{vttXVB2OUwI1s^+G(Ft(U8gYFXC}#P&E^ z>T@C^tS`Z7{6HT4_nF~n>JlZtk5&qDBl6r|^kzQYe`wq!C)n@$c>WOPA61NDFj<<6 zGW71NMMhwAl!U-yqrq2xrSFqRCI8acw7?}3j;ynxo*-b7Co;g5r%^j=H@9({PXXBf z@r>U>>N;E)81wx`B4f%{PB~MHka_);%kBCb(d|Jy5!MqJ%2p`t&@L)4$T2j&-WHvG zv3(uyA_gwqNu(k?jQTtv3dgPKRZoH8prxe7>pQBW5L&dpumS&5Ld2?(sCpJjvc4L5 zEnh&?91WVm)ZdTj=fjJ$pPDdgAttLXuke+?KdKxu*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{YCP}*Q=lvp4$ZXrTZQHhO+w%wJn3c8j%+5C3UAFD&%8dBl_qi9D5g8fry}6Ev z2_Q~)5^N$!IU`BPh1O|=BxQ#*C5*}`lluC515$lxc-vNC)IgW=K|=z7o%cWFpndn= zX}f{`!VK02_kU+Q5a3m37J;c} zTzbxteE{GNf?yLt5X=Bzc-mio^Up0nunMCgp*ZJ;%MJvPM3QK)BryP(_v@ei4UvHr z6+sbCifQaOkL6-;5fL8$W($zZ_;CZp305C;~$hhRquZr-r)jjd1z z31%ZK{-(`P#|Um_Sivn@p$-vz46uqT>QG0B1w9znfS9A8PB2LaHdzA|_)yjXVR*l{ zkcu3@vEf7bxH0nkh`q?8FmoO_Ucui*>_a~P?qQrlZ9@+D7%MTpSnztpylXrt5!-k8_QPB?YL8Kx_On8WD zgT+111d(Op$^$&KLAN5+@?>f7F4~wFi(8TL8+szgVmcMDTp5l&k6~=rA{Dt}!gb^r zSWY<)M7D|Z2P0cEodj6E42PV>&>DFmQpgt)E-|#sSUU@uKed+F680H@<;-x{p|nuH4!_mn85rx>wz;0mPi2ZkL#k6;sznu?cXh!T0S>{w6 zL^gvR05NY64l*<+_L>On$rjx9!US;l;LX6@z}yi#2XHh)F@Oo+l)h%fq$v}DNmF2> zfs^_t0)3N-W<9-N?uedVv{)-J0W5mh#29QM5R5h&KuiRM=0Zvnf#lF=K#WlCgc#9c zS;qvh(P$!_a8JwyhI^ZJV2k+B6Z^64?w|1?5gyo6y{}923CRZfYVe1#?F% z7h2SUiNO3;T#JUOyovSs@@C1GtwipycA=*x5{BpIZ_#GCMuV8XK=x;qCNy{d7?wA~ zC+=vjls;ci&zW=6$H~4^K%v{p}Ab?U%C6Z4p%eC<3ExqU$XR<}LLF67A$Sr20DR_pJ3yeBa~ z^sw{V0FI5;UpwXsScYuhbqGQ`YQ25;6p6W^+tgL&;Ml;>S3CGpSZ>VrTn0m1$y$HU z&65)I!c?oREz};c=nLCliriqQX->4uivHTgd${GqeAlf*!P^B|jkU|*IdNP(&6C>4 zqOW$)Nw9nvjy^&`?E|gotDV{JmJ9Q~vuhy<`^C4XIUDt|j4o6rK^e8_(=YqC zuaR6TRVf@tUFHB079o4MBIh{M~4>WwnGgesQH*3?w(RA%hCZ*7)b!aNV=yOQ%o_Y=Lt0Sl*(9^jfRnC210Om$=y>*o|3z} zAR&vAdrB#mWoaB0fJSw9xw|Am$fzK>rx-~R#7IFSAwdu_EI|SRfB*yl0w8oX09H^q zAjl2?0I)v*odGJ40FVGaF&2qJq9Gv`>V>2r0|c`GX8h>CX8eHcOy>S0@<;M3<_6UM z7yCEpug5NZL!H_0>Hg_HasQGxR`rY&Z{geOy?N92Z z{lER^um|$*?*G63*njwc(R?NT)Bei*3jVzR>FWUDb^gKhtL4A=kE_1p-%Fo2`!8M} z(0AjuCiS;G{?*^1tB-uY%=)SRx&D)pK4u@>f6@KPe3}2j_har$>HqzH;UCR^ssFD0 z7h+VLO4o@_Yt>>AeaZKUxqyvxWCAjKB>qjQ30UA)#w z&=RmdwlT`7a8J8Yae=7*c8XL|{@%wA8uvCqfsNX^?UZsS>wX}QD{K}ad4y~iO*p%4 z_cS{u7Ek%?WV6em2(U9#d8(&JDirb^u~7wK4+xP$iiI6IlD|a&S)6o=kG;59N|>K1 zn(0mUqbG3YIY7dQd+*4~)`!S9m7H6HP6YcKHhBc#b%1L}VIisp%;TckEkcu0>lo@u995$<*Em;XNodjTiCdC%R+TX|_ZR#|1`RR|`^@Teh zl#w@8fI1FTx2Dy+{blUT{`^kY*V-AZUd?ZZqCS4gW(kY5?retkLbF=>p=59Nl|=sf zo1Pc|{{N4>5nt#627ylGF`3n>X%`w%bw-Y~zWM_{Si$dc82|=YhISal{N7OY?O`C4 zD|qb}6nLWJ`hUyL+E>-;ricg9J@ZNYP(x(Sct&OI$Y!QWr*=^VN;G3#i>^1n4e#Je zOVhbFbLpXVu*16enDM+ic;97@R~u&kh__kgP#!R`*rQEnA+_dLkNP~L`0alC|J;c; zeiK=s8;BsLE)KbG3BD&Br@(Ha@SBT&$?xX`=$;eeel=|R_dIr6-Ro?=HEjnsJ_b`1 zK6Yg^-6;^2aW!xeTK)A~3Rm|L^FCHB_I>jIju7ZGo&N_1*QHkxH2!!%@o4iZ?vntS;&zJdPe1dH#04YD93A44o-MpfD zP{rn_aq>U%RDvC2+bp;xPlsOzauIi3*Lf42`jVKKZCRuKdYhi>FDuL2l=v{$BCN#Q6796s%r-AG$Q^t(3c@ zD?w0UhYr11@feiyl9kY_@H8~|xlmO<8PfQmj1!$@WieW@VxR@Psxfe-v9WCi1+f>F4VL?0O~K7T?m4-u|pSkBpUJZZe*16_wAp zSYZ@;k`3;W3UHKUWc8QeI}0jH5Ly=cGWQPw(Kr2fm=-5L(d`lcXofy8tJY3@Tuadz zYWXR{mW7XT!RF#RVCe%}=tM*O6!AD3^(!8un~opNI%Uko7$5t@<8+?; zTxDys(MyyGsUjtSu9$+|_-t!U3fVb1dkK?l`17<+jfl=hrBHnDSV>^R1=TnQeyqbW z>ov#l%!1|S!1>8UUxIdhQq`_klcHVx0{?#>K3#$4GlXncwldt!g17TcvKq-jo_996 z>oA=tH9CqRl6Yw?Uc`am!V?lHJbizOJaVaScf1UP5e7Dbgabq=b!B~T&_F6?ooU>w%x0A zH~&MHJ=q`fCH{U<7MDXE4SD32cDZA)WJeWkllJ`UspWaS#eDe^kg^oU_A14UE9zG-a^g{xaXf$})Wik>gT zl#dkzGr(;h0JZDuFn(+k8wNq?PZ5grQ<+sM?wBGt@JnH6v0#or-5wBQWKU~(S_> zkE!tc*ZJ1Y&*p(xX84POb3cClRMd!^qJ#CAZfIepEj-<`VURS_yCz0(?*Ixcj4 z-!zV1_QZhpm=0<;*(nm+F>T=)o?ep@CK5I%g^VAA+RB25ab?7)A~z~egru=I1S|@v zH7tXV!0wmGS^qj#e+MY;C5eUjEAp$Y?LDkS^QPZ}8WN85?r$u<-Epi;yZ1|J2J`se z$D6DpH~2F=eI0B&=UFAUnJvZAmClJlK)sutJ?M>xpZiWV&0=G4MZP+x+p>EX=HbCz zxls%Mw?*u^;LbHWIWCyq+yi)`GmFn9J112CZda_u@YIP%i;srFg_paU02Ifij*7}l z&CF-(3|>*a|+vbNR`^RP=9G?ymEJ0Z~)d&c*UE$UMepZ zcITr{0WqhxkjUnM15js_gW=e3Uh|y6ZReaXHIz-=p`x5VvB&rH9y>Amv@^WmXFEw) zQXYrk3feir=a{jMQ+wDIkkFnZ$k{sJakHn*?u za%4b!00ev8NVLM1TY=cl?KB&55BY_MU-sg?c>=Dbz_W{(Z~c?HJi*XpYL)C6Bd8WH zt+v-#0&o~@t4qESi*)+eW%@VD0|o^yF)n0hME$UtXF$*Lvh}7sso{`|pn*JDIy5^Fm3s$5*zEE=?u5<=l8FJc3r%+H} zdfoNl2J0^~!-*mOL5o-x32|e0Im*E!yY7F7E5N)W3>+v_LBydlEx?4$RL5f2oYRD# zaR0wv(-p~wO0eLDl3K=%`{5+0Gd$ktO=W)gWlGZJ0`K z$_RNA=ckrfa;H0KA~dR^p�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} zs4%E?srLW`CJh0GCIK@hTkrW7A15Iu%N&?Q^$0+!{Tv&|t^Y@u%!L zglTg&?Q5q#ijZ;&HBQ?FNPp;k3J5!&{^+SGq?AX~SiOM9jJMRpyP?RCr@z38AQyy&WRMaC;n4una$~nJKSp?q|s8F00c9?Q! zY_ovvjTFm+DeQM^LXJ#v0}6HRt3R1%5PT*}W!k8BEM;Jrj8dIceFo2fhzTqaB3KKk zGlCLI)gU25(#u6ch6GeB1k@eHq7l{EHXv0n6xE#ws#ri}08kkCf8hUt{|Ejb`2YW* zvg}0nSSX1m=76s?sZhRY$K=3dpJ+y*eDULGnL2}4>4nvW^7_<~wIM_5fjvwt4h1|g z)g0Z6ZFq9j<~9~b8((~TN{Z?ZQfw|is&Xp~AC61sj;xItKyCHdI|tCMC_LbXF>~vR z=w6V3^H=W4CbAgR4#xw}ETTwu2guW~=Crl@SMXv85jQ=%y!s^?m4PI0My7MWICO;- z175jm%&PcPWh8QdOU(#8bp4!N7ET-+)N}N2zk2)8ch|4Q&lPFNQgT-thu053`r*h3 z_8dI@G;`zn;lH$zX3RzIk`E8~`J=BBdR}qD%n@vVG1834)!pS1Y?zVkJGtsa(sB~y zNfMYKsOJb%5J(0ivK8d+l2D2y&5X!cg3BG!AJ}910|_${nF}sC1QF^nLIhzXk-Y#x z0)&1iK!O;Og0Ky!;`b~v%b$`S4E&fB)1NB4v@8wr( z&+NX4e^&o)ecb=)dd~C!{(1e6t?&9j{l8%U*k4)?`(L3;Qjw z#w7FS+U(94MaJKS!J9O8^$)36_J8;thW#2$y9i{bB{?M{QS_inZIJ!jwqAbfXYVd$ zQ5fC$6Nc9hFi8m^;oI-%C#BS|c8vy+@{jx6hFcf^_;2VRgkoN(0h!_VSGmgNPRsxI z8$rTo0LaYq-H5i&gtj81=&xU?H-Y2==G@uQV7E`@+2E9XQW@{&j`?EOktk|Ho{HU>ZqDzvgjwBmdex z&uZNd2C1h{{}2k6Ys9$*nFP3;K%u!MhW`uZy7Sn`1M1zs@Es&;z*Z>Gsh@-3Fe6pE zQD2@cqF((NrRevgvLsvM_8;;iNyJ5nyPyy?e!kvKjGj`6diRFBEe49Oa7wwkJFV7Z z$YT&DWloYu-H?3<0BKn9L&JYDT-SK~*6c5pi18P26$JESKRYj{T7Zk6KiRJcbvOO*{P56Q6s8msbeI3>|j>K9}Q9UBeq*inXKemCm`-<5|-$ZyN4u$(3 z&HcvqehFD%5Yrmykg-^d`=BSa8(i=>ZoC77^mWY{evp(km@aHqhUECBz76YiR+VYK zY_avFC~V3$=`6C4JhfHAQ@DZtUOwH`L;oYX6zK0-uI^?hS$ALfq}A7evR;ohJHij} zHSZdW?EKv9U1s4oD*<(0oQ*;MaQ6@cvGL zuHCPgm_NhVsgp^sfr*ia^Db}swo1?O(_Q2)y+S$CBm+g=9wCOUPbz(x)_GbaKa@A7 zuI&!ynLiZRT#V%_y_-D`0Z5lT*auoe{(U5NylTzFSJW()W-#F6*&A`LNO1bV#Y;QJ zSbLBnp|B^dtK|KIWC|No>JjWBWE@n7O)x{&^E(WMeMvp57#qA8m* zeTow*U@_86B#Fm*rxyYu5PRWaWHx8y> z*qmHEp(AMDl0v)ij(AY8fnH=~ZwwjVAbu*m5;xPfidh@ov6d8g zfJsi&!QyK53Es%sC39ts;54V68koALD4b|%tNHW0bIkZAJKa=W&FomJSEDT>W1xIX z1x%Z>AvNIsSPLcn3RTcHXb@KB?cuM)=x6fcIx>&(GxqZ8w3p#jJ(GVgc*`c0HG}dv zIop&Qim!K1NFwic%07KcjWgHBPUkq7f~lj;TPqVGTiT#cUeim>;nY`>h@a*S{qQex zQ`z62WK|Mj)Y{tfF{;T4P;c8$Q|KU?Joh zIkA^z%X7z|r>4aTh@|StTi!-r1D!g=zb#3d#{{&K3CqE$Iz-UH<%37c zRfkO`&uM%#AD3PHv`g5t0e^O%nVL0d{Xlx^EjEC3#skF@`zl-7PF^0oxW)1!C!JxR zWvuAHH?)61FKA1QeT*_sY7;_Id#!GmV4n`MO{~sv}VLSK` zXRw=Y=Clz*00B(5y^K;gCZMAzjT5+c3IC=)l(9VIDdatpxj3y89WwI|bH&$!ZEvp` zPR!T@#!(|KfI-w?!&+7$N3F6>tD{YO4Qg$d_`nNEdfVCha9vaPn0jI0`)`@*72hq! zpU5ND^P*RoEkbD5o#az(-g=Y)L>HH>Oc%}$ zT3Rs_ih0;4+Lv4Y;@Iv(;fUbQ=i-G(#>vghec~*j(I#r|5mqFiJBpzi&hzEcD{u$< zRsm0BVYn=pT;0>R(itW|*D&;O%bOc7et9ACaH#J>z3A1A~6fdP>pmbM%xzm4>|;c_?B+%sl;Qs2{t!60$^u zH1t@9^6>;?!FuusnISi$f5CL&;z?EqJN$FBuWDA#D5`cy_UvCFIVvf{c?4N0teh;d zET$7aVbj08KTQS!x?Nd1Is8q8qFzs}a=!@nJ;7FSfCY^T@D-gpw`w<6e#X3+;O}1h z$%I!M)0bg|EKUA04Qjn@+x{Rj8vt6Wn!R|3A92z}^$KfF5(#CWr4y#~re1CN4i4w0 z#GsypBR{xA3Er7sgAi(|}1-W?s~n$7?K|9WL8kpVfw-;#b9 z+mn;=ep!162U5R>_t}fOt~tE?s#m( zO-S$7>Ay6*hHdZ)7_oU915WYYCIX;hFI-U2EWYX!pllONr@Q--2o~`!isi6vTPLJ4@(|o=%NHYjo0_S&q*UQIROw@*N-By@PaQ&;YxFZ0aR zX&}LeOEz);#m~Hwm^VAY8DK}b$F4bo{jMN?d!lxKPhNklzr^Cd`0f4oJr^z=I|l`* zm8AHm*fPV`0=lF3Pnnp}&J0N1X@}-D94YvmUabFrLGSnTz7Mu^21F#O5tN#CuY9Vh zUZBH=ez%h*wkf0hBtXJh1SN3d+IF{gzT7lp)j}n?03lt;XSQRAh7qd&v;RwTYDuQ# zbI2*r<>?x-G0@hM{;%{VBD7nLKt~D`T~-HAt5;h%i0_=Ifs=yHma5dhJ+QMG?Ux(a z|E?1CMy1!~oA`FP!k~iG=t&5#>bVdz=peT8HMB6Y)#7PpETtNryT^+Rv3vpJaF^zP z{H}0-LyV9Fu21ID%wO9f1IKlFr1p4c{o-?03vyB-tr5duk^&L$;m_|f$vs`^Sl{j2 z95}oY{LlY+=ZS%J+tZoXCd0*sSU7w^gjovXn+g7uyra5{cU49@yHf#Z^Jl-$9cIfo z+AJuxH$VLb=#+uBbVmUjnx zxb1pZ@-O9=AIk4@S)m6fJ2?{HrNYwwnL3a45muuNjr;6$O`bGEM0T4A2_S$t=86*- zcO+0mywg*j#A4mU}enR_!cGmIYQ;qwfchWtFEXL)AK%*;=j znYne+hS4EMy3S)C*mZ1KI>!+)0V@9!N6H$Y}~MJ{rYuf zz^KljIWvFi-?#?V@LPR&c6Nn{!=XM z>}-h$S76;$H{E{Y%@^zlmOl^efBwa%UU+jJD9UVukQ3ti_kH-?H*RC0?M1W%FCvMB zM_+v6fk$6X2sx)-p~B3&Kl{nscK}pNLM*qjtpaf9>AU{-iPKQZR8yCg!TY}Qg*(;) z)gdvCcB%kppZc$VdvsK@)3l1{&DG!d_6OHOS`y=ITLEVu`unSKA2E%JD*DVX{LJ}K z9l>hMRDqxQh0lnpGHpVYneX}eA3Pt|2v%=q;rt)``R|#bDyB)OXY&vI_@|*}h}G?^ z@aZ4_!7cQPX`!fW_?{oT1NTwHs#l5L-0`E|y@48<3Q^HFf8=Idi zpJYD%1MkII!~|7I^WGo)IF=?{>ACnjJ_WUi39C}!Q{QnheVJqeKKqq5^o5CBde(g9 zvw$X6^jz_^E2$wSw4!q5*RG(C2_^XO$HBn_55vbl44OnTTRwRaePP0vo{K)U1#99& z<>rq7V&V(<&@I%MFoN5zrY}sz=(*-L&}1QQ*a%`u25h{cFj===17eB_uGuzG&byQ< zrm8BJZl4r_E$3k|Wo6FW0-6M7>qac5uFQsQcmkLWGfeH74S3Z_rJ!jgN++!@i=HW8 zkyjI(oPH-+-N#Qc^-mpNO`bc6r=2-<%&Wy5K1vfFJB(L_IkpS6fY^NmuL8qsgj>MD zn~BHH9WM~32_3vd=W&B)k7F9q%stJx+b_L_X-4zr^LVUMCmyCTA3sWtkvsmME?Xiy z?xOSfB=_$oY06~J-HcCq&)qcW{j;uP;?Dm}=hkq?zh&n!;m((-G-u_t|6x399Q;>A zgNpxoJNj{u|MFDH7Rhq@FCAl0dE|ddnl!oh9{Lq?@JDoR6L;C941IK`ISfdE$4S zE0AUQ8+2|Ncl_q5QkSp#AODp~(^mfP&%Au@@|TBQwoP`UU+V{6u8|)6ZA{~uKmQ*M zmrMTDU8S~8Eqi{^v0Ug&5Upcm#y7Z1(RbgZAG8jB$eRwCspQ)>5;U)oGZ&E5aeR*K z8Yt`Y0$G))Yd(Y3KH}tA4`-_QmNke5hU_|nq=xtyjwW(_o?itz>B>WM&^63bNdQ)k@-IgDHW*RW$Xo9#RzrTrCn7L2H{9Amq|qNg@#eZY=|P zCoI?2s+L)zsM%WX(NbVEY^`C>lFjIBYmJ6@DKJ0ZT4&F&WHW!dwa%QzOG!?jY_2(S zDcEzZbz*2Q!43|z))9yOP9X1Xt%DXzwY(3tl-TR=Qb_MbZYRrooh;dYYmS!U_as1(=YVB?Q_A|tNu5Ut&_q3jbfDM zoFxT^uEuH`nX3*sB%K?GuHUkweYReBwnHqh3P)~`+s3+Tj!rDA1e)8vuBv5J*IsxC zkd^~b(aGzArj08{>cnzOuy04C+C`}gb|Yz-1avxeWzev3NzcHbz_&4W@QCr$z3~w=8Ua- z`;vfG1~BP8CyLb=F7t1am~ph_#|O%$khSJ9%Vtcn)YmpgQxF?xM^_Vb+5fnpB^W0I`f%X8gb9#X{Q-yJG0{Z56aWeI&zPxnf5pdJA38bM`cYnS#x)% z`n1tFf$i)W-hGm(f9mde^=X@NcV_lFb=P`4&CI&H=IArijGwdCk&X@uQ$5xmj!~^? z#$ROCI)V-~t%L%GS#wo@U27ddR`4`3)WoB{R-4snfNrfee|kI8^bu#yDgYqOwas9# zmcb`3!kRJ`Cr=_tq)8aMt{aGtUZsqwVlj6DgCGre>AEt&x8H_in!x@uwgExIh|-mA zjdaC(29~CTVSaaF7HPbql&*9Uo8P@f)>LqCXclr}peS7_1BQ28u9PO8Eq1@`l3q9o zkfKCaO2?T?ZyA6loW<#9_c^O=m<&h}CA!ineAD@=(gbq`vyT|tiJ6#^B1$P;;qax` z55k&Q?wEh#87niLo*+n4L@65J(Nz~=Ya%7^(miLb(E>A3B@|Jjl;FU&D>o|9#7PJH z?|ago!o;WC^h=|T7PVBg(DAB}72cyUS zb(f>Bwbr!F1eTCO5fpj<{PqhY5>143p?~5ZA5H40);=@M#MYvrB6gqHbU_!GSY??i z%s=>-ciA4*zOOZHds0a(kWewZ4h(k8h(ua7HX)Au&mY~H8KY6(_cb$_&fA@QjIW-*heP3%$d!m5^AdnT}`12qA^c@!g3DOwZ5WwE2?)-yU z!)Vx#Mtxt?FzFTwK!77sy7)sMzUd->w4^bxtpM2j!b1pjgyk zGKwWGeb4)^zjy{9Es&PU1}gwg?|J#L$KJB7ett9@4M%-nGtIQr0>Fl@8-yh`-+1ed zS6r}(MeSvgSoFmH*_WPu@i?}!AB~2?;i&IxrkNg~cQ9Som98tcq)k^|eeER|Zl77t za-TVUc;DNvzVXJ%w52+#weN?+;i#{f#!Oc&z?81*N>^e~ltRS%ZI@lR{rs()HmqG! zx*}ZrI-EZ}ckJMiy>A^oofwDfC~IH)z8{VHKGT@#E5I(Ll&+MnMCl>~AV7+>Gi%mF zkU1QlKASdR0B80!YhP<$Ywi0?W2Ux45oPfxv9QolWzJPD^weBfvo4SONxP35106sAmh(e+vAs0GboFD@PvNs)jNPvarhW}0YliZEg{Gazv z+JDIpoojRVPr<*C|BTq<`6ga{5q^8^!|0cxe=rZ!zxH3%f5ZO0cQ*Z<^$Yt2{|Ek0 zyT|*F+CO@K;(owBKtGg!S^xj-Z~rga2m6nxKl9J=fBSuNKW_dLKWhJKeg^-Xe`^1? z`TyJj)8E!#>_3Y?uKrwqq3LJ#SGU>AzUO|6`nR^u&3FNN_jGOc zw)Nw`wr3yIKhgcee6IaN=ws>M{6677%)hPwx&HzC(f&u~&)6@b2kNRzBDQAP0*H73 zq%McOmRk{B3i47qRe=DA*$&odrbEJZ*pV9XXa&p@wlW~@Yfs>V{yiTtplMhgM*-Bz zsSnlq&pG;z0OUN%$~$3=g1UF+G*>+17eRbBf3=y79J}KR8owon@$1Z7MIrvvWWH)34nK2SD)GsrJ{l z1Cl#oVo3A8qY3e=aF)qzms~FG#2$LzT=gs&aVMOj>(%{y<&O0cG!nCiESl~x=^dF{ zKvj8F1K8Ng171wwM5Fh4KoQw`_c6#y$(5cAm7e}~nJ#A*fx+c9;y#&W!#VukR)ugk zKp3=+;Ut+IYn%m+r4d*<`L2h%aDnX5}^!5R|H;(34AoVWjRx(msBZvk;rCI*|~ zdOijqI@9Z{Vu!~jvHW{lBa$rnl4+!s_5sfK3bCGk-B%iDe&@-}+%fOKU|(9?V1 zHE8&@4z)Kx!RAvAs z!Wic9=o#(bg?kc-G68-m(jZ`^=XGUXb)}t(%&~sjFnV^sEX%hSy6UKC4iOhgV=BHV z2w`4g7Y=s#Vu2B_?#VQ|hP39@eArgfX>-0S+dd&^mx0*wp}>)x;c4RUgxz%;oNe?& z-7-lJ@Y^2^C;=qJsxx5|xF)*pTGhch2B&kxtn;f!7=gznk}I3}Dh}(CoMXgA5-p&kS202!l?!fT3t|HG*rIP~mS* z$Wjo}jq3}z$Qq!9yrtd3fM0N629ZM?LU$nv@Tv9b7I;D|;0H2dsA~g7Z7zp1| zB)XmrkMgF6OQr|R)HHD^TE{Y#j!~SR?b`Xt3Qs`B+x<hxexYeAjMUWdZ-*n9%(1)Wb(n2U<><7&9dwGJmrob)4%H? zlQ%z+L-^$dFhhH|@u$%97Qz?*Ynh2VG@q|?8vY&L74&fs&_b&3$x&Oyjl~LQDRRap zJU4U*R+(2Dd!G+lh8!V{pT_UJn+^1Qg6$` zqkNm(a#hWyc6SP+p5=C4HL8-m`pO`5o~`-LI?_h5CsH?F_%?nDodmz&pWR20WTpJE z?N|wSzLjMUK8E)a2tI}Lf;+;*M|h3Y(U#>)g1>zk9|Hd}oZAa2 zLYBWBoSW!Ts!RwXr^8h+U*@{9{zqS^iH)Op<;r`Uw~nc}<^$V~_i%$GFjaG?X1@E|M`h)nekvFKt`Dh-f>@|0-`Xoq)o` zx;JmzDfOV9qCx|EVpogEe0LK~tGS?5$$L_i6P$P6wIsCQaP_;d{{N=iV@+8LI}o#( zvo*Ejy=IIn{rdIQh1&q-{EuohpVOjJ^Q3lD*YTp37$^RRgn8ihpdu5{Ct%5-KO!VL zcNB6dUajXI9jkm-P|i3~GB-A(X`P1Oqqb$tcku)UJw0w3GeUijb__#QT4j%64z%EeB7S?jlWwx_7&+EEvB|6N=kV}DwnyAlX=?j`) zmU#!$*^@NIu#n_d7;WoJV@*Fbv9|yJO4;n|BNF2xy(54RyB>t~8lUOUW$&2%Nwi1y zx6JxW88>U2$#qhl^6KUbtmg9}D0o5vYDT7kWJthLGkpGnN4T>{St^_EU>4;DmLF9o zr|LqsA8_MoNLQ=}w?8u!ziSZ@PC#Y<#9uJFo-ozVo6D;<8j^1$c|qAE3ZTE5i~zmE z$BU5lw6l=EWsg^y^;8>r9qH{xfL|~PZYK#md$zZ0?o11gV<*WSW~cgy2GYGQir%wf zt4iW8D+;s*;RGrmd(-T<@2&j(Cb9xhV*l-x`TpK`xq|7p?5R%5*s!69?2c!cC*VY* z2DE^9pvOPLU!1e}wA8S8opcTJ3`NB>hY=JQnL~QFXR4K8A$BqJnoEB$wn-%u@E6Mh zCfMF4kusv3N!(aHC}4)Xs^xoOwXd%e^6pi5|DZo=Q25j+6HlJ^7FodH6y1bMROR^q zGu6)fopS`h%Sw<;ZH%TEPf+#81-#_v+@8nlR0jLcIDKQtLleOC)6yLZgC!D9X3GgS zohwU{v$jl=quD#Go^hB{`@Qw*a%`(^jyT~=q^bWgGzRj;|12J55HWdCWV}EB|K=%N z3Nq-qxJJ`>^|1MNN+q}zTB&ooE3j==AgK@^UW<^oSbeALa2peF)Th6{@sj0KyMNHZ zksk1+MXN2tv+22A%cQOGpS9)77(uP9mh+!5T5ERLvF@b}$+WvXM45Z?-kCa)fb~f1 znVbTD$Gx-0Zxc`0D@YgHakge6SL0H`-vN_x?AP0>iGH0_EE&=v83hMJgaKAI0jJXm zVxVz;X<$v6WW7}fxROO7vr#YLP;;lij5VrX{;>7kK6TtOH&6|Ar^xo>00%+u$C4@# z>!jOt6*3><171+WxoZnKDTzJtDRw+T030;yI}~uV@9fCnei^I*j>Bp&mzP2d=FPb_ zCM*l_+$LDR3B*a!A$g#>xsrZvw0lckxmMg>0aQd7tPyN=t{dgXb;Ie+T8{fZH=gdu zM7Rg9c(kg(Jg0?ARRRl=AONFKrvFj)lTY$KfT%6^6s`mk*ABGhsce*LsoD>K{z_M2 ziPpnu+lw22PfF!CoId^6n*G4H(Ix+#+N{C(da7t1BYMGEaE#PdpOLxsVD5riQXHp@OX;`S`8VnpM~)I920w~<3|mo0 zf8~Az`*?2?H&gZ&*K&bRkV@qzvMlRHXys8*Ze2+1c?5o!^+$&MHxB@4Ee5cke52R! zmn7AZtY6ST%ixgU5)%$%QcwHj7Es-Qu^kLAPwy%7pGBw_4Q9#da^W2$}axNHr03)_nw z5?yuNmXrI5HgS46)c5&}B)Tts49oU92>3xBLLy}FMUW=84DQbVq^;7_e7|(Sdz|&J z73N+M`rc2rt*oSWu#7S{*s~nH6HRHJS1SmzeXk|;CA)FI4bat3<%}nkB%;;?=F>B7ms9QSxv#@+69;@>QaR?REYX4&)=itG>rM{<{A79Rmk)`5ON#GL`*KX%}Ihk3w(RtM-WLt z?f&FLF}4N^yE!(pZ&Yj&Bc`~K0@4_}*0Om?wN|}4WJ>WL;G^H2*QpgEkGA~OET-Km zkwz|5{6dnz1U<2Pe9DNL>3g5FEIvp1jzP&2K#z~j%g6!7B;^zF+o95?fV{3mnB8*RMhCDNp>Am-3e@jNfMj?jHV$MWjk!DDKP zkAz$Y?Sr)!GUOX}qTQ5aMh|wq1uq}~joWyKl=b_LboM#wi{CMuz5x6BKlA-qy++cM01D3b7`uD z#l6M4pI;JCypO8JZ6?U&wNxR!{4oB_ zlV!x9+-&Qy6{%MQ{~yoZGkKiTSC`YS_j22~G;xUV855g2&C(zm^V!(wpcm@zn{%!g z4}JGo(sGZ1O~to-}le

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/appcompose/src/main/res/values-night/themes.xml b/appcompose/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..9244c6b6 --- /dev/null +++ b/appcompose/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/appcompose/src/main/res/values/colors.xml b/appcompose/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/appcompose/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/appcompose/src/main/res/values/strings.xml b/appcompose/src/main/res/values/strings.xml new file mode 100644 index 00000000..731b4987 --- /dev/null +++ b/appcompose/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + app compose + \ No newline at end of file diff --git a/appcompose/src/main/res/values/themes.xml b/appcompose/src/main/res/values/themes.xml new file mode 100644 index 00000000..de5183ef --- /dev/null +++ b/appcompose/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/appcompose/src/test/java/com/readrops/app/compose/ExampleUnitTest.kt b/appcompose/src/test/java/com/readrops/app/compose/ExampleUnitTest.kt new file mode 100644 index 00000000..64e9d35d --- /dev/null +++ b/appcompose/src/test/java/com/readrops/app/compose/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.readrops.app.compose + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 3836269d..1ac70bad 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':app', ':api', ':db' +include ':appcompose' From 3a0a8d5c0587875ec2e31ff42fa7ecd30d1dace4 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 28 Feb 2023 21:51:43 +0100 Subject: [PATCH 06/95] Rename compose activity --- appcompose/src/main/AndroidManifest.xml | 2 +- .../readrops/app/compose/{BaseActivity.kt => MainActivity.kt} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/{BaseActivity.kt => MainActivity.kt} (98%) diff --git a/appcompose/src/main/AndroidManifest.xml b/appcompose/src/main/AndroidManifest.xml index 1cbd94ad..7ae094c0 100644 --- a/appcompose/src/main/AndroidManifest.xml +++ b/appcompose/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ android:theme="@style/Theme.Readrops"> diff --git a/appcompose/src/main/java/com/readrops/app/compose/BaseActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/BaseActivity.kt rename to appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 5b687973..1ed1d3c5 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/BaseActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -18,7 +18,7 @@ import com.readrops.app.compose.feeds.FeedsScreen import com.readrops.app.compose.more.MoreScreen import com.readrops.app.compose.timelime.TimelineScreen -class BaseActivity : ComponentActivity() { +class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { From 6468d9d733ca78629202be460d2cd263a62c1447 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 28 Feb 2023 22:05:32 +0100 Subject: [PATCH 07/95] Add new dependencies --- app/src/main/AndroidManifest.xml | 5 +++++ appcompose/build.gradle | 3 +++ db/build.gradle | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fc9bca41..7c4c79f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -62,6 +62,11 @@ android:launchMode="singleTask" android:theme="@style/SplashTheme" android:exported="true"> + + + + + Date: Fri, 3 Mar 2023 19:49:49 +0100 Subject: [PATCH 08/95] Add new DAOs * Replacement of RxJava by coroutines * Prepare for their use in the news repositories --- db/build.gradle | 2 +- db/src/main/java/com/readrops/db/Database.kt | 8 ++++++ .../com/readrops/db/dao/newdao/NewBaseDao.kt | 26 +++++++++++++++++++ .../com/readrops/db/dao/newdao/NewFeedDao.kt | 18 +++++++++++++ .../com/readrops/db/dao/newdao/NewItemDao.kt | 10 +++++++ 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 db/src/main/java/com/readrops/db/dao/newdao/NewBaseDao.kt create mode 100644 db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt create mode 100644 db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt diff --git a/db/build.gradle b/db/build.gradle index bd7a47e6..b0cfdd28 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -76,7 +76,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - def room_version = "2.3.0" + def room_version = "2.4.3" api "androidx.room:room-runtime:$room_version" api "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt index 16ab3856..3e24102c 100644 --- a/db/src/main/java/com/readrops/db/Database.kt +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -4,6 +4,8 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.readrops.db.dao.* +import com.readrops.db.dao.newdao.NewFeedDao +import com.readrops.db.dao.newdao.NewItemDao import com.readrops.db.entities.* import com.readrops.db.entities.account.Account import dev.matrix.roomigrant.GenerateRoomMigrations @@ -24,4 +26,10 @@ abstract class Database : RoomDatabase() { abstract fun itemStateDao(): ItemStateDao abstract fun itemStateChangesDao(): ItemStateChangeDao + + // new dao + + abstract fun newFeedDao(): NewFeedDao + + abstract fun newItemDao(): NewItemDao } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewBaseDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewBaseDao.kt new file mode 100644 index 00000000..8f41e43d --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewBaseDao.kt @@ -0,0 +1,26 @@ +package com.readrops.db.dao.newdao + +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update + +interface NewBaseDao { + + @Insert + suspend fun insert(entity: T): Long + + @Insert + suspend fun insert(entities: List?): List + + @Update + suspend fun update(entity: T) + + @Update + suspend fun update(entities: List?) + + @Delete + suspend fun delete(entity: T) + + @Delete + suspend fun delete(entities: List?) +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt new file mode 100644 index 00000000..b5d11f56 --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -0,0 +1,18 @@ +package com.readrops.db.dao.newdao + +import androidx.room.Dao +import androidx.room.Query +import com.readrops.db.entities.Feed + +@Dao +abstract class NewFeedDao : NewBaseDao { + + @Query("Select * from Feed Where account_id = :accountId order by name ASC") + abstract suspend fun selectFeeds(accountId: Int): List + + @Query("Update Feed set etag = :etag, last_modified = :lastModified Where id = :feedId") + abstract suspend fun updateHeaders(etag: String, lastModified: String, feedId: Int) + + @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") + abstract suspend fun feedExists(feedUrl: String, accountId: Int): Boolean +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt new file mode 100644 index 00000000..dc43188b --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -0,0 +1,10 @@ +package com.readrops.db.dao.newdao + +import androidx.room.Dao +import com.readrops.db.entities.Item + +@Dao +abstract class NewItemDao : NewBaseDao { + + +} \ No newline at end of file From e3726092986c3f56840248825a17eabad9289a93 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 3 Mar 2023 19:50:10 +0100 Subject: [PATCH 09/95] Disable Niddler entirely --- api/src/main/java/com/readrops/api/ApiModule.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index 262bfde3..2c43dac8 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -1,6 +1,5 @@ package com.readrops.api -import com.chimerapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.Credentials import com.readrops.api.services.freshrss.FreshRSSDataSource @@ -30,7 +29,7 @@ val apiModule = module { .callTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.HOURS) .addInterceptor(get()) - .addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler")) + //.addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler")) .build() } From 4e09c16d53c2d6e489a7b33247a4694ec9816221 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 8 Mar 2023 22:45:52 +0100 Subject: [PATCH 10/95] Add koin first declarations in compose module --- appcompose/build.gradle | 12 +++++++++- appcompose/src/main/AndroidManifest.xml | 1 + .../readrops/app/compose/ComposeAppModule.kt | 10 ++++++++ .../com/readrops/app/compose/ReadropsApp.kt | 23 +++++++++++++++++++ .../app/compose/timelime/TimelineScreen.kt | 3 +++ .../app/compose/timelime/TimelineViewModel.kt | 12 ++++++++++ build.gradle | 2 +- db/build.gradle | 2 +- 8 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/ReadropsApp.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt diff --git a/appcompose/build.gradle b/appcompose/build.gradle index bc55fd60..fab20394 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -76,8 +76,18 @@ dependencies { implementation "cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyager" implementation "cafe.adriel.voyager:voyager-tab-navigator:$voyager" implementation "cafe.adriel.voyager:voyager-androidx:$voyager" - //implementation "cafe.adriel.voyager:voyager-koin:$voyager" + implementation "cafe.adriel.voyager:voyager-koin:$voyager" debugImplementation "androidx.compose.ui:ui-tooling:1.3.3" implementation "androidx.compose.ui:ui-tooling-preview:1.3.3" + + def lifecycle_version = "2.5.1" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" + + /*def koin_version = "3.3.3" + implementation "io.insert-koin:koin-core:$koin_version" + implementation "io.insert-koin:koin-android:$koin_version" + implementation "io.insert-koin:koin-androidx-compose:3.4.2"*/ } \ No newline at end of file diff --git a/appcompose/src/main/AndroidManifest.xml b/appcompose/src/main/AndroidManifest.xml index 7ae094c0..379d77bf 100644 --- a/appcompose/src/main/AndroidManifest.xml +++ b/appcompose/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ () + Column { TimelineItem() } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt new file mode 100644 index 00000000..0a7f73ab --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -0,0 +1,12 @@ +package com.readrops.app.compose.timelime + +import androidx.lifecycle.ViewModel +import com.readrops.db.Database + +class TimelineViewModel( + val database: Database +) : ViewModel() { + + + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b8530fd1..eb7591c4 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ ext { targetSdkVersion = 33 buildToolsVersion = "33.0.2" - koin_version = "3.1.2" + koin_version = "3.3.3" } task clean(type: Delete) { diff --git a/db/build.gradle b/db/build.gradle index b0cfdd28..7c2ab020 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -93,7 +93,7 @@ dependencies { api "io.insert-koin:koin-core:$rootProject.ext.koin_version" api "io.insert-koin:koin-android:$rootProject.ext.koin_version" - api "io.insert-koin:koin-android-compat:$rootProject.ext.koin_version" + api "io.insert-koin:koin-androidx-compose:3.4.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' From 37f8bab393d510db07e91bece9f3eced19cccd21 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 18 Jul 2023 22:39:20 +0200 Subject: [PATCH 11/95] Update some dependencies --- appcompose/build.gradle | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/appcompose/build.gradle b/appcompose/build.gradle index fab20394..a936f9a6 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -64,11 +64,11 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - def composeBom = platform('androidx.compose:compose-bom:2022.12.00') + def composeBom = platform('androidx.compose:compose-bom:2023.06.01') implementation composeBom androidTestImplementation composeBom - implementation 'androidx.activity:activity-compose:1.5.1' + implementation 'androidx.activity:activity-compose:1.7.2' implementation 'androidx.compose.material3:material3' def voyager = "1.0.0-rc03" @@ -78,10 +78,10 @@ dependencies { implementation "cafe.adriel.voyager:voyager-androidx:$voyager" implementation "cafe.adriel.voyager:voyager-koin:$voyager" - debugImplementation "androidx.compose.ui:ui-tooling:1.3.3" - implementation "androidx.compose.ui:ui-tooling-preview:1.3.3" + debugImplementation "androidx.compose.ui:ui-tooling:1.4.3" + implementation "androidx.compose.ui:ui-tooling-preview:1.4.3" - def lifecycle_version = "2.5.1" + def lifecycle_version = "2.6.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" @@ -90,4 +90,8 @@ dependencies { implementation "io.insert-koin:koin-core:$koin_version" implementation "io.insert-koin:koin-android:$koin_version" implementation "io.insert-koin:koin-androidx-compose:3.4.2"*/ + + androidTestImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version" + androidTestImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version" + } \ No newline at end of file From f7d1eea3ec3fe16d77e1d923471a7308b556d956 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 21 Jul 2023 22:29:52 +0200 Subject: [PATCH 12/95] Add new LocalRSSRepository with some tests --- api/build.gradle | 2 +- appcompose/build.gradle | 1 + .../app/compose/LocalRSSRepositoryTest.kt | 113 ++++++++++++++++++ .../com/readrops/app/compose/TestUtils.kt | 9 ++ .../src/androidTest/resources/rss_feed.xml | 61 ++++++++++ .../app/compose/repositories/ARepository.kt | 56 +++++++++ .../repositories/LocalRSSRepository.kt | 93 ++++++++++++++ 7 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 appcompose/src/androidTest/java/com/readrops/app/compose/LocalRSSRepositoryTest.kt create mode 100644 appcompose/src/androidTest/java/com/readrops/app/compose/TestUtils.kt create mode 100644 appcompose/src/androidTest/resources/rss_feed.xml create mode 100644 appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt diff --git a/api/build.gradle b/api/build.gradle index 3b258810..c1aaf9bf 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -59,7 +59,7 @@ dependencies { implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0' implementation 'org.redundent:kotlin-xml-builder:1.7.3' - implementation 'com.squareup.okhttp3:okhttp:4.9.1' + api 'com.squareup.okhttp3:okhttp:4.9.1' implementation('com.squareup.retrofit2:retrofit:2.9.0') { exclude group: 'com.squareup.okhttp3', module: 'okhttp3' diff --git a/appcompose/build.gradle b/appcompose/build.gradle index a936f9a6..3eade690 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -94,4 +94,5 @@ dependencies { androidTestImplementation "io.insert-koin:koin-test-junit4:$rootProject.ext.koin_version" androidTestImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version" + androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' } \ No newline at end of file diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/LocalRSSRepositoryTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/LocalRSSRepositoryTest.kt new file mode 100644 index 00000000..44220734 --- /dev/null +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/LocalRSSRepositoryTest.kt @@ -0,0 +1,113 @@ +package com.readrops.app.compose + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import com.readrops.api.apiModule +import com.readrops.api.utils.ApiUtils +import com.readrops.api.utils.AuthInterceptor +import com.readrops.app.compose.repositories.LocalRSSRepository +import com.readrops.db.Database +import com.readrops.db.entities.Feed +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType +import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import org.junit.Before +import org.junit.Test +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koin.test.get +import java.net.HttpURLConnection +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class LocalRSSRepositoryTest : KoinTest { + + private val mockServer: MockWebServer = MockWebServer() + private val account = Account(accountType = AccountType.LOCAL) + private lateinit var database: Database + private lateinit var repository: LocalRSSRepository + private lateinit var feeds: List + + @Before + fun before() { + val context = ApplicationProvider.getApplicationContext() + database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build() + + KoinTestRule.create { + modules(apiModule, module { + single { database } + single { + OkHttpClient.Builder() + .callTimeout(1, TimeUnit.MINUTES) + .readTimeout(1, TimeUnit.HOURS) + .addInterceptor(get()) + .build() + } + }) + } + + mockServer.start() + val url = mockServer.url("/rss") + + account.id = database.accountDao().compatInsert(account).toInt() + feeds = listOf( + Feed( + name = "feedTest", + url = url.toString(), + accountId = account.id, + ), + ) + + runBlocking { + database.newFeedDao().insert(feeds).run { + feeds.first().id = first().toInt() + } + } + + repository = LocalRSSRepository(get(), database, account) + } + + @Test + fun synchronizeTest() = runBlocking { + val stream = TestUtils.loadResource("rss_feed.xml") + + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8") + .setBody(Buffer().readFrom(stream)) + ) + + val result = repository.synchronize(null) { + assertEquals(it.name, feeds.first().name) + } + + assertTrue { result.first.items.isNotEmpty() } + assertTrue { database.itemDao().itemExists(result.first.items.first().guid!!, account.id) } + } + + @Test + fun synchronizeWithFeedsTest(): Unit = runBlocking { + val stream = TestUtils.loadResource("rss_feed.xml") + + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8") + .setBody(Buffer().readFrom(stream)) + ) + + val result = repository.synchronize(feeds) { + assertEquals(it.name, feeds.first().name) + } + + assertTrue { result.first.items.isNotEmpty() } + assertTrue { database.itemDao().itemExists(result.first.items.first().guid!!, account.id) } + } +} \ No newline at end of file diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/TestUtils.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/TestUtils.kt new file mode 100644 index 00000000..e7e69a6b --- /dev/null +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/TestUtils.kt @@ -0,0 +1,9 @@ +package com.readrops.app.compose + +import java.io.InputStream + +object TestUtils { + + fun loadResource(path: String): InputStream = + javaClass.classLoader?.getResourceAsStream(path)!! +} \ No newline at end of file diff --git a/appcompose/src/androidTest/resources/rss_feed.xml b/appcompose/src/androidTest/resources/rss_feed.xml new file mode 100644 index 00000000..c4702aae --- /dev/null +++ b/appcompose/src/androidTest/resources/rss_feed.xml @@ -0,0 +1,61 @@ + + + + Hacker News + + https://news.ycombinator.com/ + Links for the intellectually curious, ranked by readers. + + Africa declared free of wild polio + https://www.bbc.com/news/world-africa-53887947 + Tue, 25 Aug 2020 17:15:49 +0000 + https://news.ycombinator.com/item?id=24273602 + Author 1 + Comments]]> + media description + + + Palantir S-1 + https://www.sec.gov/Archives/edgar/data/1321655/000119312520230013/d904406ds1.htm + Tue, 25 Aug 2020 21:03:42 +0000 + https://news.ycombinator.com/item?id=24276086 + Comments]]> + + + Openwifi: Linux mac80211 compatible full-stack 802.11/Wi-Fi design based on SDR + https://github.com/open-sdr/openwifi + Tue, 25 Aug 2020 17:45:19 +0000 + https://news.ycombinator.com/item?id=24273919 + Comments]]> + + + Syllabus for Eric's PhD Students + https://docs.google.com/document/d/11D3kHElzS2HQxTwPqcaTnU5HCJ8WGE5brTXI4KLf4dM/edit + Tue, 25 Aug 2020 18:55:12 +0000 + https://news.ycombinator.com/item?id=24274699 + Comments]]> + + + WebBundles harmful to content blocking, security tools, and the open web + https://brave.com/webbundles-harmful-to-content-blocking-security-tools-and-the-open-web/ + Tue, 25 Aug 2020 19:18:50 +0000 + https://news.ycombinator.com/item?id=24274968 + Comments]]> + + + Zappos CEO Tony Hsieh is stepping down after 21 years + https://footwearnews.com/2020/business/executive-moves/zappos-ceo-tony-hsieh-steps-down-1203045974/ + Tue, 25 Aug 2020 06:11:42 +0000 + https://news.ycombinator.com/item?id=24268522 + Comments]]> + + + Evgeny Kuznetsov practices with Bauer stick that has hole in the blade + https://russianmachineneverbreaks.com/2020/07/17/evgeny-kuznetsov-practices-with-bauer-stick-that-has-hole-in-the-blade/ + Tue, 25 Aug 2020 19:38:09 +0000 + https://news.ycombinator.com/item?id=24275159 + Comments]]> + + + \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt new file mode 100644 index 00000000..a4f40ba8 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -0,0 +1,56 @@ +package com.readrops.app.compose.repositories + +import com.readrops.api.services.SyncResult +import com.readrops.db.Database +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder +import com.readrops.db.entities.account.Account + +data class ErrorResult( + val values: Map +) + +abstract class ARepository( + val database: Database, + val account: Account +) { + + /** + * This method is intended for remote accounts. + */ + abstract suspend fun login() + + /** + * Global synchronization for the local account. + * @param selectedFeeds feeds to be updated + * @param onUpdate get synchronization status + * @return returns the result of the synchronization used by notifications + * and errors per feed if occurred to be transmitted to the user + */ + abstract suspend fun synchronize( + selectedFeeds: List?, + onUpdate: (Feed) -> Unit + ): Pair + + /** + * Global synchronization for remote accounts. Unlike the local account, remote accounts + * won't benefit from synchronization status and granular synchronization + */ + abstract suspend fun synchronize(): SyncResult + + abstract suspend fun insertNewFeeds() +} + +abstract class BaseRepository( + database: Database, + account: Account, +) : ARepository(database, account) { + + open suspend fun updateFeed(feed: Feed) {} + + open suspend fun deleteFeed(feed: Feed) {} + + open suspend fun addFolder(folder: Folder) {} + + open suspend fun deleteFolder(folder: Folder) {} +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt new file mode 100644 index 00000000..21fd9438 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -0,0 +1,93 @@ +package com.readrops.app.compose.repositories + +import com.readrops.api.localfeed.LocalRSSDataSource +import com.readrops.api.services.SyncResult +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 + +class LocalRSSRepository( + private val dataSource: LocalRSSDataSource, + database: Database, + account: Account +) : BaseRepository(database, account) { + + override suspend fun login() { /* useless here */ + } + + override suspend fun synchronize( + selectedFeeds: List?, + onUpdate: (Feed) -> Unit + ): Pair { + val errors = mutableMapOf() + val syncResult = SyncResult() + + val feeds = if (selectedFeeds.isNullOrEmpty()) { + database.newFeedDao().selectFeeds(account.id) + } else selectedFeeds + + for (feed in feeds) { + try { + val pair = dataSource.queryRSSResource(feed.url!!, null) + + pair?.let { + insertNewItems(it.second, feed) + syncResult.items = it.second + } + } catch (e: Exception) { + errors[feed] = e + } + + } + + return Pair(syncResult, ErrorResult(errors)) + } + + override suspend fun synchronize(): SyncResult = throw NotImplementedError("This method can't be called here") + + + override suspend fun insertNewFeeds() { + /*TODO("Not yet implemented")*/ + } + + private suspend fun insertNewItems(items: List, feed: Feed) { + items.sortedWith(Item::compareTo) // TODO Check if ordering is useful in this situation + val itemsToInsert = mutableListOf() + + for (item in items) { + if (!database.itemDao().itemExists(item.guid!!, feed.accountId)) { + if (item.description != null) { + item.cleanDescription = Jsoup.parse(item.description).text() + } + + if (item.content != null) { + item.readTime = 0.0 + } else if (item.description != null) { + item.readTime = 0.0 + } + + item.feedId = feed.id + itemsToInsert += item + } + } + + database.newItemDao().insert(itemsToInsert) + } + + private suspend fun insertFeed(feed: Feed): Feed { + if (database.newFeedDao().feedExists(feed.url!!, account.id)) { + throw IllegalStateException("Feed already exists for account ${account.accountName}") + } + + return feed.apply { + accountId = account.id + // we need empty headers to query the feed just after, without any 304 result + etag = null + lastModified = null + + id = database.newFeedDao().insert(this).toInt() + } + } +} \ No newline at end of file From da496cd91a0208ffe3c2efc69e395d224429e275 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 22 Jul 2023 15:22:43 +0200 Subject: [PATCH 13/95] Initial UI for adding new feed --- .../readrops/app/compose/ComposeAppModule.kt | 11 ++++ .../app/compose/feeds/AddFeedDialog.kt | 59 +++++++++++++++++++ .../readrops/app/compose/feeds/FeedsScreen.kt | 47 +++++++++++++-- .../app/compose/feeds/FeedsViewModel.kt | 18 ++++++ .../app/compose/repositories/ARepository.kt | 2 +- .../repositories/LocalRSSRepository.kt | 15 +++-- 6 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index b919ee49..e7042738 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -1,10 +1,21 @@ package com.readrops.app.compose +import com.readrops.app.compose.feeds.FeedsViewModel +import com.readrops.app.compose.repositories.BaseRepository +import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.app.compose.timelime.TimelineViewModel +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val composeAppModule = module { viewModel { TimelineViewModel(get()) } + + viewModel { FeedsViewModel(get()) } + + // repositories + + single { LocalRSSRepository(get(), get(), Account(id = 1, isCurrentAccount = true, accountType = AccountType.LOCAL)) } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt new file mode 100644 index 00000000..d1bf7b85 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt @@ -0,0 +1,59 @@ +package com.readrops.app.compose.feeds + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +@Composable +fun AddFeedDialog( + onDismiss: () -> Unit, + onValidate: (String) -> Unit, +) { + var url by remember { mutableStateOf("") } + + Dialog( + onDismissRequest = onDismiss + ) { + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) + ) { + Text( + text = "Add new feed", + style = MaterialTheme.typography.headlineSmall + ) + + Spacer(modifier = Modifier.size(8.dp)) + + TextField( + value = url, + onValueChange = { url = it } + ) + + Spacer(modifier = Modifier.size(8.dp)) + + Button( + onClick = { onValidate(url) }, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Text(text = "Validate") + } + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt index d0f463fd..48a7ba2d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt @@ -1,18 +1,55 @@ package com.readrops.app.compose.feeds -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import cafe.adriel.voyager.androidx.AndroidScreen +import org.koin.androidx.compose.getViewModel class FeedsScreen : AndroidScreen() { @Composable override fun Content() { - Column { - Text( - text = "Feeds" + val viewModel = getViewModel() + var showDialog by remember { mutableStateOf(false) } + + if (showDialog) { + AddFeedDialog( + onDismiss = { showDialog = false }, + onValidate = { + showDialog = false + viewModel.insertFeed(it) + } ) } + + Box( + modifier = Modifier.fillMaxSize() + ) { + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + onClick = { showDialog = true } + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null + ) + } + } + } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt new file mode 100644 index 00000000..6f626a68 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt @@ -0,0 +1,18 @@ +package com.readrops.app.compose.feeds + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.app.compose.repositories.BaseRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class FeedsViewModel( + private val repository: BaseRepository, +) : ViewModel() { + + fun insertFeed(url: String) { + viewModelScope.launch(context = Dispatchers.IO) { + repository.insertNewFeeds(listOf(url)) + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index a4f40ba8..46efa7fa 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -38,7 +38,7 @@ abstract class ARepository( */ abstract suspend fun synchronize(): SyncResult - abstract suspend fun insertNewFeeds() + abstract suspend fun insertNewFeeds(urls: List) } abstract class BaseRepository( diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index 21fd9438..de0824c4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -48,8 +48,15 @@ class LocalRSSRepository( override suspend fun synchronize(): SyncResult = throw NotImplementedError("This method can't be called here") - override suspend fun insertNewFeeds() { - /*TODO("Not yet implemented")*/ + override suspend fun insertNewFeeds(urls: List) { + for (url in urls) { + try { + val result = dataSource.queryRSSResource(url, null)!! + insertFeed(result.first) + } catch (e: Exception) { + throw e + } + } } private suspend fun insertNewItems(items: List, feed: Feed) { @@ -77,8 +84,8 @@ class LocalRSSRepository( } private suspend fun insertFeed(feed: Feed): Feed { - if (database.newFeedDao().feedExists(feed.url!!, account.id)) { - throw IllegalStateException("Feed already exists for account ${account.accountName}") + require(!database.newFeedDao().feedExists(feed.url!!, account.id)) { + "Feed already exists for account ${account.accountName}" } return feed.apply { From bda5896413b335272c23c6cdd3f54b840085a42e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 22 Jul 2023 19:44:44 +0200 Subject: [PATCH 14/95] Rewrite HtmlParser in Kotlin with some tests --- .../java/com/readrops/api/utils/HtmlParser.kt | 75 +++ .../com/readrops/api/utils/HtmlParserTest.kt | 89 +++ api/src/test/resources/utils/file.html | 601 ++++++++++++++++++ .../resources/utils/file_without_head.html | 593 +++++++++++++++++ 4 files changed, 1358 insertions(+) create mode 100644 api/src/main/java/com/readrops/api/utils/HtmlParser.kt create mode 100644 api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt create mode 100644 api/src/test/resources/utils/file.html create mode 100644 api/src/test/resources/utils/file_without_head.html diff --git a/api/src/main/java/com/readrops/api/utils/HtmlParser.kt b/api/src/main/java/com/readrops/api/utils/HtmlParser.kt new file mode 100644 index 00000000..5e0e3201 --- /dev/null +++ b/api/src/main/java/com/readrops/api/utils/HtmlParser.kt @@ -0,0 +1,75 @@ +package com.readrops.api.utils + +import android.nfc.FormatException +import com.readrops.api.localfeed.LocalRSSHelper +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.Jsoup +import org.jsoup.nodes.Document + +data class ParsingResult( + val url: String, + val label: String?, +) + +object HtmlParser { + + suspend fun getFeedLink(url: String, client: OkHttpClient): List { + val results = mutableListOf() + + val document = getHTMLHeadFromUrl(url, client) + val elements = document.select("link") + + for (element in elements) { + val type = element.attributes()["type"] + + if (LocalRSSHelper.isRSSType(type)) { + results += ParsingResult( + url = element.absUrl("href"), + label = element.attributes()["title"] + ) + } + } + + return results + } + + private fun getHTMLHeadFromUrl(url: String, client: OkHttpClient): Document { + client.newCall(Request.Builder().url(url).build()).execute().use { response -> + if (response.header(ApiUtils.CONTENT_TYPE_HEADER)!!.contains(ApiUtils.HTML_CONTENT_TYPE) + ) { + val body = response.body!!.source() + + val stringBuilder = StringBuilder() + var collectionStarted = false + + while (!body.exhausted()) { + val currentLine = body.readUtf8LineStrict() + + when { + currentLine.contains("") -> { + stringBuilder.append(currentLine) + collectionStarted = true + } + currentLine.contains("") -> { + stringBuilder.append(currentLine) + break + } + collectionStarted -> { + stringBuilder.append(currentLine) + } + } + } + + if (!stringBuilder.contains("") || !stringBuilder.contains("")) + throw Exception("Failed to get HTML head") + + body.close() + return Jsoup.parse(stringBuilder.toString(), url) + } else { + throw FormatException("The response is not a html file") + } + } + } + +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt b/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt new file mode 100644 index 00000000..b4eadaff --- /dev/null +++ b/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt @@ -0,0 +1,89 @@ +package com.readrops.api.utils + +import android.nfc.FormatException +import com.readrops.api.TestUtils +import kotlinx.coroutines.runBlocking +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import org.junit.Rule +import org.junit.Test +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import java.net.HttpURLConnection +import java.util.concurrent.TimeUnit +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class HtmlParserTest : KoinTest { + + private val mockServer = MockWebServer() + + @get:Rule + val koinTestRule = KoinTestRule.create { + modules(module { + single { + OkHttpClient.Builder() + .callTimeout(1, TimeUnit.MINUTES) + .readTimeout(1, TimeUnit.HOURS) + .build() + } + }) + } + + @Test + fun before() { + mockServer.start() + } + + @Test + fun after() { + mockServer.shutdown() + } + + @Test + fun getFeedLinkTest() { + val stream = TestUtils.loadResource("utils/file.html") + + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, ApiUtils.HTML_CONTENT_TYPE) + .setBody(Buffer().readFrom(stream)) + ) + + runBlocking { + val result = + HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) + + assertTrue { result.size == 1 } + assertTrue { result.first().url.endsWith("/rss") } + assertEquals("RSS", result.first().label) + + } + } + + @Test(expected = Exception::class) + fun getFeedLinkWithoutHeadTest() { + val stream = TestUtils.loadResource("utils/file_without_head.html") + + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, ApiUtils.HTML_CONTENT_TYPE) + .setBody(Buffer().readFrom(stream)) + ) + + runBlocking { HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) } + } + + @Test(expected = FormatException::class) + fun getFeedLinkNoHtmlFileTest() { + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/rss+xml")) + + + runBlocking { HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) } + } +} \ No newline at end of file diff --git a/api/src/test/resources/utils/file.html b/api/src/test/resources/utils/file.html new file mode 100644 index 00000000..d55ef435 --- /dev/null +++ b/api/src/test/resources/utils/file.html @@ -0,0 +1,601 @@ + + + + + + + + Hacker News + + +

+ + + + + + + + + + + +
+ + + + + + +
Hacker News + new | past | comments | ask | show | jobs | submit + + login + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1.A Brief History of Computers (lesswrong.com)
+ 31 points by zdw 1 hour ago | hide | 3 comments +
2.Consumer Software Is Expected to Be Next Fast-Growing Segment (1994) (csmonitor.com)
+ 9 points by 1970-01-01 1 hour ago | hide | 1 comment +
3.MSX-DOS (wikipedia.org)
+ 82 points by pavlov 6 hours ago | hide | 26 comments +
4.New Yorkers Got Broken Promises. Developers Got 20M Sq. Ft (nytimes.com)
+ 12 points by asnyder 20 minutes ago | hide | 1 comment +
5.Apple's interactive television box: Hacking the set top box System 7.1 in ROM (oldvcr.blogspot.com)
+ 160 points by todsacerdoti 10 hours ago | hide | 20 comments +
6.Putting the “You” in CPU (cpu.land)
+ 187 points by uneekname 10 hours ago | hide | 73 comments +
7.Botulinum toxin: Bioweapon and magic drug (nih.gov)
+ 12 points by redbell 2 hours ago | hide | 10 comments +
8.Octos – HTML live wallpaper engine (github.com/underpig1)
+ 85 points by underpig1 6 hours ago | hide | 23 comments +
9.More than you've ever wanted to know about errors in Rust (shuttle.rs)
+ 13 points by asymmetric 2 hours ago | hide | 3 comments +
10.Embrace Complexity; Tighten Your Feedback Loops (ferd.ca)
+ 27 points by lutzh 4 hours ago | hide | 1 comment +
11.AWS networking concepts in a diagram (miparnisariblog.wordpress.com)
+ 171 points by mparnisari 10 hours ago | hide | 66 comments +
12.Plane – Open-source Jira alternative (plane.so)
+ 240 points by prhrb 7 hours ago | hide | 93 comments +
13.Neurotechnology: Current Developments and Ethical Issues (frontiersin.org)
+ 28 points by Quinzel 3 hours ago | hide | 15 comments +
14.What we talk about when we talk about System Design (maheshba.bitbucket.io)
+ 166 points by scv119 11 hours ago | hide | 22 comments +
15.ElKaWe – Electrocaloric heat pumps (fraunhofer.de)
+ 140 points by danans 10 hours ago | hide | 73 comments +
16.Over-grazing and desertification in the Syrian steppe root causes of war (2015) (theecologist.org)
+ 64 points by joveian 6 hours ago | hide | 43 comments +
17.Redmine – open-source project management (redmine.org)
+ 34 points by synergy20 2 hours ago | hide | 24 comments +
18.Google tries internet air-gap for some staff PCs (theregister.com)
+ 67 points by beardyw 9 hours ago | hide | 73 comments +
19.I thought I wanted to be a professor, then I served on a hiring committee (2021) (science.org)
+ 104 points by ykonstant 4 hours ago | hide | 72 comments +
20.Internet search tips (gwern.net)
+ 161 points by herbertl 12 hours ago | hide | 58 comments +
21.Bayesian methods to provide probablistic solution for the Drake equation (2019) (sciencedirect.com)
+ 22 points by benbreen 4 hours ago | hide | 18 comments +
22.Biotumen: Bitumen Reinvented (biofabrik.com)
+ 40 points by patall 7 hours ago | hide | 11 comments +
23.Why even let users set their own passwords? (devever.net)
+ 103 points by hlandau 2 hours ago | hide | 121 comments +
24.Confronting failure as a core life skill (buildinghealthier.substack.com)
+ 168 points by blh75 15 hours ago | hide | 75 comments +
25.Hokusai’s Illustrated Warrior Vanguard of Japan and China (1836) (publicdomainreview.org)
+ 19 points by tintinnabula 2 hours ago | hide | discuss +
26.Bun v0.7.0 (bun.sh)
+ 163 points by sshroot 9 hours ago | hide | 107 comments +
27.Simpson Fan Grows Tomacco (2003) (simpsonsarchive.com)
+ 81 points by pipeline_peak 6 hours ago | hide | 55 comments +
28.Discovery: Metals can heal themselves (sandia.gov)
+ 77 points by bobvanluijt 13 hours ago | hide | 24 comments +
29.Pressure and vacuum marination does not work (2016) (genuineideas.com)
+ 87 points by OJFord 13 hours ago | hide | 57 comments +
30.Scientists: Fishing boats compete with whales and penguins for Antarctic krill (mongabay.com)
+ 5 points by PaulHoule 1 hour ago | hide | discuss +
+
+ + + + + +
+
+
+ Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

+
Search:
+
+
+
+ + + + diff --git a/api/src/test/resources/utils/file_without_head.html b/api/src/test/resources/utils/file_without_head.html new file mode 100644 index 00000000..3606ac07 --- /dev/null +++ b/api/src/test/resources/utils/file_without_head.html @@ -0,0 +1,593 @@ + + +
+ + + + + + + + + + + +
+ + + + + + +
Hacker News + new | past | comments | ask | show | jobs | submit + + login + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1.A Brief History of Computers (lesswrong.com)
+ 31 points by zdw 1 hour ago | hide | 3 comments +
2.Consumer Software Is Expected to Be Next Fast-Growing Segment (1994) (csmonitor.com)
+ 9 points by 1970-01-01 1 hour ago | hide | 1 comment +
3.MSX-DOS (wikipedia.org)
+ 82 points by pavlov 6 hours ago | hide | 26 comments +
4.New Yorkers Got Broken Promises. Developers Got 20M Sq. Ft (nytimes.com)
+ 12 points by asnyder 20 minutes ago | hide | 1 comment +
5.Apple's interactive television box: Hacking the set top box System 7.1 in ROM (oldvcr.blogspot.com)
+ 160 points by todsacerdoti 10 hours ago | hide | 20 comments +
6.Putting the “You” in CPU (cpu.land)
+ 187 points by uneekname 10 hours ago | hide | 73 comments +
7.Botulinum toxin: Bioweapon and magic drug (nih.gov)
+ 12 points by redbell 2 hours ago | hide | 10 comments +
8.Octos – HTML live wallpaper engine (github.com/underpig1)
+ 85 points by underpig1 6 hours ago | hide | 23 comments +
9.More than you've ever wanted to know about errors in Rust (shuttle.rs)
+ 13 points by asymmetric 2 hours ago | hide | 3 comments +
10.Embrace Complexity; Tighten Your Feedback Loops (ferd.ca)
+ 27 points by lutzh 4 hours ago | hide | 1 comment +
11.AWS networking concepts in a diagram (miparnisariblog.wordpress.com)
+ 171 points by mparnisari 10 hours ago | hide | 66 comments +
12.Plane – Open-source Jira alternative (plane.so)
+ 240 points by prhrb 7 hours ago | hide | 93 comments +
13.Neurotechnology: Current Developments and Ethical Issues (frontiersin.org)
+ 28 points by Quinzel 3 hours ago | hide | 15 comments +
14.What we talk about when we talk about System Design (maheshba.bitbucket.io)
+ 166 points by scv119 11 hours ago | hide | 22 comments +
15.ElKaWe – Electrocaloric heat pumps (fraunhofer.de)
+ 140 points by danans 10 hours ago | hide | 73 comments +
16.Over-grazing and desertification in the Syrian steppe root causes of war (2015) (theecologist.org)
+ 64 points by joveian 6 hours ago | hide | 43 comments +
17.Redmine – open-source project management (redmine.org)
+ 34 points by synergy20 2 hours ago | hide | 24 comments +
18.Google tries internet air-gap for some staff PCs (theregister.com)
+ 67 points by beardyw 9 hours ago | hide | 73 comments +
19.I thought I wanted to be a professor, then I served on a hiring committee (2021) (science.org)
+ 104 points by ykonstant 4 hours ago | hide | 72 comments +
20.Internet search tips (gwern.net)
+ 161 points by herbertl 12 hours ago | hide | 58 comments +
21.Bayesian methods to provide probablistic solution for the Drake equation (2019) (sciencedirect.com)
+ 22 points by benbreen 4 hours ago | hide | 18 comments +
22.Biotumen: Bitumen Reinvented (biofabrik.com)
+ 40 points by patall 7 hours ago | hide | 11 comments +
23.Why even let users set their own passwords? (devever.net)
+ 103 points by hlandau 2 hours ago | hide | 121 comments +
24.Confronting failure as a core life skill (buildinghealthier.substack.com)
+ 168 points by blh75 15 hours ago | hide | 75 comments +
25.Hokusai’s Illustrated Warrior Vanguard of Japan and China (1836) (publicdomainreview.org)
+ 19 points by tintinnabula 2 hours ago | hide | discuss +
26.Bun v0.7.0 (bun.sh)
+ 163 points by sshroot 9 hours ago | hide | 107 comments +
27.Simpson Fan Grows Tomacco (2003) (simpsonsarchive.com)
+ 81 points by pipeline_peak 6 hours ago | hide | 55 comments +
28.Discovery: Metals can heal themselves (sandia.gov)
+ 77 points by bobvanluijt 13 hours ago | hide | 24 comments +
29.Pressure and vacuum marination does not work (2016) (genuineideas.com)
+ 87 points by OJFord 13 hours ago | hide | 57 comments +
30.Scientists: Fishing boats compete with whales and penguins for Antarctic krill (mongabay.com)
+ 5 points by PaulHoule 1 hour ago | hide | discuss +
+
+ + + + + +
+
+
+ Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

+
Search:
+
+
+
+ + + + From 1a37401139ec0ae607f9880702fd0db70be81afa Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 23 Jul 2023 17:18:03 +0200 Subject: [PATCH 15/95] Add swipe to refresh in TimelineScreen with basic item display and refresh --- appcompose/build.gradle | 2 + .../readrops/app/compose/ComposeAppModule.kt | 2 +- .../app/compose/timelime/TimelineScreen.kt | 39 ++++++++++++++-- .../app/compose/timelime/TimelineViewModel.kt | 44 ++++++++++++++++++- .../com/readrops/db/dao/newdao/NewItemDao.kt | 5 ++- 5 files changed, 85 insertions(+), 7 deletions(-) diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 3eade690..2fb960d3 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -71,6 +71,8 @@ dependencies { implementation 'androidx.activity:activity-compose:1.7.2' implementation 'androidx.compose.material3:material3' + implementation "com.google.accompanist:accompanist-swiperefresh:0.30.1" + def voyager = "1.0.0-rc03" implementation "cafe.adriel.voyager:voyager-navigator:$voyager" implementation "cafe.adriel.voyager:voyager-bottom-sheet-navigator:$voyager" diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index e7042738..685890c3 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -11,7 +11,7 @@ import org.koin.dsl.module val composeAppModule = module { - viewModel { TimelineViewModel(get()) } + viewModel { TimelineViewModel(get(), get()) } viewModel { FeedsViewModel(get()) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt index 0f80ecf3..56060bc8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt @@ -1,8 +1,16 @@ package com.readrops.app.compose.timelime -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import cafe.adriel.voyager.androidx.AndroidScreen +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import org.koin.androidx.compose.getViewModel @@ -11,9 +19,34 @@ class TimelineScreen : AndroidScreen() { @Composable override fun Content() { val viewModel = getViewModel() + val state by viewModel.timelineState.collectAsState() + val isRefreshing by viewModel.isRefreshing.collectAsState() - Column { - TimelineItem() + val swipeToRefreshState = rememberSwipeRefreshState(isRefreshing) + + SwipeRefresh( + state = swipeToRefreshState, + onRefresh = { + viewModel.refreshTimeline() + }, + modifier = Modifier.fillMaxSize() + ) { + when (state) { + is TimelineState.ErrorState -> { + Text(text = "error") + } + + TimelineState.InitialState -> {} + is TimelineState.LoadedState -> { + LazyColumn { + items( + items = (state as TimelineState.LoadedState).items + ) { + TimelineItem() + } + } + } + } } } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 0a7f73ab..b310318d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -1,12 +1,52 @@ package com.readrops.app.compose.timelime import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.app.compose.repositories.BaseRepository import com.readrops.db.Database +import com.readrops.db.entities.Item +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.launch class TimelineViewModel( - val database: Database + private val database: Database, + private val repository: BaseRepository, ) : ViewModel() { + private val _timelineState = MutableStateFlow(TimelineState.InitialState) + val timelineState = _timelineState.asStateFlow() + private var _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + init { + viewModelScope.launch(context = Dispatchers.IO) { + database.newItemDao().selectAll() + .catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) } + .collect { + _timelineState.value = TimelineState.LoadedState(it) + } + } + } + + fun refreshTimeline() { + _isRefreshing.value = true + viewModelScope.launch(context = Dispatchers.IO) { + repository.synchronize(null) { + + } + + _isRefreshing.value = false + } + } +} + +sealed class TimelineState { + object InitialState : TimelineState() + data class ErrorState(val exception: Exception) : TimelineState() + data class LoadedState(val items: List) : TimelineState() +} -} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt index dc43188b..561c874c 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -1,10 +1,13 @@ package com.readrops.db.dao.newdao import androidx.room.Dao +import androidx.room.Query import com.readrops.db.entities.Item +import kotlinx.coroutines.flow.Flow @Dao abstract class NewItemDao : NewBaseDao { - + @Query("Select * From Item") + abstract fun selectAll(): Flow> } \ No newline at end of file From d76200e292a0609b4fd2e55117a4d93cb8770d97 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 27 Jul 2023 14:53:22 +0200 Subject: [PATCH 16/95] Fix pull to refresh not reachable when no item is displayted --- .../app/compose/timelime/TimelineScreen.kt | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt index 56060bc8..18589980 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt @@ -1,12 +1,19 @@ package com.readrops.app.compose.timelime +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.androidx.AndroidScreen import com.google.accompanist.swiperefresh.SwipeRefresh @@ -29,24 +36,43 @@ class TimelineScreen : AndroidScreen() { onRefresh = { viewModel.refreshTimeline() }, - modifier = Modifier.fillMaxSize() ) { when (state) { - is TimelineState.ErrorState -> { - Text(text = "error") - } - - TimelineState.InitialState -> {} is TimelineState.LoadedState -> { - LazyColumn { - items( - items = (state as TimelineState.LoadedState).items - ) { - TimelineItem() + val items = (state as TimelineState.LoadedState).items + + if (items.isNotEmpty()) { + LazyColumn { + items( + items = (state as TimelineState.LoadedState).items + ) { + TimelineItem() + } } + } else { + NoItemPlaceholder() } } + else -> { + NoItemPlaceholder() + } } } } +} +@Composable +fun NoItemPlaceholder() { + val scrollState = rememberScrollState() + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + .verticalScroll(scrollState) + ) { + Text( + text = "No item", + style = MaterialTheme.typography.displayMedium + ) + } } \ No newline at end of file From 5c7f86b170fb558a89339b178c682fe24bf1444a Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 27 Jul 2023 17:37:36 +0200 Subject: [PATCH 17/95] Add basic feed list in FeedScreen --- appcompose/build.gradle | 2 + .../readrops/app/compose/ComposeAppModule.kt | 4 +- .../com/readrops/app/compose/MainActivity.kt | 4 +- .../readrops/app/compose/feeds/FeedItem.kt | 36 ++++++++++++++++ .../feeds/{FeedsScreen.kt => FeedScreen.kt} | 39 ++++++++++++++++-- .../app/compose/feeds/FeedViewModel.kt | 41 +++++++++++++++++++ .../app/compose/feeds/FeedsViewModel.kt | 18 -------- .../com/readrops/db/dao/newdao/NewFeedDao.kt | 4 ++ 8 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt rename appcompose/src/main/java/com/readrops/app/compose/feeds/{FeedsScreen.kt => FeedScreen.kt} (58%) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt delete mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 2fb960d3..9f1b1783 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -97,4 +97,6 @@ dependencies { androidTestImplementation "io.insert-koin:koin-test:$rootProject.ext.koin_version" androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' + + implementation "com.github.skydoves:landscapist-coil:2.2.2" } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index 685890c3..e8db3def 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -1,6 +1,6 @@ package com.readrops.app.compose -import com.readrops.app.compose.feeds.FeedsViewModel +import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.repositories.BaseRepository import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.app.compose.timelime.TimelineViewModel @@ -13,7 +13,7 @@ val composeAppModule = module { viewModel { TimelineViewModel(get(), get()) } - viewModel { FeedsViewModel(get()) } + viewModel { FeedViewModel(get(), get()) } // repositories diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 1ed1d3c5..6edcac87 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator import com.readrops.app.compose.account.AccountScreen -import com.readrops.app.compose.feeds.FeedsScreen +import com.readrops.app.compose.feeds.FeedScreen import com.readrops.app.compose.more.MoreScreen import com.readrops.app.compose.timelime.TimelineScreen @@ -44,7 +44,7 @@ class MainActivity : ComponentActivity() { NavigationBarItem( selected = false, - onClick = { navigator.push(FeedsScreen()) }, + onClick = { navigator.push(FeedScreen()) }, icon = { Icon( painter = painterResource(R.drawable.ic_rss_feed_grey), diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt new file mode 100644 index 00000000..d4ecfe5f --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt @@ -0,0 +1,36 @@ +package com.readrops.app.compose.feeds + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.request.ImageRequest +import com.readrops.db.entities.Feed +import com.skydoves.landscapist.coil.CoilImage + +@Composable +fun FeedItem(feed: Feed) { + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + CoilImage(imageRequest = { + ImageRequest.Builder(context) + .data(feed.url) + .build() + }) + + Text( + text = feed.name!!, + style = MaterialTheme.typography.headlineSmall + ) + } +} diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreen.kt similarity index 58% rename from appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreen.kt index 48a7ba2d..ca8f2548 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreen.kt @@ -3,11 +3,14 @@ package com.readrops.app.compose.feeds import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -18,13 +21,15 @@ import androidx.compose.ui.unit.dp import cafe.adriel.voyager.androidx.AndroidScreen import org.koin.androidx.compose.getViewModel -class FeedsScreen : AndroidScreen() { +class FeedScreen : AndroidScreen() { @Composable override fun Content() { - val viewModel = getViewModel() + val viewModel = getViewModel() var showDialog by remember { mutableStateOf(false) } + val state by viewModel.feedsState.collectAsState() + if (showDialog) { AddFeedDialog( onDismiss = { showDialog = false }, @@ -38,10 +43,36 @@ class FeedsScreen : AndroidScreen() { Box( modifier = Modifier.fillMaxSize() ) { + when (state) { + is FeedsState.LoadedState -> { + val feeds = (state as FeedsState.LoadedState).feeds + + if (feeds.isNotEmpty()) { + LazyColumn { + items( + items = feeds + ) { feed -> + FeedItem( + feed = feed, + ) + } + } + } + } + is FeedsState.ErrorState -> { + + } + else -> { + + } + } + + + FloatingActionButton( modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), + .align(Alignment.BottomEnd) + .padding(16.dp), onClick = { showDialog = true } ) { Icon( diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt new file mode 100644 index 00000000..14b26400 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -0,0 +1,41 @@ +package com.readrops.app.compose.feeds + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.app.compose.repositories.BaseRepository +import com.readrops.db.Database +import com.readrops.db.entities.Feed +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.launch + +class FeedViewModel( + private val database: Database, + private val repository: BaseRepository, +) : ViewModel() { + + private val _feedsState = MutableStateFlow(FeedsState.InitialState) + val feedsState = _feedsState.asStateFlow() + + init { + viewModelScope.launch(context = Dispatchers.IO) { + database.newFeedDao().selectFeeds() + .catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) } + .collect { _feedsState.value = FeedsState.LoadedState(it) } + } + } + + fun insertFeed(url: String) { + viewModelScope.launch(context = Dispatchers.IO) { + repository.insertNewFeeds(listOf(url)) + } + } +} + +sealed class FeedsState { + object InitialState : FeedsState() + data class ErrorState(val exception: Exception) : FeedsState() + data class LoadedState(val feeds: List) : FeedsState() +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt deleted file mode 100644 index 6f626a68..00000000 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedsViewModel.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.readrops.app.compose.feeds - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.readrops.app.compose.repositories.BaseRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch - -class FeedsViewModel( - private val repository: BaseRepository, -) : ViewModel() { - - fun insertFeed(url: String) { - viewModelScope.launch(context = Dispatchers.IO) { - repository.insertNewFeeds(listOf(url)) - } - } -} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index b5d11f56..471fefca 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -3,10 +3,14 @@ package com.readrops.db.dao.newdao import androidx.room.Dao import androidx.room.Query import com.readrops.db.entities.Feed +import kotlinx.coroutines.flow.Flow @Dao abstract class NewFeedDao : NewBaseDao { + @Query("Select * From Feed") + abstract fun selectFeeds(): Flow> + @Query("Select * from Feed Where account_id = :accountId order by name ASC") abstract suspend fun selectFeeds(accountId: Int): List From d673616bb48f88fb6a3e90a1cfd1180e0484afe7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 27 Jul 2023 22:55:28 +0200 Subject: [PATCH 18/95] Add OkHttp interceptor to raise exceptions for non-successfull requests --- .../main/java/com/readrops/api/ApiModule.kt | 4 ++ .../readrops/api/utils/ErrorInterceptor.kt | 19 +++++++++ .../api/utils/exceptions/HttpException.kt | 13 +++++++ .../api/utils/ErrorInterceptorTest.kt | 39 +++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 api/src/main/java/com/readrops/api/utils/ErrorInterceptor.kt create mode 100644 api/src/main/java/com/readrops/api/utils/exceptions/HttpException.kt create mode 100644 api/src/test/java/com/readrops/api/utils/ErrorInterceptorTest.kt diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt index 2c43dac8..3d1258eb 100644 --- a/api/src/main/java/com/readrops/api/ApiModule.kt +++ b/api/src/main/java/com/readrops/api/ApiModule.kt @@ -11,6 +11,7 @@ import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter import com.readrops.api.utils.AuthInterceptor +import com.readrops.api.utils.ErrorInterceptor import com.readrops.db.entities.Item import com.squareup.moshi.Moshi import com.squareup.moshi.Types @@ -29,12 +30,15 @@ val apiModule = module { .callTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.HOURS) .addInterceptor(get()) + .addInterceptor(get()) //.addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler")) .build() } single { AuthInterceptor() } + single { ErrorInterceptor() } + single { LocalRSSDataSource(get()) } //region freshrss diff --git a/api/src/main/java/com/readrops/api/utils/ErrorInterceptor.kt b/api/src/main/java/com/readrops/api/utils/ErrorInterceptor.kt new file mode 100644 index 00000000..a0b8bb94 --- /dev/null +++ b/api/src/main/java/com/readrops/api/utils/ErrorInterceptor.kt @@ -0,0 +1,19 @@ +package com.readrops.api.utils + +import com.readrops.api.utils.exceptions.HttpException +import okhttp3.Interceptor +import okhttp3.Response + +class ErrorInterceptor() : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + if (!response.isSuccessful) { + throw HttpException(response) + } + + return response + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/utils/exceptions/HttpException.kt b/api/src/main/java/com/readrops/api/utils/exceptions/HttpException.kt new file mode 100644 index 00000000..b3aa3062 --- /dev/null +++ b/api/src/main/java/com/readrops/api/utils/exceptions/HttpException.kt @@ -0,0 +1,13 @@ +package com.readrops.api.utils.exceptions + +import okhttp3.Response + + +class HttpException(val response: Response) : Exception() { + + val code: Int + get() = response.code + + override val message: String + get() = "HTTP " + response.code + " " + response.message +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/utils/ErrorInterceptorTest.kt b/api/src/test/java/com/readrops/api/utils/ErrorInterceptorTest.kt new file mode 100644 index 00000000..22f243c5 --- /dev/null +++ b/api/src/test/java/com/readrops/api/utils/ErrorInterceptorTest.kt @@ -0,0 +1,39 @@ +package com.readrops.api.utils + +import com.readrops.api.utils.exceptions.HttpException +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.net.HttpURLConnection + +class ErrorInterceptorTest { + + private val interceptor = ErrorInterceptor() + private val server = MockWebServer() + private lateinit var client: OkHttpClient + + @Before + fun before() { + client = OkHttpClient.Builder() + .addInterceptor(interceptor) + .build() + server.start(8080) + } + + @After + fun tearDown() { + server.close() + } + + @Test(expected = HttpException::class) + fun interceptorTest() { + server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)) + + client.newCall(Request.Builder().url(server.url("/url")).build()).execute() + //val request = server.takeRequest() + } +} \ No newline at end of file From 35ad5dfbc439a94942a22f12daa4a420128d3e8d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 1 Aug 2023 21:40:47 +0200 Subject: [PATCH 19/95] Add FreshRSSDataSource kotlin migration with tests This new data source is intended to replace the old java one --- .../freshrss/NewFreshRSSDataSource.kt | 121 ++++++++ .../services/freshrss/NewFreshRSSService.kt | 73 +++++ .../freshrss/FreshRSSDataSourceTest.kt | 260 ++++++++++++++++++ .../services/freshrss/login_response_body | 3 + .../freshrss/writetoken_response_body | 1 + 5 files changed, 458 insertions(+) create mode 100644 api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSDataSource.kt create mode 100644 api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSService.kt create mode 100644 api/src/test/java/com/readrops/api/services/freshrss/FreshRSSDataSourceTest.kt create mode 100644 api/src/test/resources/services/freshrss/login_response_body create mode 100644 api/src/test/resources/services/freshrss/writetoken_response_body diff --git a/api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSDataSource.kt b/api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSDataSource.kt new file mode 100644 index 00000000..9f036827 --- /dev/null +++ b/api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSDataSource.kt @@ -0,0 +1,121 @@ +package com.readrops.api.services.freshrss + +import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo +import com.readrops.db.entities.Item +import okhttp3.MultipartBody +import java.io.StringReader +import java.util.Properties + +class NewFreshRSSDataSource(private val service: NewFreshRSSService) { + + suspend fun login(login: String, password: String): String { + val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("Email", login) + .addFormDataPart("Passwd", password) + .build() + + val response = service.login(requestBody) + + val properties = Properties() + properties.load(StringReader(response.string())) + + response.close() + return properties.getProperty("Auth") + } + + suspend fun getWriteToken(): String = service.getWriteToken().string() + + suspend fun getUserInfo(): FreshRSSUserInfo = service.userInfo() + + suspend fun sync() { + + } + + suspend fun getFolders() = service.getFolders() + + suspend fun getFeeds() = service.getFeeds() + + suspend fun getItems(excludeTargets: List, max: Int, lastModified: Long): List { + return service.getItems(excludeTargets, max, lastModified) + } + + suspend fun getStarredItems(max: Int) = service.getStarredItems(max) + + suspend fun getItemsIds(excludeTarget: String, includeTarget: String, max: Int): List { + return service.getItemsIds(excludeTarget, includeTarget, max) + } + + private suspend fun setItemsReadState(read: Boolean, itemIds: List, token: String) { + return if (read) { + service.setItemsState(token, GOOGLE_READ, null, itemIds) + } else { + service.setItemsState(token, null, GOOGLE_READ, itemIds) + } + } + + private suspend fun setItemStarState(starred: Boolean, itemIds: List, token: String) { + return if (starred) { + service.setItemsState(token, GOOGLE_STARRED, null, itemIds) + } else { + service.setItemsState(token, null, GOOGLE_STARRED, itemIds) + } + } + + suspend fun createFeed(token: String, feedUrl: String) { + service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "subscribe"); + } + + suspend fun deleteFeed(token: String, feedUrl: String) { + service.createOrDeleteFeed(token, FEED_PREFIX + feedUrl, "unsubscribe") + } + + suspend fun updateFeed(token: String, feedUrl: String, title: String, folderId: String) { + service.updateFeed(token, FEED_PREFIX + feedUrl, title, folderId, "edit") + } + + suspend fun createFolder(token: String, tagName: String) { + service.createFolder(token, "$FOLDER_PREFIX$tagName") + } + + suspend fun updateFolder(token: String, folderId: String, name: String) { + service.updateFolder(token, folderId, "$FOLDER_PREFIX$name") + } + + suspend fun deleteFolder(token: String, folderId: String) { + service.deleteFolder(token, folderId) + } + + suspend fun setItemsReadState(syncData: FreshRSSSyncData, token: String) { + if (syncData.readItemsIds.isNotEmpty()) { + setItemsReadState(true, syncData.readItemsIds, token) + } + + if (syncData.unreadItemsIds.isNotEmpty()) { + setItemsReadState(false, syncData.unreadItemsIds, token) + } + } + + suspend fun setItemsStarState(syncData: FreshRSSSyncData, token: String) { + if (syncData.starredItemsIds.isNotEmpty()) { + setItemStarState(true, syncData.starredItemsIds, token) + } + + if (syncData.unstarredItemsIds.isNotEmpty()) { + setItemStarState(false, syncData.unstarredItemsIds, token) + } + } + + companion object { + private const val MAX_ITEMS = 2500 + private const val MAX_STARRED_ITEMS = 1000 + + const val GOOGLE_READ = "user/-/state/com.google/read" + const val GOOGLE_UNREAD = "user/-/state/com.google/unread" + const val GOOGLE_STARRED = "user/-/state/com.google/starred" + const val GOOGLE_READING_LIST = "user/-/state/com.google/reading-list" + + const val FEED_PREFIX = "feed/" + const val FOLDER_PREFIX = "user/-/label/" + } +} \ No newline at end of file diff --git a/api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSService.kt b/api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSService.kt new file mode 100644 index 00000000..243f8d44 --- /dev/null +++ b/api/src/main/java/com/readrops/api/services/freshrss/NewFreshRSSService.kt @@ -0,0 +1,73 @@ +package com.readrops.api.services.freshrss + +import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder +import com.readrops.db.entities.Item +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface NewFreshRSSService { + + @POST("accounts/ClientLogin") + suspend fun login(@Body body: RequestBody?): ResponseBody + + @GET("reader/api/0/token") + suspend fun getWriteToken(): ResponseBody + + @GET("reader/api/0/user-info") + suspend fun userInfo(): FreshRSSUserInfo + + @GET("reader/api/0/subscription/list?output=json") + suspend fun getFeeds(): List + + @GET("reader/api/0/tag/list?output=json") + suspend fun getFolders(): List + + @GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list") + suspend fun getItems(@Query("xt") excludeTarget: List?, @Query("n") max: Int, + @Query("ot") lastModified: Long?): List + + @GET("reader/api/0/stream/contents/user/-/state/com.google/starred") + suspend fun getStarredItems(@Query("n") max: Int): List + + @GET("reader/api/0/stream/items/ids") + suspend fun getItemsIds(@Query("xt") excludeTarget: String?, @Query("s") includeTarget: String?, + @Query("n") max: Int): List + + @FormUrlEncoded + @POST("reader/api/0/edit-tag") + suspend fun setItemsState(@Field("T") token: String, @Field("a") addAction: String?, + @Field("r") removeAction: String?, @Field("i") itemIds: List) + + @FormUrlEncoded + @POST("reader/api/0/subscription/edit") + suspend fun createOrDeleteFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("ac") action: String) + + @FormUrlEncoded + @POST("reader/api/0/subscription/edit") + suspend fun updateFeed(@Field("T") token: String, @Field("s") feedUrl: String, @Field("t") title: String, + @Field("a") folderId: String, @Field("ac") action: String) + + @FormUrlEncoded + @POST("reader/api/0/edit-tag") + suspend fun createFolder(@Field("T") token: String, @Field("a") tagName: String) + + @FormUrlEncoded + @POST("reader/api/0/rename-tag") + suspend fun updateFolder(@Field("T") token: String, @Field("s") folderId: String, @Field("dest") newFolderId: String) + + @FormUrlEncoded + @POST("reader/api/0/disable-tag") + suspend fun deleteFolder(@Field("T") token: String, @Field("s") folderId: String) + + companion object { + const val END_POINT = "/api/greader.php/" + } +} \ No newline at end of file diff --git a/api/src/test/java/com/readrops/api/services/freshrss/FreshRSSDataSourceTest.kt b/api/src/test/java/com/readrops/api/services/freshrss/FreshRSSDataSourceTest.kt new file mode 100644 index 00000000..1cc882f1 --- /dev/null +++ b/api/src/test/java/com/readrops/api/services/freshrss/FreshRSSDataSourceTest.kt @@ -0,0 +1,260 @@ +package com.readrops.api.services.freshrss + +import com.readrops.api.TestUtils +import com.readrops.api.apiModule +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.KoinTestRule +import org.koin.test.get +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import java.net.HttpURLConnection +import java.net.URLEncoder +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FreshRSSDataSourceTest : KoinTest { + + private lateinit var freshRSSDataSource: NewFreshRSSDataSource + private val mockServer = MockWebServer() + + @get:Rule + val koinTestRule = KoinTestRule.create { + modules(apiModule, module { + single { + Retrofit.Builder() + .baseUrl("http://localhost:8080/") + .client(get()) + .addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi")))) + .build() + .create(NewFreshRSSService::class.java) + } + }) + } + + @Before + fun before() { + mockServer.start(8080) + freshRSSDataSource = NewFreshRSSDataSource(get()) + } + + @After + fun tearDown() { + mockServer.shutdown() + } + + @Test + fun loginTest() { + runBlocking { + val responseBody = TestUtils.loadResource("services/freshrss/login_response_body") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(responseBody))) + + val authString = freshRSSDataSource.login("Login", "Password") + assertEquals("login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a", authString) + + val request = mockServer.takeRequest() + val requestBody = request.body.readUtf8() + + assertTrue { + requestBody.contains("name=\"Email\"") && requestBody.contains("Login") + } + + assertTrue { + requestBody.contains("name=\"Passwd\"") && requestBody.contains("Password") + } + } + } + + @Test + fun writeTokenTest() = runBlocking { + val responseBody = TestUtils.loadResource("services/freshrss/writetoken_response_body") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(responseBody))) + + val writeToken = freshRSSDataSource.getWriteToken() + + assertEquals("PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg", writeToken) + } + + @Test + fun userInfoTest() = runBlocking { + + } + + @Test + fun foldersTest() = runBlocking { + val stream = TestUtils.loadResource("services/freshrss/adapters/folders.json") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(stream))) + + val folders = freshRSSDataSource.getFolders() + assertTrue { folders.size == 1 } + } + + @Test + fun feedsTest() = runBlocking { + val stream = TestUtils.loadResource("services/freshrss/adapters/feeds.json") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(stream))) + + val feeds = freshRSSDataSource.getFeeds() + assertTrue { feeds.size == 1 } + } + + @Test + fun itemsTest() = runBlocking { + val stream = TestUtils.loadResource("services/freshrss/adapters/items.json") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(stream))) + + val items = freshRSSDataSource.getItems(listOf(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_STARRED), 100, 21343321321321) + assertTrue { items.size == 2 } + + val request = mockServer.takeRequest() + + with(request.requestUrl!!) { + assertEquals(listOf(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_STARRED), queryParameterValues("xt")) + assertEquals("100", queryParameter("n")) + assertEquals("21343321321321", queryParameter("ot")) + + } + } + + @Test + fun starredItemsTest() = runBlocking { + val stream = TestUtils.loadResource("services/freshrss/adapters/items.json") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(stream))) + + val items = freshRSSDataSource.getStarredItems(100) + assertTrue { items.size == 2 } + + val request = mockServer.takeRequest() + + assertEquals("100", request.requestUrl!!.queryParameter("n")) + } + + @Test + fun getItemsIdsTest() = runBlocking { + val stream = TestUtils.loadResource("services/freshrss/adapters/items_starred_ids.json") + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Buffer().readFrom(stream))) + + val ids = freshRSSDataSource.getItemsIds(NewFreshRSSDataSource.GOOGLE_READ, NewFreshRSSDataSource.GOOGLE_READING_LIST, 100) + assertTrue { ids.size == 5 } + + val request = mockServer.takeRequest() + with(request.requestUrl!!) { + assertEquals(NewFreshRSSDataSource.GOOGLE_READ, queryParameter("xt")) + assertEquals(NewFreshRSSDataSource.GOOGLE_READING_LIST, queryParameter("s")) + assertEquals("100", queryParameter("n")) + } + } + + @Test + fun createFeedTest() = runBlocking { + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK)) + + freshRSSDataSource.createFeed("token", "https://feed.url") + val request = mockServer.takeRequest() + + with(request.body.readUtf8()) { + assertTrue { contains("T=token") } + assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") } + assertTrue { contains("ac=subscribe") } + } + } + + @Test + fun deleteFeedTest() = runBlocking { + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK)) + + freshRSSDataSource.deleteFeed("token", "https://feed.url") + val request = mockServer.takeRequest() + + with(request.body.readUtf8()) { + assertTrue { contains("T=token") } + assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") } + assertTrue { contains("ac=unsubscribe") } + } + } + + @Test + fun updateFeedTest() = runBlocking { + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK)) + + freshRSSDataSource.updateFeed("token", "https://feed.url", "title", "folderId") + val request = mockServer.takeRequest() + + with(request.body.readUtf8()) { + assertTrue { contains("T=token") } + assertTrue { contains("s=${URLEncoder.encode("${NewFreshRSSDataSource.FEED_PREFIX}https://feed.url", "UTF-8")}") } + assertTrue { contains("t=title") } + assertTrue { contains("a=folderId") } + assertTrue { contains("ac=edit") } + } + } + + @Test + fun createFolderTest() = runBlocking { + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK)) + + freshRSSDataSource.createFolder("token", "folder") + val request = mockServer.takeRequest() + + with(request.body.readUtf8()) { + assertTrue { contains("T=token") } + assertTrue { contains("a=${URLEncoder.encode("${NewFreshRSSDataSource.FOLDER_PREFIX}folder", "UTF-8")}") } + } + } + + @Test + fun updateFolderTest() = runBlocking { + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK)) + + freshRSSDataSource.updateFolder("token", "folderId", "folder") + val request = mockServer.takeRequest() + + with(request.body.readUtf8()) { + assertTrue { contains("T=token") } + assertTrue { contains("s=folderId") } + assertTrue { contains("dest=${URLEncoder.encode("${NewFreshRSSDataSource.FOLDER_PREFIX}folder", "UTF-8")}") } + } + } + + @Test + fun deleteFolderTest() = runBlocking { + mockServer.enqueue(MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK)) + + freshRSSDataSource.deleteFolder("token", "folderId") + val request = mockServer.takeRequest() + + with(request.body.readUtf8()) { + assertTrue { contains("T=token") } + assertTrue { contains("s=folderId") } + } + } +} \ No newline at end of file diff --git a/api/src/test/resources/services/freshrss/login_response_body b/api/src/test/resources/services/freshrss/login_response_body new file mode 100644 index 00000000..e6bf504c --- /dev/null +++ b/api/src/test/resources/services/freshrss/login_response_body @@ -0,0 +1,3 @@ +SID=login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a +LSID=null +Auth=login/p1f8vmzid4hzzxf31mgx50gt8pnremgp4z8xe44a diff --git a/api/src/test/resources/services/freshrss/writetoken_response_body b/api/src/test/resources/services/freshrss/writetoken_response_body new file mode 100644 index 00000000..42765922 --- /dev/null +++ b/api/src/test/resources/services/freshrss/writetoken_response_body @@ -0,0 +1 @@ +PMvYZHrnC57cyPLzxFvQmJEGN6KvNmkHCmHQPKG5eznWMXriq13H1nQZg \ No newline at end of file From 469721931297fcf05c17dec4bd8a62567a23c6a2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 1 Aug 2023 21:54:18 +0200 Subject: [PATCH 20/95] Use HttpException in LocalRSSDataSource --- .../java/com/readrops/api/localfeed/LocalRSSDataSource.kt | 5 +++-- .../com/readrops/api/localfeed/LocalRSSDataSourceTest.kt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 4f536dda..867f17bc 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -7,6 +7,7 @@ import com.gitlab.mvysny.konsumexml.konsumeXml import com.readrops.api.localfeed.json.JSONFeedAdapter import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.AuthInterceptor +import com.readrops.api.utils.exceptions.HttpException import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import com.readrops.db.entities.Feed @@ -32,7 +33,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent { * @param headers request headers * @return a Feed object with its items */ - @Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class) + @Throws(ParseException::class, UnknownFormatException::class, HttpException::class, IOException::class) @WorkerThread fun queryRSSResource(url: String, headers: Headers?): Pair>? { get().credentials = null @@ -46,7 +47,7 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent { pair } response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null - else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}") + else -> throw HttpException(response) } } diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt index 3c33b932..48ed7a99 100644 --- a/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt +++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt @@ -5,6 +5,7 @@ import com.readrops.api.TestUtils import com.readrops.api.apiModule import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.AuthInterceptor +import com.readrops.api.utils.exceptions.HttpException import com.readrops.api.utils.exceptions.ParseException import com.readrops.api.utils.exceptions.UnknownFormatException import junit.framework.TestCase.* @@ -149,7 +150,7 @@ class LocalRSSDataSourceTest : KoinTest { assertNull(pair) } - @Test(expected = NetworkErrorException::class) + @Test(expected = HttpException::class) fun response404Test() { mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)) From 0942dbe0539b1908ac751bb2c661cd8335cef1c1 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 2 Aug 2023 21:28:58 +0200 Subject: [PATCH 21/95] Improve navigation with voyager --- appcompose/build.gradle | 1 + .../com/readrops/app/compose/MainActivity.kt | 79 +++----------- .../{AccountScreen.kt => AccountTab.kt} | 12 ++- .../feeds/{FeedScreen.kt => FeedTab.kt} | 12 ++- .../readrops/app/compose/home/HomeScreen.kt | 102 ++++++++++++++++++ .../readrops/app/compose/item/ItemScreen.kt | 13 +++ .../more/{MoreScreen.kt => MoreTab.kt} | 13 ++- .../app/compose/timelime/TimelineItem.kt | 6 +- .../{TimelineScreen.kt => TimelineTab.kt} | 31 ++++-- 9 files changed, 191 insertions(+), 78 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/account/{AccountScreen.kt => AccountTab.kt} (50%) rename appcompose/src/main/java/com/readrops/app/compose/feeds/{FeedScreen.kt => FeedTab.kt} (90%) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt rename appcompose/src/main/java/com/readrops/app/compose/more/{MoreScreen.kt => MoreTab.kt} (52%) rename appcompose/src/main/java/com/readrops/app/compose/timelime/{TimelineScreen.kt => TimelineTab.kt} (71%) diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 9f1b1783..25b7fe38 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -79,6 +79,7 @@ dependencies { implementation "cafe.adriel.voyager:voyager-tab-navigator:$voyager" implementation "cafe.adriel.voyager:voyager-androidx:$voyager" implementation "cafe.adriel.voyager:voyager-koin:$voyager" + implementation "cafe.adriel.voyager:voyager-transitions:$voyager" debugImplementation "androidx.compose.ui:ui-tooling:1.4.3" implementation "androidx.compose.ui:ui-tooling-preview:1.4.3" diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 6edcac87..9a86bf6b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -3,6 +3,7 @@ package com.readrops.app.compose import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -13,79 +14,27 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator -import com.readrops.app.compose.account.AccountScreen -import com.readrops.app.compose.feeds.FeedScreen -import com.readrops.app.compose.more.MoreScreen -import com.readrops.app.compose.timelime.TimelineScreen +import cafe.adriel.voyager.navigator.tab.TabNavigator +import cafe.adriel.voyager.transitions.FadeTransition +import cafe.adriel.voyager.transitions.SlideTransition +import com.readrops.app.compose.account.AccountTab +import com.readrops.app.compose.feeds.FeedTab +import com.readrops.app.compose.home.HomeScreen +import com.readrops.app.compose.more.MoreTab +import com.readrops.app.compose.timelime.TimelineTab class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3Api::class) + @OptIn(ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ReadropsTheme { - Navigator(screen = TimelineScreen()) { navigator -> - Scaffold( - bottomBar = { - BottomAppBar { - NavigationBarItem( - selected = false, - onClick = { navigator.push(TimelineScreen()) }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_timeline), - contentDescription = null - ) - }, - label = { Text("Timeline") } - ) - - NavigationBarItem( - selected = false, - onClick = { navigator.push(FeedScreen()) }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_rss_feed_grey), - contentDescription = null - ) - }, - label = { Text("Feeds") } - ) - - NavigationBarItem( - selected = false, - onClick = { navigator.push(AccountScreen()) }, - icon = { - Icon( - imageVector = Icons.Default.AccountBox, - contentDescription = null, - ) - }, - label = { Text("Account") } - ) - - NavigationBarItem( - selected = false, - onClick = { navigator.push(MoreScreen()) }, - icon = { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null, - ) - }, - label = { Text("More") } - ) - } - }, - ) { paddingValues -> - Box( - modifier = Modifier.padding(paddingValues) - ) { - CurrentScreen() - } - } + Navigator( + screen = HomeScreen() + ) { navigator -> + FadeTransition(navigator) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt similarity index 50% rename from appcompose/src/main/java/com/readrops/app/compose/account/AccountScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index d5250459..ced0aba9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -3,9 +3,17 @@ package com.readrops.app.compose.account import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions -class AccountScreen : AndroidScreen() { +object AccountTab : Tab { + + override val options: TabOptions + @Composable + get() = TabOptions( + index = 3u, + title = "Account" + ) @Composable override fun Content() { diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt similarity index 90% rename from appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index ca8f2548..b0e8173a 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -18,10 +18,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions import org.koin.androidx.compose.getViewModel -class FeedScreen : AndroidScreen() { +object FeedTab : Tab { + + override val options: TabOptions + @Composable + get() = TabOptions( + index = 2u, + title = "Feeds" + ) @Composable override fun Content() { diff --git a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt new file mode 100644 index 00000000..a6600451 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt @@ -0,0 +1,102 @@ +package com.readrops.app.compose.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.CurrentScreen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.tab.CurrentTab +import cafe.adriel.voyager.navigator.tab.TabNavigator +import com.readrops.app.compose.R +import com.readrops.app.compose.account.AccountTab +import com.readrops.app.compose.feeds.FeedTab +import com.readrops.app.compose.more.MoreTab +import com.readrops.app.compose.timelime.TimelineTab + +class HomeScreen : AndroidScreen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + TabNavigator( + tab = TimelineTab + ) { tabNavigator -> + CompositionLocalProvider(LocalNavigator provides navigator) { + Scaffold( + bottomBar = { + BottomAppBar { + NavigationBarItem( + selected = false, + onClick = { tabNavigator.current = TimelineTab }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_timeline), + contentDescription = null + ) + }, + label = { Text("Timeline") } + ) + + NavigationBarItem( + selected = false, + onClick = { tabNavigator.current = FeedTab }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_rss_feed_grey), + contentDescription = null + ) + }, + label = { Text("Feeds") } + ) + + NavigationBarItem( + selected = false, + onClick = { tabNavigator.current = AccountTab }, + icon = { + Icon( + imageVector = Icons.Default.AccountBox, + contentDescription = null, + ) + }, + label = { Text("Account") } + ) + + NavigationBarItem( + selected = false, + onClick = { tabNavigator.current = MoreTab }, + icon = { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + ) + }, + label = { Text("More") } + ) + } + }, + ) { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues) + ) { + CurrentTab() + } + } + } + + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt new file mode 100644 index 00000000..17eff549 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/item/ItemScreen.kt @@ -0,0 +1,13 @@ +package com.readrops.app.compose.item + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.androidx.AndroidScreen + +class ItemScreen : AndroidScreen() { + + @Composable + override fun Content() { + Text(text ="item screen") + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/more/MoreScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt similarity index 52% rename from appcompose/src/main/java/com/readrops/app/compose/more/MoreScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt index d3d7832e..e7d915f1 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/more/MoreScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt @@ -3,9 +3,18 @@ package com.readrops.app.compose.more import androidx.compose.foundation.layout.Column import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions + +object MoreTab : Tab { + + override val options: TabOptions + @Composable + get() = TabOptions( + index = 4u, + title = "More" + ) -class MoreScreen : AndroidScreen() { @Composable override fun Content() { diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index 55fe0f2e..f93eda0f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -1,6 +1,7 @@ package com.readrops.app.compose.timelime import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Card import androidx.compose.material3.Text @@ -13,11 +14,14 @@ import androidx.compose.ui.unit.dp @Preview @Composable -fun TimelineItem() { +fun TimelineItem( + onClick: () -> Unit, +) { Card( // elevation = 4.card, modifier = Modifier.background(Color.White) .padding(8.dp) + .clickable { onClick() } ) { Column( modifier = Modifier diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt similarity index 71% rename from appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt rename to appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 18589980..63d11702 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -1,6 +1,5 @@ package com.readrops.app.compose.timelime -import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -15,13 +14,28 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.navigator.tab.LocalTabNavigator +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import com.readrops.app.compose.item.ItemScreen import org.koin.androidx.compose.getViewModel -class TimelineScreen : AndroidScreen() { +object TimelineTab : Tab { + + override val options: TabOptions + @Composable + get() { + return TabOptions( + index = 1u, + title = "Timeline", + ) + } @Composable override fun Content() { @@ -31,6 +45,8 @@ class TimelineScreen : AndroidScreen() { val swipeToRefreshState = rememberSwipeRefreshState(isRefreshing) + val navigator = LocalNavigator.currentOrThrow + SwipeRefresh( state = swipeToRefreshState, onRefresh = { @@ -44,9 +60,11 @@ class TimelineScreen : AndroidScreen() { if (items.isNotEmpty()) { LazyColumn { items( - items = (state as TimelineState.LoadedState).items + items = items ) { - TimelineItem() + TimelineItem( + onClick = { navigator.push(ItemScreen()) } + ) } } } else { @@ -67,7 +85,8 @@ fun NoItemPlaceholder() { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .verticalScroll(scrollState) ) { Text( From ad0ffe55c9edfa6b0d4e762e94b14ce41de85e33 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 3 Aug 2023 14:57:04 +0200 Subject: [PATCH 22/95] Add initial account creation path --- .../readrops/app/compose/ComposeAppModule.kt | 3 +++ .../com/readrops/app/compose/MainActivity.kt | 8 ++++++- .../compose/account/AccountSelectionScreen.kt | 17 +++++++++++++++ .../app/compose/account/AccountViewModel.kt | 21 +++++++++++++++++++ db/src/main/java/com/readrops/db/Database.kt | 3 +++ .../readrops/db/dao/newdao/NewAccountDao.kt | 12 +++++++++++ 6 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt create mode 100644 db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index e8db3def..1503d674 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose +import com.readrops.app.compose.account.AccountViewModel import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.repositories.BaseRepository import com.readrops.app.compose.repositories.LocalRSSRepository @@ -15,6 +16,8 @@ val composeAppModule = module { viewModel { FeedViewModel(get(), get()) } + viewModel { AccountViewModel(get()) } + // repositories single { LocalRSSRepository(get(), get(), Account(id = 1, isCurrentAccount = true, accountType = AccountType.LOCAL)) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 9a86bf6b..4e8e0e2f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -17,11 +17,14 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.transitions.FadeTransition import cafe.adriel.voyager.transitions.SlideTransition +import com.readrops.app.compose.account.AccountSelectionScreen import com.readrops.app.compose.account.AccountTab +import com.readrops.app.compose.account.AccountViewModel import com.readrops.app.compose.feeds.FeedTab import com.readrops.app.compose.home.HomeScreen import com.readrops.app.compose.more.MoreTab import com.readrops.app.compose.timelime.TimelineTab +import org.koin.androidx.viewmodel.ext.android.getViewModel class MainActivity : ComponentActivity() { @@ -29,10 +32,13 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val viewModel = getViewModel() + val accountExists = viewModel.accountExists() + setContent { ReadropsTheme { Navigator( - screen = HomeScreen() + screen = if (accountExists) HomeScreen() else AccountSelectionScreen() ) { navigator -> FadeTransition(navigator) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt new file mode 100644 index 00000000..46f9a560 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt @@ -0,0 +1,17 @@ +package com.readrops.app.compose.account + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import cafe.adriel.voyager.androidx.AndroidScreen + +class AccountSelectionScreen : AndroidScreen() { + + @Composable + override fun Content() { + Column { + Text(text = "account selection") + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt new file mode 100644 index 00000000..9f8e9312 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt @@ -0,0 +1,21 @@ +package com.readrops.app.compose.account + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.db.Database +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +class AccountViewModel( + private val database: Database, +) : ViewModel() { + + fun accountExists(): Boolean { + val accountCount = runBlocking { + database.newAccountDao().selectAccountCount() + } + + return accountCount > 0 + } +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt index 3e24102c..8b6c9834 100644 --- a/db/src/main/java/com/readrops/db/Database.kt +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -4,6 +4,7 @@ import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.readrops.db.dao.* +import com.readrops.db.dao.newdao.NewAccountDao import com.readrops.db.dao.newdao.NewFeedDao import com.readrops.db.dao.newdao.NewItemDao import com.readrops.db.entities.* @@ -32,4 +33,6 @@ abstract class Database : RoomDatabase() { abstract fun newFeedDao(): NewFeedDao abstract fun newItemDao(): NewItemDao + + abstract fun newAccountDao(): NewAccountDao } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt new file mode 100644 index 00000000..af1c575d --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt @@ -0,0 +1,12 @@ +package com.readrops.db.dao.newdao + +import androidx.room.Dao +import androidx.room.Query + +@Dao +interface NewAccountDao { + + @Query("Select Count(*) From Account") + suspend fun selectAccountCount(): Int + +} \ No newline at end of file From c9db21e88164844d5dec20a56cdca0454728d667 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sat, 5 Aug 2023 22:17:51 +0200 Subject: [PATCH 23/95] Enable BottomAppBar selected animation --- .../java/com/readrops/app/compose/home/HomeScreen.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt index a6600451..da741f9e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import cafe.adriel.voyager.androidx.AndroidScreen -import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.CurrentTab @@ -40,7 +39,7 @@ class HomeScreen : AndroidScreen() { bottomBar = { BottomAppBar { NavigationBarItem( - selected = false, + selected = tabNavigator.current.key == TimelineTab.key, onClick = { tabNavigator.current = TimelineTab }, icon = { Icon( @@ -52,7 +51,7 @@ class HomeScreen : AndroidScreen() { ) NavigationBarItem( - selected = false, + selected = tabNavigator.current.key == FeedTab.key, onClick = { tabNavigator.current = FeedTab }, icon = { Icon( @@ -64,7 +63,7 @@ class HomeScreen : AndroidScreen() { ) NavigationBarItem( - selected = false, + selected = tabNavigator.current.key == AccountTab.key, onClick = { tabNavigator.current = AccountTab }, icon = { Icon( @@ -76,7 +75,7 @@ class HomeScreen : AndroidScreen() { ) NavigationBarItem( - selected = false, + selected = tabNavigator.current.key == MoreTab.key, onClick = { tabNavigator.current = MoreTab }, icon = { Icon( From dac54a307f1c4160c8607f3c54c5a7e747b4da1f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 7 Aug 2023 13:45:28 +0200 Subject: [PATCH 24/95] Add account creation screens with global account state management --- .../readrops/app/compose/ComposeAppModule.kt | 14 ++-- .../com/readrops/app/compose/MainActivity.kt | 20 +---- .../compose/account/AccountSelectionScreen.kt | 17 ---- .../app/compose/account/AccountViewModel.kt | 21 ----- .../credentials/AccountCredentialsScreen.kt | 41 ++++++++++ .../selection/AccountSelectionScreen.kt | 80 +++++++++++++++++++ .../selection/AccountSelectionViewModel.kt | 69 ++++++++++++++++ .../readrops/app/compose/base/TabViewModel.kt | 49 ++++++++++++ .../app/compose/feeds/FeedViewModel.kt | 10 ++- .../app/compose/timelime/TimelineViewModel.kt | 22 ++--- .../readrops/db/dao/newdao/NewAccountDao.kt | 6 +- 11 files changed, 274 insertions(+), 75 deletions(-) delete mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt delete mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/credentials/AccountCredentialsScreen.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionViewModel.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index 1503d674..5a76a6a2 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -1,24 +1,26 @@ package com.readrops.app.compose -import com.readrops.app.compose.account.AccountViewModel +import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.repositories.BaseRepository import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.app.compose.timelime.TimelineViewModel import com.readrops.db.entities.account.Account -import com.readrops.db.entities.account.AccountType import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val composeAppModule = module { - viewModel { TimelineViewModel(get(), get()) } + viewModel { TimelineViewModel(get()) } - viewModel { FeedViewModel(get(), get()) } + viewModel { FeedViewModel(get()) } - viewModel { AccountViewModel(get()) } + viewModel { AccountSelectionViewModel(get()) } // repositories - single { LocalRSSRepository(get(), get(), Account(id = 1, isCurrentAccount = true, accountType = AccountType.LOCAL)) } + factory { (account: Account) -> + LocalRSSRepository(get(), get(), account) + } + } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 4e8e0e2f..8547e9c6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -4,26 +4,12 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountBox -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.transitions.FadeTransition -import cafe.adriel.voyager.transitions.SlideTransition -import com.readrops.app.compose.account.AccountSelectionScreen -import com.readrops.app.compose.account.AccountTab -import com.readrops.app.compose.account.AccountViewModel -import com.readrops.app.compose.feeds.FeedTab +import com.readrops.app.compose.account.selection.AccountSelectionScreen +import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.home.HomeScreen -import com.readrops.app.compose.more.MoreTab -import com.readrops.app.compose.timelime.TimelineTab import org.koin.androidx.viewmodel.ext.android.getViewModel class MainActivity : ComponentActivity() { @@ -32,7 +18,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val viewModel = getViewModel() + val viewModel = getViewModel() val accountExists = viewModel.accountExists() setContent { diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt deleted file mode 100644 index 46f9a560..00000000 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountSelectionScreen.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.readrops.app.compose.account - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import cafe.adriel.voyager.androidx.AndroidScreen - -class AccountSelectionScreen : AndroidScreen() { - - @Composable - override fun Content() { - Column { - Text(text = "account selection") - } - } -} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt deleted file mode 100644 index 9f8e9312..00000000 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.readrops.app.compose.account - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.readrops.db.Database -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking - -class AccountViewModel( - private val database: Database, -) : ViewModel() { - - fun accountExists(): Boolean { - val accountCount = runBlocking { - database.newAccountDao().selectAccountCount() - } - - return accountCount > 0 - } -} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/credentials/AccountCredentialsScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/credentials/AccountCredentialsScreen.kt new file mode 100644 index 00000000..01bab8eb --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/credentials/AccountCredentialsScreen.kt @@ -0,0 +1,41 @@ +package com.readrops.app.compose.account.credentials + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.readrops.app.compose.home.HomeScreen +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType + +class AccountCredentialsScreen( + private val accountType: AccountType, + private val account: Account? = null, +) : AndroidScreen() { + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + + Column { + Text( + text = "AccountCredentialsScreen" + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Button(onClick = { navigator.replaceAll(HomeScreen()) }) { + Text( + text = "skip" + ) + } + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt new file mode 100644 index 00000000..95e120d1 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt @@ -0,0 +1,80 @@ +package com.readrops.app.compose.account.selection + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.androidx.AndroidScreen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.readrops.app.compose.R +import com.readrops.app.compose.account.credentials.AccountCredentialsScreen +import com.readrops.app.compose.home.HomeScreen +import com.readrops.db.entities.account.AccountType +import org.koin.androidx.compose.getViewModel + +class AccountSelectionScreen : AndroidScreen() { + + @Composable + override fun Content() { + val viewModel = getViewModel() + val navState by viewModel.navState.collectAsState() + val navigator = LocalNavigator.currentOrThrow + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + Text(text = "Choose an account") + + Spacer(modifier = Modifier.size(8.dp)) + + AccountType.values().forEach { accountType -> + Row( + modifier = Modifier.clickable { viewModel.createAccount(accountType) } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_freshrss), + contentDescription = accountType.name, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.size(4.dp)) + + Text(text = accountType.name) + } + + Spacer(modifier = Modifier.size(8.dp)) + } + } + + when (navState) { + is AccountSelectionViewModel.NavState.GoToHomeScreen -> { + // using replace makes the app crash due to a screen key conflict + navigator.replaceAll(HomeScreen()) + } + + is AccountSelectionViewModel.NavState.GoToAccountCredentialsScreen -> { + val accountType = (navState as AccountSelectionViewModel.NavState.GoToAccountCredentialsScreen).accountType + + navigator.push(AccountCredentialsScreen(accountType)) + viewModel.resetNavState() + } + + else -> {} + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionViewModel.kt new file mode 100644 index 00000000..a68581e7 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionViewModel.kt @@ -0,0 +1,69 @@ +package com.readrops.app.compose.account.selection + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.db.Database +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import org.koin.core.component.KoinComponent +import org.koin.core.component.get + +class AccountSelectionViewModel( + private val database: Database, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : ViewModel(), KoinComponent { + + private val _navState = MutableStateFlow(NavState.Idle) + val navState = _navState.asStateFlow() + + fun accountExists(): Boolean { + val accountCount = runBlocking { + database.newAccountDao().selectAccountCount() + } + + return accountCount > 0 + } + + fun createAccount(accountType: AccountType) { + if (accountType == AccountType.LOCAL) { + createLocalAccount() + } else { + _navState.update { NavState.GoToAccountCredentialsScreen(accountType) } + } + } + + fun resetNavState() { + _navState.update { NavState.Idle } + } + + private fun createLocalAccount() { + val context = get() + val account = Account( + url = null, + accountName = context.getString(AccountType.LOCAL.typeName), + accountType = AccountType.LOCAL, + isCurrentAccount = true + ) + + viewModelScope.launch(dispatcher) { + database.newAccountDao().insert(account) + + _navState.update { NavState.GoToHomeScreen } + } + } + + + sealed class NavState { + object Idle : NavState() + object GoToHomeScreen : NavState() + class GoToAccountCredentialsScreen(val accountType: AccountType) : NavState() + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt new file mode 100644 index 00000000..7d73073f --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt @@ -0,0 +1,49 @@ +package com.readrops.app.compose.base + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.app.compose.repositories.BaseRepository +import com.readrops.app.compose.repositories.LocalRSSRepository +import com.readrops.db.Database +import com.readrops.db.entities.account.Account +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +/** + * Custom ViewModel for Tab screens handling account change + */ +abstract class TabViewModel( + private val database: Database, +) : ViewModel(), KoinComponent { + + /** + * Repository intended to be rebuilt when the current account changes + */ + protected var repository: BaseRepository? = null + + protected var currentAccount: Account? = null + + /** + * This method is called when the repository has been rebuilt from the new current account + */ + abstract fun invalidate() + + init { + viewModelScope.launch { + database.newAccountDao() + .selectCurrentAccount() + .distinctUntilChanged() + .collect { account -> + currentAccount = account + repository = get(parameters = { parametersOf(account) }) + + invalidate() + } + } + } + +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 14b26400..64f1ba71 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -2,6 +2,7 @@ package com.readrops.app.compose.feeds import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.BaseRepository import com.readrops.db.Database import com.readrops.db.entities.Feed @@ -13,8 +14,7 @@ import kotlinx.coroutines.launch class FeedViewModel( private val database: Database, - private val repository: BaseRepository, -) : ViewModel() { +) : TabViewModel(database) { private val _feedsState = MutableStateFlow(FeedsState.InitialState) val feedsState = _feedsState.asStateFlow() @@ -29,9 +29,13 @@ class FeedViewModel( fun insertFeed(url: String) { viewModelScope.launch(context = Dispatchers.IO) { - repository.insertNewFeeds(listOf(url)) + repository?.insertNewFeeds(listOf(url)) } } + + override fun invalidate() { + + } } sealed class FeedsState { diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index b310318d..8eed245f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -1,8 +1,7 @@ package com.readrops.app.compose.timelime -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.readrops.app.compose.repositories.BaseRepository +import com.readrops.app.compose.base.TabViewModel import com.readrops.db.Database import com.readrops.db.entities.Item import kotlinx.coroutines.Dispatchers @@ -12,9 +11,8 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch class TimelineViewModel( - private val database: Database, - private val repository: BaseRepository, -) : ViewModel() { + private val database: Database, +) : TabViewModel(database) { private val _timelineState = MutableStateFlow(TimelineState.InitialState) val timelineState = _timelineState.asStateFlow() @@ -25,23 +23,27 @@ class TimelineViewModel( init { viewModelScope.launch(context = Dispatchers.IO) { database.newItemDao().selectAll() - .catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) } - .collect { - _timelineState.value = TimelineState.LoadedState(it) - } + .catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) } + .collect { + _timelineState.value = TimelineState.LoadedState(it) + } } } fun refreshTimeline() { _isRefreshing.value = true viewModelScope.launch(context = Dispatchers.IO) { - repository.synchronize(null) { + repository?.synchronize(null) { } _isRefreshing.value = false } } + + override fun invalidate() { + refreshTimeline() + } } sealed class TimelineState { diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt index af1c575d..9356ac42 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt @@ -2,11 +2,15 @@ package com.readrops.db.dao.newdao import androidx.room.Dao import androidx.room.Query +import com.readrops.db.entities.account.Account +import kotlinx.coroutines.flow.Flow @Dao -interface NewAccountDao { +interface NewAccountDao : NewBaseDao { @Query("Select Count(*) From Account") suspend fun selectAccountCount(): Int + @Query("Select * From Account Where current_account = 1") + fun selectCurrentAccount(): Flow } \ No newline at end of file From c88f6fe2bc84262571dbfbeb75af92bf76fc89bc Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 7 Aug 2023 16:55:14 +0200 Subject: [PATCH 25/95] Handle back gesture in HomeScreen --- .../main/java/com/readrops/app/compose/home/HomeScreen.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt index da741f9e..08637a3c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose.home +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -93,6 +94,11 @@ class HomeScreen : AndroidScreen() { ) { CurrentTab() } + + BackHandler( + enabled = tabNavigator.current != TimelineTab, + onBack = { tabNavigator.current = TimelineTab } + ) } } From f08d3eac7189e98481b32d43396303497c83fced Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 7 Aug 2023 17:01:19 +0200 Subject: [PATCH 26/95] Add delete accounts and new account screen tracks in AccountTab --- .../readrops/app/compose/ComposeAppModule.kt | 3 ++ .../app/compose/account/AccountTab.kt | 38 +++++++++++++++++++ .../app/compose/account/AccountViewModel.kt | 29 ++++++++++++++ .../readrops/app/compose/base/TabViewModel.kt | 10 ++--- .../repositories/LocalRSSRepository.kt | 18 ++++++++- .../readrops/db/dao/newdao/NewAccountDao.kt | 5 ++- 6 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index 5a76a6a2..4741a29e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose +import com.readrops.app.compose.account.AccountViewModel import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.repositories.BaseRepository @@ -17,6 +18,8 @@ val composeAppModule = module { viewModel { AccountSelectionViewModel(get()) } + viewModel { AccountViewModel(get()) } + // repositories factory { (account: Account) -> diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index ced0aba9..5164a69d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -1,10 +1,22 @@ package com.readrops.app.compose.account import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.readrops.app.compose.account.selection.AccountSelectionScreen +import org.koin.androidx.compose.getViewModel object AccountTab : Tab { @@ -17,8 +29,34 @@ object AccountTab : Tab { @Composable override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val viewModel = getViewModel() + val closeHome by viewModel.closeHome.collectAsState() + + if (closeHome) { + navigator.replaceAll(AccountSelectionScreen()) + } + Column { Text(text = "Account") + + Spacer(modifier = Modifier.size(16.dp)) + + Row { + Button(onClick = { viewModel.deleteAccount() }) { + Text( + text = "Delete" + ) + } + + Spacer(modifier = Modifier.size(16.dp)) + + Button(onClick = { navigator.push(AccountSelectionScreen()) }) { + Text( + text = "New" + ) + } + } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt new file mode 100644 index 00000000..2f4d6efb --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt @@ -0,0 +1,29 @@ +package com.readrops.app.compose.account + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.readrops.db.Database +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class AccountViewModel( + private val database: Database +) : ViewModel() { + + private val _closeHome = MutableStateFlow(false) + val closeHome = _closeHome.asStateFlow() + + + fun deleteAccount() { + viewModelScope.launch(Dispatchers.IO) { + database.newAccountDao() + .deleteAllAccounts() + + _closeHome.update { true } + } + } + +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt index 7d73073f..05887ce8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt @@ -3,10 +3,8 @@ package com.readrops.app.compose.base import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.readrops.app.compose.repositories.BaseRepository -import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.db.Database import com.readrops.db.entities.account.Account -import kotlinx.coroutines.async import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -38,10 +36,12 @@ abstract class TabViewModel( .selectCurrentAccount() .distinctUntilChanged() .collect { account -> - currentAccount = account - repository = get(parameters = { parametersOf(account) }) + if (account != null) { + currentAccount = account + repository = get(parameters = { parametersOf(account) }) - invalidate() + invalidate() + } } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index de0824c4..61c590ba 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -2,10 +2,14 @@ package com.readrops.app.compose.repositories import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.SyncResult +import com.readrops.api.utils.ApiUtils 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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Headers import org.jsoup.Jsoup class LocalRSSRepository( @@ -29,8 +33,18 @@ class LocalRSSRepository( } else selectedFeeds for (feed in feeds) { + onUpdate(feed) + + val headers = Headers.Builder() + if (feed.etag != null) { + headers[ApiUtils.IF_NONE_MATCH_HEADER] = feed.etag!! + } + if (feed.lastModified != null) { + headers[ApiUtils.IF_MODIFIED_HEADER] = feed.lastModified!! + } + try { - val pair = dataSource.queryRSSResource(feed.url!!, null) + val pair = dataSource.queryRSSResource(feed.url!!, headers.build()) pair?.let { insertNewItems(it.second, feed) @@ -48,7 +62,7 @@ class LocalRSSRepository( override suspend fun synchronize(): SyncResult = throw NotImplementedError("This method can't be called here") - override suspend fun insertNewFeeds(urls: List) { + override suspend fun insertNewFeeds(urls: List) = withContext(Dispatchers.IO) { for (url in urls) { try { val result = dataSource.queryRSSResource(url, null)!! diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt index 9356ac42..b62cef67 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt @@ -12,5 +12,8 @@ interface NewAccountDao : NewBaseDao { suspend fun selectAccountCount(): Int @Query("Select * From Account Where current_account = 1") - fun selectCurrentAccount(): Flow + fun selectCurrentAccount(): Flow + + @Query("Delete From Account") + suspend fun deleteAllAccounts() } \ No newline at end of file From cf0f94bff262717f210b15ca395c1f47bc7db94f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 7 Aug 2023 19:31:01 +0200 Subject: [PATCH 27/95] Add topAppBar to some tabs --- .../app/compose/account/AccountTab.kt | 41 +++++---- .../com/readrops/app/compose/feeds/FeedTab.kt | 88 +++++++++++-------- .../app/compose/timelime/TimelineTab.kt | 62 ++++++++----- 3 files changed, 115 insertions(+), 76 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index 5164a69d..89cddabd 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -3,9 +3,13 @@ package com.readrops.app.compose.account import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -27,6 +31,7 @@ object AccountTab : Tab { title = "Account" ) + @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -37,27 +42,33 @@ object AccountTab : Tab { navigator.replaceAll(AccountSelectionScreen()) } - Column { - Text(text = "Account") - - Spacer(modifier = Modifier.size(16.dp)) - - Row { - Button(onClick = { viewModel.deleteAccount() }) { - Text( - text = "Delete" - ) + Scaffold( + topBar = { + TopAppBar(title = { Text(text = "Account") }) } + ) { paddingValues -> + Column( + modifier = Modifier.padding(paddingValues) + ) { + Row { + Button(onClick = { viewModel.deleteAccount() }) { + Text( + text = "Delete" + ) + } - Spacer(modifier = Modifier.size(16.dp)) + Spacer(modifier = Modifier.size(16.dp)) - Button(onClick = { navigator.push(AccountSelectionScreen()) }) { - Text( - text = "New" - ) + Button(onClick = { navigator.push(AccountSelectionScreen()) }) { + Text( + text = "New" + ) + } } } } + + } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index b0e8173a..0395e580 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -7,8 +7,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -31,6 +35,7 @@ object FeedTab : Tab { title = "Feeds" ) + @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { val viewModel = getViewModel() @@ -40,55 +45,62 @@ object FeedTab : Tab { if (showDialog) { AddFeedDialog( - onDismiss = { showDialog = false }, - onValidate = { - showDialog = false - viewModel.insertFeed(it) - } + onDismiss = { showDialog = false }, + onValidate = { + showDialog = false + viewModel.insertFeed(it) + } ) } - Box( - modifier = Modifier.fillMaxSize() - ) { - when (state) { - is FeedsState.LoadedState -> { - val feeds = (state as FeedsState.LoadedState).feeds + Scaffold( + topBar = { + TopAppBar(title = { Text(text = "Feeds") }) + } + ) { paddingValues -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + when (state) { + is FeedsState.LoadedState -> { + val feeds = (state as FeedsState.LoadedState).feeds - if (feeds.isNotEmpty()) { - LazyColumn { - items( - items = feeds - ) { feed -> - FeedItem( - feed = feed, - ) + if (feeds.isNotEmpty()) { + LazyColumn { + items( + items = feeds + ) { feed -> + FeedItem( + feed = feed, + ) + } } } } + + is FeedsState.ErrorState -> { + + } + + else -> { + + } } - is FeedsState.ErrorState -> { + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + onClick = { showDialog = true } + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null + ) } - else -> { - - } - } - - - - FloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - onClick = { showDialog = true } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null - ) } } - } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 63d11702..10536ffe 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -3,21 +3,23 @@ package com.readrops.app.compose.timelime import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow -import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.google.accompanist.swiperefresh.SwipeRefresh @@ -37,6 +39,7 @@ object TimelineTab : Tab { ) } + @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { val viewModel = getViewModel() @@ -47,37 +50,50 @@ object TimelineTab : Tab { val navigator = LocalNavigator.currentOrThrow - SwipeRefresh( - state = swipeToRefreshState, - onRefresh = { - viewModel.refreshTimeline() - }, - ) { - when (state) { - is TimelineState.LoadedState -> { - val items = (state as TimelineState.LoadedState).items + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "Timeline") } + ) + } + ) { paddingValues -> + SwipeRefresh( + state = swipeToRefreshState, + onRefresh = { + viewModel.refreshTimeline() + }, + modifier = Modifier.padding(paddingValues) + ) { + when (state) { + is TimelineState.LoadedState -> { + val items = (state as TimelineState.LoadedState).items - if (items.isNotEmpty()) { - LazyColumn { - items( - items = items - ) { - TimelineItem( - onClick = { navigator.push(ItemScreen()) } - ) + if (items.isNotEmpty()) { + LazyColumn { + items( + items = items + ) { + TimelineItem( + onClick = { navigator.push(ItemScreen()) } + ) + } } + } else { + NoItemPlaceholder() } - } else { + } + + else -> { NoItemPlaceholder() } } - else -> { - NoItemPlaceholder() - } } } + + } } + @Composable fun NoItemPlaceholder() { val scrollState = rememberScrollState() From 38179f66c0b61770cd018f813aa1fd1dcb8247c4 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 15:58:29 +0200 Subject: [PATCH 28/95] Make Github CI job work again * Make old app module compile * Disable appcompose lint abortion * Make appcompose instrumented tests work --- .../feeds/EditFeedDialogFragment.java | 10 ++++---- .../app/feedsfolders/feeds/FeedsFragment.java | 12 +++++----- .../feedsfolders/folders/FoldersFragment.java | 10 ++++---- .../app/repositories/LocalFeedRepository.java | 3 +-- appcompose/build.gradle | 4 ++++ .../app/compose/ExampleInstrumentedTest.kt | 24 ------------------- db/build.gradle | 1 + 7 files changed, 22 insertions(+), 42 deletions(-) delete mode 100644 appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt diff --git a/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java index 9f8c64d5..32f1c567 100644 --- a/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java @@ -1,5 +1,7 @@ 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; @@ -20,6 +22,8 @@ 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; @@ -27,10 +31,6 @@ import java.util.TreeMap; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; -import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; - -import org.koin.android.compat.SharedViewModelCompat; - public class EditFeedDialogFragment extends DialogFragment implements AdapterView.OnItemSelectedListener { private TextInputEditText feedName; @@ -60,7 +60,7 @@ public class EditFeedDialogFragment extends DialogFragment implements AdapterVie @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); + viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue(); feedWithFolder = getArguments().getParcelable("feedWithFolder"); account = getArguments().getParcelable(ACCOUNT); diff --git a/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java index d498f813..34b6efb6 100644 --- a/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java @@ -1,6 +1,8 @@ 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; @@ -15,21 +17,19 @@ 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.app.feedsfolders.ManageFeedsFoldersViewModel; 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; -import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; - -import org.koin.android.compat.SharedViewModelCompat; - public class FeedsFragment extends Fragment { @@ -64,7 +64,7 @@ public class FeedsFragment extends Fragment { if (account.getPassword() == null) account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey())); - viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); + viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue(); viewModel.setAccount(account); viewModel.getFeedsWithFolder().observe(this, feedWithFolders -> { diff --git a/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java index d8d64ab0..3881e4c9 100644 --- a/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java +++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java @@ -1,6 +1,8 @@ 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; @@ -23,14 +25,12 @@ 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; -import static com.readrops.app.utils.ReadropsKeys.ACCOUNT; - -import org.koin.android.compat.SharedViewModelCompat; - public class FoldersFragment extends Fragment { private FoldersAdapter adapter; @@ -65,7 +65,7 @@ public class FoldersFragment extends Fragment { account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey())); adapter = new FoldersAdapter(this::openFolderOptionsDialog); - viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class); + viewModel = SharedViewModelCompat.sharedViewModel(this, ManageFeedsFoldersViewModel.class).getValue(); viewModel.setAccount(account); viewModel.getFeedCountByAccount() diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java index 38a0042d..62a94779 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java +++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java @@ -1,6 +1,5 @@ package com.readrops.app.repositories; -import android.accounts.NetworkErrorException; import android.content.Context; import android.os.Handler; import android.os.Looper; @@ -114,7 +113,7 @@ public class LocalFeedRepository extends ARepository { } catch (UnknownFormatException e) { Log.d(TAG, "addFeeds: " + e.getMessage()); insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR); - } catch (NetworkErrorException | IOException e) { + } catch (IOException e) { Log.d(TAG, "addFeeds: " + e.getMessage()); insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR); } catch (Exception e) { diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 25b7fe38..6356c382 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -50,6 +50,10 @@ android { composeOptions { kotlinCompilerExtensionVersion = "1.4.0" } + + lint { + abortOnError false + } } dependencies { diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt deleted file mode 100644 index 831e9c8c..00000000 --- a/appcompose/src/androidTest/java/com/readrops/app/compose/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.readrops.app.compose - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.readrops.app.compose", appContext.packageName) - } -} \ No newline at end of file diff --git a/db/build.gradle b/db/build.gradle index 7c2ab020..6eb8ff23 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -94,6 +94,7 @@ dependencies { api "io.insert-koin:koin-core:$rootProject.ext.koin_version" api "io.insert-koin:koin-android:$rootProject.ext.koin_version" api "io.insert-koin:koin-androidx-compose:3.4.2" + api "io.insert-koin:koin-android-compat:$rootProject.ext.koin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' From 23e47c93d87cf98a1a289456af6e4ac16938fa5d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 16:20:33 +0200 Subject: [PATCH 29/95] Use java 11 in CI --- .github/workflows/android.yml | 6 +++--- build.gradle | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4d290bd9..1ca6b5ec 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -15,10 +15,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: set up JDK 1.8 - uses: actions/setup-java@v1 + - name: set up JDK 1.11 + uses: actions/setup-java@v3 with: - java-version: 1.8 + java-version: 1.11 - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2.20.0 with: diff --git a/build.gradle b/build.gradle index eb7591c4..4c674d51 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jacoco:org.jacoco.core:0.8.7" } From 39e824a02c11226d25f9e3582e3a1b02eaacee1b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 16:24:46 +0200 Subject: [PATCH 30/95] Add distribution in setup-java step --- .github/workflows/android.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 1ca6b5ec..c8483c2f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -18,6 +18,7 @@ jobs: - name: set up JDK 1.11 uses: actions/setup-java@v3 with: + distribution: 'temurin' java-version: 1.11 - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2.20.0 From 7fbc4cf5c39dc4f333b8d565ebf736fcef2fda4e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 16:26:30 +0200 Subject: [PATCH 31/95] Put java version in quotes --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index c8483c2f..f5b3813f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -19,7 +19,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 1.11 + java-version: '11' - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2.20.0 with: From 5b73f2952c8f68c77af036d0f6c4a143cc38e9fd Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 16:36:47 +0200 Subject: [PATCH 32/95] Update android-emulator-runner step --- .github/workflows/android.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index f5b3813f..79eb5ac7 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -21,9 +21,9 @@ jobs: distribution: 'temurin' java-version: '11' - name: Android Emulator Runner - uses: ReactiveCircus/android-emulator-runner@v2.20.0 + uses: ReactiveCircus/android-emulator-runner@v2.28 with: - api-level: 29 + api-level: 33 script: ./gradlew clean build connectedCheck jacocoFullReport - uses: codecov/codecov-action@v2.1.0 with: From 816aa852b4dd62c9d153ca62ed42afabb8f72ae1 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 16:38:09 +0200 Subject: [PATCH 33/95] Fix android-emulator-runner version --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 79eb5ac7..4a846776 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -21,7 +21,7 @@ jobs: distribution: 'temurin' java-version: '11' - name: Android Emulator Runner - uses: ReactiveCircus/android-emulator-runner@v2.28 + uses: ReactiveCircus/android-emulator-runner@v2.28.0 with: api-level: 33 script: ./gradlew clean build connectedCheck jacocoFullReport From 89eddf04b0a1bcb609863dcb3077572fb71805b4 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 16:43:33 +0200 Subject: [PATCH 34/95] Revert android-emulator-runner api-version to 29 --- .github/workflows/android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 4a846776..ed02aca4 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -23,7 +23,7 @@ jobs: - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2.28.0 with: - api-level: 33 + api-level: 29 script: ./gradlew clean build connectedCheck jacocoFullReport - uses: codecov/codecov-action@v2.1.0 with: From 87aa0a5f44ffc74489d4bb774bf72f9b5a8bb11f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 17:38:14 +0200 Subject: [PATCH 35/95] Add spacing MaterialTheme extension and spacer composables --- .../com/readrops/app/compose/utils/Spacing.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt b/appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt new file mode 100644 index 00000000..675e558a --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt @@ -0,0 +1,41 @@ +package com.readrops.app.compose.utils + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +data class Spacing( + val veryShortSpacing: Dp = 4.dp, + val shortSpacing: Dp = 8.dp, + val mediumSpacing: Dp = 16.dp, + val largeSpacing: Dp = 24.dp, + val veryLargeSpacing: Dp = 48.dp +) + +val LocalSpacing = compositionLocalOf { Spacing() } + +val MaterialTheme.spacing + @Composable + @ReadOnlyComposable + get() = LocalSpacing.current + +@Composable +fun VeryShortSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.veryShortSpacing)) + +@Composable +fun ShortSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.shortSpacing)) + +@Composable +fun MediumSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.mediumSpacing)) + +@Composable +fun LargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.largeSpacing)) + +@Composable +fun VeryLargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.veryLargeSpacing)) \ No newline at end of file From 8a04fad0d77530de6cbcf061b33679a3ecfd5712 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 18:17:36 +0200 Subject: [PATCH 36/95] Add subpackage theme --- .../src/main/java/com/readrops/app/compose/MainActivity.kt | 1 + .../java/com/readrops/app/compose/{ => utils/theme}/Color.kt | 2 +- .../java/com/readrops/app/compose/utils/{ => theme}/Spacing.kt | 2 +- .../java/com/readrops/app/compose/{ => utils/theme}/Theme.kt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/{ => utils/theme}/Color.kt (98%) rename appcompose/src/main/java/com/readrops/app/compose/utils/{ => theme}/Spacing.kt (94%) rename appcompose/src/main/java/com/readrops/app/compose/{ => utils/theme}/Theme.kt (98%) diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 8547e9c6..1a8769fa 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -10,6 +10,7 @@ import cafe.adriel.voyager.transitions.FadeTransition import com.readrops.app.compose.account.selection.AccountSelectionScreen import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.home.HomeScreen +import com.readrops.app.compose.utils.theme.ReadropsTheme import org.koin.androidx.viewmodel.ext.android.getViewModel class MainActivity : ComponentActivity() { diff --git a/appcompose/src/main/java/com/readrops/app/compose/Color.kt b/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Color.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/Color.kt rename to appcompose/src/main/java/com/readrops/app/compose/utils/theme/Color.kt index 11500362..196fef21 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/Color.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Color.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose +package com.readrops.app.compose.utils.theme import androidx.compose.ui.graphics.Color diff --git a/appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt b/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Spacing.kt similarity index 94% rename from appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt rename to appcompose/src/main/java/com/readrops/app/compose/utils/theme/Spacing.kt index 675e558a..cdcf12f2 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/utils/Spacing.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Spacing.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.utils +package com.readrops.app.compose.utils.theme import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size diff --git a/appcompose/src/main/java/com/readrops/app/compose/Theme.kt b/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Theme.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/Theme.kt rename to appcompose/src/main/java/com/readrops/app/compose/utils/theme/Theme.kt index 7e2086fa..d06d61c7 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/Theme.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose +package com.readrops.app.compose.utils.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme From b15eb9fa91bc511cf2feeee455c1c1ad0c3e1031 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 22:40:29 +0200 Subject: [PATCH 37/95] Add TimelineItem large layout base --- appcompose/build.gradle | 3 +- .../readrops/app/compose/feeds/FeedItem.kt | 6 +- .../app/compose/timelime/TimelineItem.kt | 143 ++++++++++++++---- .../app/compose/timelime/TimelineTab.kt | 53 ++++--- .../app/compose/timelime/TimelineViewModel.kt | 22 +-- .../com/readrops/db/dao/newdao/NewItemDao.kt | 13 +- 6 files changed, 167 insertions(+), 73 deletions(-) diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 6356c382..84c07e05 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -103,5 +103,6 @@ dependencies { androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0' - implementation "com.github.skydoves:landscapist-coil:2.2.2" + implementation "io.coil-kt:coil:2.4.0" + implementation "io.coil-kt:coil-compose:2.4.0" } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt index d4ecfe5f..0ea04f2b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt @@ -9,9 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import coil.request.ImageRequest import com.readrops.db.entities.Feed -import com.skydoves.landscapist.coil.CoilImage @Composable fun FeedItem(feed: Feed) { @@ -22,11 +20,11 @@ fun FeedItem(feed: Feed) { .fillMaxWidth() .padding(8.dp) ) { - CoilImage(imageRequest = { + /*CoilImage(imageRequest = { ImageRequest.Builder(context) .data(feed.url) .build() - }) + })*/ Text( text = feed.name!!, diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index f93eda0f..eabc87bf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -1,73 +1,152 @@ package com.readrops.app.compose.timelime -import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.FavoriteBorder +import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.readrops.api.utils.DateUtils +import com.readrops.app.compose.R +import com.readrops.app.compose.utils.theme.ShortSpacer +import com.readrops.app.compose.utils.theme.VeryShortSpacer +import com.readrops.app.compose.utils.theme.spacing +import com.readrops.db.pojo.ItemWithFeed -@Preview @Composable fun TimelineItem( - onClick: () -> Unit, + itemWithFeed: ItemWithFeed, + onClick: () -> Unit, + onFavorite: () -> Unit, + onReadLater: () -> Unit, + onShare: () -> Unit ) { Card( - // elevation = 4.card, - modifier = Modifier.background(Color.White) - .padding(8.dp) - .clickable { onClick() } + modifier = Modifier + .padding( + PaddingValues( + horizontal = MaterialTheme.spacing.shortSpacing, + vertical = MaterialTheme.spacing.veryShortSpacing + ) + ) + .clickable { onClick() } ) { Column( modifier = Modifier .fillMaxWidth() - .padding(8.dp) ) { Row( horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding( + start = MaterialTheme.spacing.shortSpacing, + end = MaterialTheme.spacing.shortSpacing, + top = MaterialTheme.spacing.shortSpacing, + ) ) { Row( verticalAlignment = Alignment.CenterVertically, ) { - /* Icon( - painter = painterResource(id = com.readrops.app.R.drawable.ic_rss_feed_grey), + Icon( + painter = painterResource(id = R.drawable.ic_rss_feed_grey), contentDescription = null, - // modifier = Modifier.size((MaterialTheme.typography.subtitle2.fontSize.value * 1.5).dp) - )*/ - -// Spacer(Modifier.padding(4.dp)) - - Text( - text = "feed name", - //style = MaterialTheme.typography. + modifier = Modifier.size(MaterialTheme.typography.labelLarge.fontSize.value.dp) ) + + VeryShortSpacer() + + Column { + Text( + text = itemWithFeed.feedName, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + VeryShortSpacer() + + if (itemWithFeed.folder != null) { + Text( + text = itemWithFeed.folder!!.name!!, + style = MaterialTheme.typography.labelMedium + ) + } + } } Text( - text = "Item date", - // style = MaterialTheme.typography.subtitle2 + text = DateUtils.formattedDateByLocal(itemWithFeed.item.pubDate!!), + style = MaterialTheme.typography.labelMedium, ) } - Spacer(Modifier.size(8.dp)) + ShortSpacer() Text( - text = "title example", - //style = MaterialTheme.typography.h5, + text = itemWithFeed.item.title!!, + style = MaterialTheme.typography.titleMedium, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = MaterialTheme.spacing.shortSpacing) ) - Spacer(Modifier.size(8.dp)) + ShortSpacer() - /* Image( - painter = painterResource(id = com.readrops.app.R.drawable.header_background), - contentDescription = null - )*/ + if (itemWithFeed.item.imageLink != null) { + AsyncImage( + model = itemWithFeed.item.imageLink, + contentDescription = itemWithFeed.item.title!!, + contentScale = ContentScale.Crop, + modifier = Modifier + .aspectRatio(3f / 2f) + .fillMaxWidth() + ) + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(MaterialTheme.spacing.shortSpacing) + ) { + Icon( + imageVector = Icons.Outlined.FavoriteBorder, + contentDescription = null, + modifier = Modifier.clickable { onFavorite() } + ) + + Icon( + imageVector = Icons.Outlined.Add, // placeholder icon + contentDescription = null, + modifier = Modifier.clickable { onReadLater() } + ) + + Icon( + imageVector = Icons.Outlined.Share, + contentDescription = null, + modifier = Modifier.clickable { onShare() } + ) + } } } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 10536ffe..59b0d2bf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -34,8 +34,8 @@ object TimelineTab : Tab { @Composable get() { return TabOptions( - index = 1u, - title = "Timeline", + index = 1u, + title = "Timeline", ) } @@ -51,30 +51,35 @@ object TimelineTab : Tab { val navigator = LocalNavigator.currentOrThrow Scaffold( - topBar = { - TopAppBar( - title = { Text(text = "Timeline") } - ) - } + topBar = { + TopAppBar( + title = { Text(text = "Timeline") } + ) + } ) { paddingValues -> SwipeRefresh( - state = swipeToRefreshState, - onRefresh = { - viewModel.refreshTimeline() - }, - modifier = Modifier.padding(paddingValues) + state = swipeToRefreshState, + onRefresh = { + viewModel.refreshTimeline() + }, + modifier = Modifier.padding(paddingValues) ) { when (state) { - is TimelineState.LoadedState -> { - val items = (state as TimelineState.LoadedState).items + is TimelineState.Loaded -> { + val items = (state as TimelineState.Loaded).items if (items.isNotEmpty()) { LazyColumn { items( - items = items - ) { + items = items, + key = { it.item.id }, + ) { itemWithFeed -> TimelineItem( - onClick = { navigator.push(ItemScreen()) } + itemWithFeed = itemWithFeed, + onClick = { navigator.push(ItemScreen()) }, + onFavorite = {}, + onReadLater = {}, + onShare = {}, ) } } @@ -99,15 +104,15 @@ fun NoItemPlaceholder() { val scrollState = rememberScrollState() Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) ) { Text( - text = "No item", - style = MaterialTheme.typography.displayMedium + text = "No item", + style = MaterialTheme.typography.displayMedium ) } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 8eed245f..373ffd4c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -3,7 +3,9 @@ package com.readrops.app.compose.timelime import androidx.lifecycle.viewModelScope import com.readrops.app.compose.base.TabViewModel import com.readrops.db.Database -import com.readrops.db.entities.Item +import com.readrops.db.pojo.ItemWithFeed +import com.readrops.db.queries.ItemsQueryBuilder +import com.readrops.db.queries.QueryFilters import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,7 +16,7 @@ class TimelineViewModel( private val database: Database, ) : TabViewModel(database) { - private val _timelineState = MutableStateFlow(TimelineState.InitialState) + private val _timelineState = MutableStateFlow(TimelineState.Loading) val timelineState = _timelineState.asStateFlow() private var _isRefreshing = MutableStateFlow(false) @@ -22,10 +24,12 @@ class TimelineViewModel( init { viewModelScope.launch(context = Dispatchers.IO) { - database.newItemDao().selectAll() - .catch { _timelineState.value = TimelineState.ErrorState(Exception(it)) } + val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = 1)) + + database.newItemDao().selectAll(query) + .catch { _timelineState.value = TimelineState.Error(Exception(it)) } .collect { - _timelineState.value = TimelineState.LoadedState(it) + _timelineState.value = TimelineState.Loaded(it) } } } @@ -42,13 +46,13 @@ class TimelineViewModel( } override fun invalidate() { - refreshTimeline() + } } sealed class TimelineState { - object InitialState : TimelineState() - data class ErrorState(val exception: Exception) : TimelineState() - data class LoadedState(val items: List) : TimelineState() + object Loading : TimelineState() + data class Error(val exception: Exception) : TimelineState() + data class Loaded(val items: List) : TimelineState() } diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt index 561c874c..7d9e97eb 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -1,13 +1,20 @@ package com.readrops.db.dao.newdao import androidx.room.Dao -import androidx.room.Query +import androidx.room.RawQuery +import androidx.sqlite.db.SupportSQLiteQuery +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.pojo.ItemWithFeed import kotlinx.coroutines.flow.Flow @Dao abstract class NewItemDao : NewBaseDao { - @Query("Select * From Item") - abstract fun selectAll(): Flow> + @RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class]) + abstract fun selectAll(query: SupportSQLiteQuery): Flow> + + } \ No newline at end of file From 3d6cbfe65f8e26babbf7fb9395fb710a57aff353 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 14 Aug 2023 23:31:56 +0200 Subject: [PATCH 38/95] Better react to account change in TimelineViewModel --- .../readrops/app/compose/base/TabViewModel.kt | 24 +++++++++---------- .../app/compose/feeds/FeedViewModel.kt | 6 ----- .../app/compose/timelime/TimelineViewModel.kt | 20 +++++++++------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt index 05887ce8..22a24f99 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.readrops.app.compose.repositories.BaseRepository import com.readrops.db.Database import com.readrops.db.entities.account.Account +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -15,7 +16,7 @@ import org.koin.core.parameter.parametersOf * Custom ViewModel for Tab screens handling account change */ abstract class TabViewModel( - private val database: Database, + private val database: Database, ) : ViewModel(), KoinComponent { /** @@ -25,24 +26,21 @@ abstract class TabViewModel( protected var currentAccount: Account? = null - /** - * This method is called when the repository has been rebuilt from the new current account - */ - abstract fun invalidate() + protected val accountEvent = Channel() init { viewModelScope.launch { database.newAccountDao() - .selectCurrentAccount() - .distinctUntilChanged() - .collect { account -> - if (account != null) { - currentAccount = account - repository = get(parameters = { parametersOf(account) }) + .selectCurrentAccount() + .distinctUntilChanged() + .collect { account -> + if (account != null) { + currentAccount = account + repository = get(parameters = { parametersOf(account) }) - invalidate() - } + accountEvent.send(account) } + } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 64f1ba71..d067b415 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -1,9 +1,7 @@ package com.readrops.app.compose.feeds -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.readrops.app.compose.base.TabViewModel -import com.readrops.app.compose.repositories.BaseRepository import com.readrops.db.Database import com.readrops.db.entities.Feed import kotlinx.coroutines.Dispatchers @@ -32,10 +30,6 @@ class FeedViewModel( repository?.insertNewFeeds(listOf(url)) } } - - override fun invalidate() { - - } } sealed class FeedsState { diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 373ffd4c..331169fc 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -6,14 +6,18 @@ import com.readrops.db.Database import com.readrops.db.pojo.ItemWithFeed import com.readrops.db.queries.ItemsQueryBuilder import com.readrops.db.queries.QueryFilters +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch class TimelineViewModel( - private val database: Database, + private val database: Database, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : TabViewModel(database) { private val _timelineState = MutableStateFlow(TimelineState.Loading) @@ -23,20 +27,22 @@ class TimelineViewModel( val isRefreshing = _isRefreshing.asStateFlow() init { - viewModelScope.launch(context = Dispatchers.IO) { - val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = 1)) + viewModelScope.launch(dispatcher) { + accountEvent.consumeAsFlow().collectLatest { account -> + val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = account.id)) - database.newItemDao().selectAll(query) + database.newItemDao().selectAll(query) .catch { _timelineState.value = TimelineState.Error(Exception(it)) } .collect { _timelineState.value = TimelineState.Loaded(it) } + } } } fun refreshTimeline() { _isRefreshing.value = true - viewModelScope.launch(context = Dispatchers.IO) { + viewModelScope.launch(dispatcher) { repository?.synchronize(null) { } @@ -44,10 +50,6 @@ class TimelineViewModel( _isRefreshing.value = false } } - - override fun invalidate() { - - } } sealed class TimelineState { From d1e170a1b8d783f1947ec6211d978151b5fc2759 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Aug 2023 14:55:36 +0200 Subject: [PATCH 39/95] Add favicon recovery in HtmlParser --- .../java/com/readrops/api/utils/HtmlParser.kt | 15 +- .../com/readrops/api/utils/HtmlParserTest.kt | 37 +- .../resources/utils/file_without_icon.html | 600 ++++++++++++++++++ 3 files changed, 650 insertions(+), 2 deletions(-) create mode 100644 api/src/test/resources/utils/file_without_icon.html diff --git a/api/src/main/java/com/readrops/api/utils/HtmlParser.kt b/api/src/main/java/com/readrops/api/utils/HtmlParser.kt index 5e0e3201..02005881 100644 --- a/api/src/main/java/com/readrops/api/utils/HtmlParser.kt +++ b/api/src/main/java/com/readrops/api/utils/HtmlParser.kt @@ -14,6 +14,19 @@ data class ParsingResult( object HtmlParser { + suspend fun getFaviconLink(url: String, client: OkHttpClient): String? { + val document = getHTMLHeadFromUrl(url, client) + val elements = document.select("link") + + for (element in elements) { + if (element.attributes()["rel"].lowercase().contains("icon")) { + return element.absUrl("href") + } + } + + return null + } + suspend fun getFeedLink(url: String, client: OkHttpClient): List { val results = mutableListOf() @@ -62,7 +75,7 @@ object HtmlParser { } if (!stringBuilder.contains("") || !stringBuilder.contains("")) - throw Exception("Failed to get HTML head") + throw FormatException("Failed to get HTML head") body.close() return Jsoup.parse(stringBuilder.toString(), url) diff --git a/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt b/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt index b4eadaff..0bf410c8 100644 --- a/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt +++ b/api/src/test/java/com/readrops/api/utils/HtmlParserTest.kt @@ -15,6 +15,7 @@ import org.koin.test.KoinTestRule import java.net.HttpURLConnection import java.util.concurrent.TimeUnit import kotlin.test.assertEquals +import kotlin.test.assertNull import kotlin.test.assertTrue class HtmlParserTest : KoinTest { @@ -64,7 +65,7 @@ class HtmlParserTest : KoinTest { } } - @Test(expected = Exception::class) + @Test(expected = FormatException::class) fun getFeedLinkWithoutHeadTest() { val stream = TestUtils.loadResource("utils/file_without_head.html") @@ -86,4 +87,38 @@ class HtmlParserTest : KoinTest { runBlocking { HtmlParser.getFeedLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) } } + + @Test + fun getFaviconLinkTest() { + val stream = TestUtils.loadResource("utils/file.html") + + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, ApiUtils.HTML_CONTENT_TYPE) + .setBody(Buffer().readFrom(stream)) + ) + + runBlocking { + val result = HtmlParser.getFaviconLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) + + assertTrue { result!!.contains("favicon.ico") } + } + } + + @Test + fun getFaviconLinkWithoutHeadTest() { + val stream = TestUtils.loadResource("utils/file_without_icon.html") + + mockServer.enqueue( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, ApiUtils.HTML_CONTENT_TYPE) + .setBody(Buffer().readFrom(stream)) + ) + + runBlocking { + val result = HtmlParser.getFaviconLink(mockServer.url("/rss").toString(), koinTestRule.koin.get()) + + assertNull(result) + } + } } \ No newline at end of file diff --git a/api/src/test/resources/utils/file_without_icon.html b/api/src/test/resources/utils/file_without_icon.html new file mode 100644 index 00000000..1c2df253 --- /dev/null +++ b/api/src/test/resources/utils/file_without_icon.html @@ -0,0 +1,600 @@ + + + + + + + Hacker News + + +
+ + + + + + + + + + + +
+ + + + + + +
Hacker News + new | past | comments | ask | show | jobs | submit + + login + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
1.A Brief History of Computers (lesswrong.com)
+ 31 points by zdw 1 hour ago | hide | 3 comments +
2.Consumer Software Is Expected to Be Next Fast-Growing Segment (1994) (csmonitor.com)
+ 9 points by 1970-01-01 1 hour ago | hide | 1 comment +
3.MSX-DOS (wikipedia.org)
+ 82 points by pavlov 6 hours ago | hide | 26 comments +
4.New Yorkers Got Broken Promises. Developers Got 20M Sq. Ft (nytimes.com)
+ 12 points by asnyder 20 minutes ago | hide | 1 comment +
5.Apple's interactive television box: Hacking the set top box System 7.1 in ROM (oldvcr.blogspot.com)
+ 160 points by todsacerdoti 10 hours ago | hide | 20 comments +
6.Putting the “You” in CPU (cpu.land)
+ 187 points by uneekname 10 hours ago | hide | 73 comments +
7.Botulinum toxin: Bioweapon and magic drug (nih.gov)
+ 12 points by redbell 2 hours ago | hide | 10 comments +
8.Octos – HTML live wallpaper engine (github.com/underpig1)
+ 85 points by underpig1 6 hours ago | hide | 23 comments +
9.More than you've ever wanted to know about errors in Rust (shuttle.rs)
+ 13 points by asymmetric 2 hours ago | hide | 3 comments +
10.Embrace Complexity; Tighten Your Feedback Loops (ferd.ca)
+ 27 points by lutzh 4 hours ago | hide | 1 comment +
11.AWS networking concepts in a diagram (miparnisariblog.wordpress.com)
+ 171 points by mparnisari 10 hours ago | hide | 66 comments +
12.Plane – Open-source Jira alternative (plane.so)
+ 240 points by prhrb 7 hours ago | hide | 93 comments +
13.Neurotechnology: Current Developments and Ethical Issues (frontiersin.org)
+ 28 points by Quinzel 3 hours ago | hide | 15 comments +
14.What we talk about when we talk about System Design (maheshba.bitbucket.io)
+ 166 points by scv119 11 hours ago | hide | 22 comments +
15.ElKaWe – Electrocaloric heat pumps (fraunhofer.de)
+ 140 points by danans 10 hours ago | hide | 73 comments +
16.Over-grazing and desertification in the Syrian steppe root causes of war (2015) (theecologist.org)
+ 64 points by joveian 6 hours ago | hide | 43 comments +
17.Redmine – open-source project management (redmine.org)
+ 34 points by synergy20 2 hours ago | hide | 24 comments +
18.Google tries internet air-gap for some staff PCs (theregister.com)
+ 67 points by beardyw 9 hours ago | hide | 73 comments +
19.I thought I wanted to be a professor, then I served on a hiring committee (2021) (science.org)
+ 104 points by ykonstant 4 hours ago | hide | 72 comments +
20.Internet search tips (gwern.net)
+ 161 points by herbertl 12 hours ago | hide | 58 comments +
21.Bayesian methods to provide probablistic solution for the Drake equation (2019) (sciencedirect.com)
+ 22 points by benbreen 4 hours ago | hide | 18 comments +
22.Biotumen: Bitumen Reinvented (biofabrik.com)
+ 40 points by patall 7 hours ago | hide | 11 comments +
23.Why even let users set their own passwords? (devever.net)
+ 103 points by hlandau 2 hours ago | hide | 121 comments +
24.Confronting failure as a core life skill (buildinghealthier.substack.com)
+ 168 points by blh75 15 hours ago | hide | 75 comments +
25.Hokusai’s Illustrated Warrior Vanguard of Japan and China (1836) (publicdomainreview.org)
+ 19 points by tintinnabula 2 hours ago | hide | discuss +
26.Bun v0.7.0 (bun.sh)
+ 163 points by sshroot 9 hours ago | hide | 107 comments +
27.Simpson Fan Grows Tomacco (2003) (simpsonsarchive.com)
+ 81 points by pipeline_peak 6 hours ago | hide | 55 comments +
28.Discovery: Metals can heal themselves (sandia.gov)
+ 77 points by bobvanluijt 13 hours ago | hide | 24 comments +
29.Pressure and vacuum marination does not work (2016) (genuineideas.com)
+ 87 points by OJFord 13 hours ago | hide | 57 comments +
30.Scientists: Fishing boats compete with whales and penguins for Antarctic krill (mongabay.com)
+ 5 points by PaulHoule 1 hour ago | hide | discuss +
+
+ + + + + +
+
+
+ Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

+
Search:
+
+
+
+ + + + From d97da320b4bbedb1e67556f7e425e17e4f84842c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Aug 2023 19:07:55 +0200 Subject: [PATCH 40/95] Rewrite FeedColors with some tests --- appcompose/build.gradle | 1 + .../readrops/app/compose/FeedColorsTest.kt | 58 ++++++++++++++++++ .../src/androidTest/resources/favicon.ico | Bin 0 -> 7527 bytes .../com/readrops/app/compose/ReadropsApp.kt | 13 +++- .../readrops/app/compose/utils/FeedColors.kt | 48 +++++++++++++++ 5 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt create mode 100644 appcompose/src/androidTest/resources/favicon.ico create mode 100644 appcompose/src/main/java/com/readrops/app/compose/utils/FeedColors.kt diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 84c07e05..751f7402 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -72,6 +72,7 @@ dependencies { implementation composeBom androidTestImplementation composeBom + implementation 'androidx.palette:palette-ktx:1.0.0' implementation 'androidx.activity:activity-compose:1.7.2' implementation 'androidx.compose.material3:material3' diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt new file mode 100644 index 00000000..8271e781 --- /dev/null +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt @@ -0,0 +1,58 @@ +package com.readrops.app.compose + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.readrops.api.apiModule +import com.readrops.api.utils.ApiUtils +import com.readrops.app.compose.utils.FeedColors +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okio.Buffer +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.koin.dsl.module +import org.koin.test.KoinTestRule +import java.net.HttpURLConnection +import kotlin.test.assertTrue + +class FeedColorsTest { + + private val mockServer = MockWebServer() + + @Before + fun before() { + val context = ApplicationProvider.getApplicationContext() + + KoinTestRule.create { + modules(apiModule, module { + single { context } + }) + } + + mockServer.start() + } + + @After + fun after() { + mockServer.shutdown() + } + + @Test + fun getFeedColorTest() = runBlocking { + val stream = TestUtils.loadResource("favicon.ico") + + mockServer.enqueue( + MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "image/jpeg") + .setBody(Buffer().readFrom(stream)) + ) + + val url = mockServer.url("/rss").toString() + val color = FeedColors.getFeedColor(url) + + assertTrue { color != 0 } + } +} \ No newline at end of file diff --git a/appcompose/src/androidTest/resources/favicon.ico b/appcompose/src/androidTest/resources/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a415c1eaf189ee69d9ee54de5cb606cd9cdf06f4 GIT binary patch literal 7527 zcmeHLg2BcjlRA&NFl7o_l`hch0$YKp-68{Lg^{VgqTjfIxJB9Id4Ry+^`G0*Loi zloj9n=k0$kA_CxM>``V50+Gl$C@5&DC@8RLdAQj+INN|g%J*{8eBSEJ(?ufS&*5YD z+1WTzjQ+%I6OzibVC`s?5G~w@FfMOJd?gwx9zE-~-8j%lb}I-C>8FpDv&3hVTEBX< z6(O6;aZbd?)nflvvM>WM-HnK*UJDWM6|$w=4iji_&)V;|0V7N zN*rAON+~M1{<8n{#=2B07L?r7Jv1D zi+&R~MW^8>gD@8S84zi??GaBb{rbfDtyz@Iy2=u0v}j zi*vOo4qRZI!THy&D3H(w{#GRmw0Zeta`Nh{I!K5n(`;m$tH3#07(&Bv6n%yZB1-9( z_~Ahwdo5v~3-T?o+lT>2t5}v$&1N`k?hM+ZPaAZuEuZnG^u1n<@>NHm8p%Ej(TCK! zr9p5wVJ?~WiHFt9hljA4E&J=PdE~;>3mQn2&V&Ga2RH@ zG<2rwf*M^2WH(o_RpXZWDTm9PIEMmU=$^{kH^sx%9hz(@3q_jj$ab$#yJRM<;xunFdcS|_@8S3Nvn%7N#8or~e zy^}&57H!`V{wmbqi-0vrSn%{}@B?vfLWU1HPBixK%|Jon5?ZY)goAEUAce8JAc?$C)Ro<2>ah{qghFfV_a9&<=Az$F31wBJva;b7>bZ#&ONyIzYu!m zlZb{22;uUX-(wCDamQO>1BHbsQ{tuJ2`J+_$b#$Tq~`z%KAXew{^aNmnhNzF18aN~ zcPB6g>v!LCCnXDk%92F%s0rb$ggOv?JfWm5l&knc_l+V`euk3CkjPI#PU{5?-cN<- zFO*5L*JE5|51xLiC{WOnLOu3kY$r1Lgw7`%V-qF7-~@$HW#CR)+?xY6hs@VWG7~|2 zhyN=2(tahR?_OM0ZKabYZ|S99WjX=zg`4#z6Uhz|;f*U95HX!9%6~E7z@d8Nl*s-K zw;Yo7h5Q>+TfB1OyA3>o$O4Ou2Xwy})+1qYtCo5eGnT4C(-~G7dg}CB;zOB*8`Y(M$D6Y_kYpww=w?`BcK^`&z*yK-tOnddF&ozr zvfp=amFv{^$I}&>5VLFw0TY{~Qe^PJod6=p7tWEzzS4*EA7JeiH5Z&e)GO}M1E;E$4r zURpyv9>8QxHETHX*vvR7l4P_}o^a7M>e{3IThWf^eS1=S$&6Wyos0~Ox{T?fzr+}_T(gFrb4NiMlT4BncQjmuoR@ z(eaVjR1-81G^^DYyftq}G$P2Hw(Pk?E5$kn3hS0&uqvgQ ze2KnSgMmeU+1I?JQn3cHIdsc(W_*WyI{f>5tS}xJzJW{QULCq`p+C#X#42bY!g1ZI zVYFm7}&2`9gt^YVVu5f(H}wZF64 zI(iznPRJd={e4oU6qy4XQ!Z2Y!&zf1q}oYXv~^@f*1jUo{mJCIVEyv>LpHH}KI)lbx~ZArScJa#-Tm3}E5 zEgdgS-Ra#~86iT~0^#Su+LKGGHJCqX!CkCOh z@pNi*K1xH-pO9(jYe)bj9ioM5*V=IxOV`ZWZ;PCb+znoZBi(EfgBUwKZ z5HU_`I9Tv#+AJSN)O<1PfxUbBkcpKANr{c7?k(R`uR)o$`D}t`lRF}BlD2>GZpWXR zT-n?vg44t5!^RcU`%R*)RBBXcRn~De<(mG!uQxjj-6&7_l2XJYQa)b($04los&P`D z(;}vh*F;2$8!p%tNSC@$c05u2i6_jqBjfX(`nhr(M>gjt7-@rJ{fZsb zChxcQ2-Oy9BWuHTm}ThS(9H)iTQQ&acAegL5w|^JM^5C`;WIVHi)|V{COz$|=bifr zE^IE!^C!bf!|^suNY|K^DeZjix=wTZhU2YY@@?|kt8o5NBXZ}T&a}>l%eD>Gj*IE+ zrsL<9!)q~Xnw(2X`$$-j{`K<6Mc5&{_@em6=dU-B&(iX4`#*zBOPJ~WSj}IP>dld} z!n2kR{ewEc%N9c6v-nYF_#)Pab_+CKEVa~$9;%pbvC3q4i z9`7fi^g|lvAHjo{T29q+EDUFn>)wD#Td)4yi669$Hn|a8xo8jC{EW?E!iyz99|?2@U`>~#c7s+hxfL?inCYuo+j2PSAb-{z(GX>q5Yeh2h)un&)$GGZlp}bR z8kP2-)lfv-b=vV!*NMz%t~da%ta`s;Km9ykhpuGd8L_}7cb)zNPK(d%jIIbg}e;=cO(1~ z_5lRCKdhoC_tqEZFo(2GSC?U+YgoY)m!08bmTgINNrszxifS+A3?v(Cga z^;ECQ)cHkNnA9fUSyu5;QsM>XmwllKWG5jg_!9C14E7sx37U24-dgFC2ngE$3LiyR zGi=rbU7%~-FaS0IIq=Jcj)K#`Hr<)w%n3HBV(llCA+m64dv`qN6v%{9gt%xfH>pW+ z27wNd105K z6)vj^779tJ|6}mz%cBq+>yZCG`TvK;n1D{HVT^2~0DDe*`a)4XLa&zc+TFp%W$)c_ z^C6_&)lNisv)q`+hN>I%yLalxWOE)B!)=ywc5`Z@V-WIs5)y%;g<-Y#Z@!%jg|J2k zV*Ig*PY0YX?q@##W5!E`---J+X{XICO1-@$iLUtau&HA()GlSEbVcTHPIn_?*jed% zWcl_~U->>>8D&OXyOQ-`g{o0gCpKC?T9o5!M3ErXZoSKX;BSngr|^O~owjQ*&_{Z^ z{3JwLt(yn07CpsX3t+WUOMHJ(bkfS^I+NqrH`$?|yIw1LDwIco2(+kbwsHoR%mdC` z%?leqIcT4wL~gw7W>}ND^+>JwvQjl4?gLp^hOlI{0f+F#uMwpL$73l7`HMdaY`rD^ zVvtfS9RzzN#+?#FY!0G*?BsfKRn0!Uwdn%)>zJ~|c@4$7h;M7{rrG?1h|;GR136_p zXZ#pGQ^gIgR%P|McEsvbPsLPE!V>Q27yGZqC~Iy4zVw4${f##DHWccET1+-`o96tY z4Ydlk_f(kf&1Bc?VAk@W?gn`jdES#Tz)qq4pr2#&!0>t{4slL;dD zds8{qYHB@~+WC}e;kY$Hxhwhk)c$U%z|jtNnsqMDevX#?ckUVlgm=-zreo{+P%dq}N4|))10|jy?!T(zX6MwmUJ&g6VRj z9(M0zNPn$3UID0-XP}MLiHg3QCPwFqbDYYYJ z4`zm0FoGw?cISlb*xT?fa*}KyEyNWp>nwiWZ;@V>66vn!_lL+P+!?*UC>1)gP242C zeXlhK%)E}_e$l;l#rM0$Oy3a?`RZp+#SN$5>^8$C(Rs(nMM6@@Lrgp{ApiYtkYT@N z-Mc@L&D_Np!C52xA~|;|1xFdx^ts^ zrI!yB`*`%ZK-mxQ))0JBb%WEwP2I!&Gy8XDy z=&<&ad?i3zz3~S@Lf6Oa$3N^JU$pfpUcCCH#GCS4JV=ooo7;t6DrZOB%>$uI0(6sn z$)ueP1rA0dj8Y2`xK*F;`hrJ1%qo$Y_g_AcC4OPc3O_9y%uZMwJoIiIq(?8>!zxj8 zJuDc+seaD2V4}VfdOyDoaHX4$is8__Ev%`mA%uF#vdo+1HWXHWmQYedD-_~b>@)Mv zwXIH))m&RTIBf6;3VSl|fSOa;7)GAYW}np!qPQ{dwVuQRX->ygl;g#-kA5$1X~sK8 z8yf15LjHDhj*n;W%ONjz1m;{(;LY+UkLzqC&)I*9A!HOBqt(0&+o(Iff}Y)C*gZ`z z0K}XD#OBN5yaRU(Ta4$HIqkJ$JOs@1JC2S6P_qL83CzxN`0*qF?PIh!E_+SNVcw>z^ z|G?Nd3MAQJ_@9Px)xOPMf4lsZM+UKHg2Om)OCp4@ea z!)G1o%-^BRz8hFCy%uNID$f8seOT7^9+y|AWOpanJ5w@!&SgaK zFgN#YA(ONtko#l8>3h!8Eu@G%*U8!ZN8&FxD|QxR7aDnej?*qDsSDTLoUTQh^}>Nc zX-BHP3Wvk1R|Kjw$rd*A^c%T(mP6^F!EheO3Ap&W66bRqGPdV0iz3>M4bnReX@dOgI@Q>$$BVFXcIeA|5!T9< zC)d#ADqI4su&I5I)#netM{}UsK=8G(l4`T6m+RjH6AH~lRa$<5Vv_Dk2+c*&&?2|{ zBx0mHO_4w!xf-w+TR%y&F)BBtbQz9AXY)mbdbn_# zj`3BniC09C2#cx(k*{r|(0W;(tdf~igRw6%8RC6l*H`RgmWZeEZ_?3UvHd&GHAK+~ z3c`D`P#ao9;X4A?8)*?0jWYNHM84~Iylb}x>T{U)<_SQtf{AhFkvXO%4~rJdu@;mh z#5z(CU5*)KJ4^z>}<|!M2}_88(e~DR38B? zEL=U7R5hJ6fpLvBXA~Ee8RVxr=@X17>bg6v6}ozv%>7}(IT5H>5Jj;ii!~`$8I`%` zU<`Tf<%#|qubKP?!n}ItT*-oKBUbzc0IdoDkQ=Fa7*40mZu=<8@?e z2u09d)m&v`>IX@Kd6y~j=sp4rjuK6w3j67?`q>2Xxi#O*&cCMH=ck!Y)xO$gUuifS zFcSMc8}R99EKp#2w41PyBQewJAA<_WlYqf_8?cTgeNe;kzLn}hHx$ASX!9@`#X!3B zZv^O%Ru-iu_N&qY7~R{jMjs-~#_Pq6NL7HqTN@e@@4bD3Z5$;;eNKBCT)oYwp_qstwF^OmJeF4D@) z>vC4`f_hIf&g?tDkmgzNP*it z#-+dp`?SF@WI(va`U0>na?HcS5=FG(znT_!Oc+~b)#ldp-RLqB$AG#zdno&756jj+L6A{~TqsNmuy-R>sM!vm=Dx_Eo+)G$R`@1v_8d3M| z78$<>+?Y-zJaR|9x2XB^od5FA^~c0+1tn=2z=lkRWi#2i@aOF#`pkch&j!@rwgFk# zHcs2R+L{d%fWDi5=cgetZ%~ z62z6DmiUN54`?KjohZ~7XkVurG0Kn3H9&Ym&=@xG8n6NEPv&J050Stj>t^QpNEsCg zfTmd!MT5Hq76N&GBa&x(2+ZMC8~Yog3?K&o9q4}*MxhbEJd4MTY2|() // TODO maybe call imageLoader directly ? may require some DI changes + + val result = context.imageLoader + .execute( + ImageRequest.Builder(context) + .data(feedUrl) + .allowHardware(false) + .build() + ).drawable as BitmapDrawable + + val palette = Palette.from(result.bitmap).generate() + + val dominantSwatch = palette.dominantSwatch + return if (dominantSwatch != null && !isColorTooBright(dominantSwatch.rgb) + && !isColorTooDark(dominantSwatch.rgb)) { + dominantSwatch.rgb + } else 0 + } + + private fun isColorTooBright(@ColorInt color: Int): Boolean { + return getColorLuma(color) > 210 + } + + private fun isColorTooDark(@ColorInt color: Int): Boolean { + return getColorLuma(color) < 40 + } + + private fun getColorLuma(@ColorInt color: Int): Double { + val r = color shr 16 and 0xff + val g = color shr 8 and 0xff + val b = color shr 0 and 0xff + return 0.2126 * r + 0.7152 * g + 0.0722 * b + } +} \ No newline at end of file From f67c3f8892ce53d69ade2f011c4921a44ca2a2d7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Aug 2023 21:48:56 +0200 Subject: [PATCH 41/95] Use feed icon and feed color in TimelineItem --- .../repositories/LocalRSSRepository.kt | 10 ++++++- .../app/compose/timelime/TimelineItem.kt | 28 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index 61c590ba..ac2409e6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -3,6 +3,8 @@ package com.readrops.app.compose.repositories import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.SyncResult import com.readrops.api.utils.ApiUtils +import com.readrops.api.utils.HtmlParser +import com.readrops.app.compose.utils.FeedColors import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Item @@ -11,12 +13,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Headers import org.jsoup.Jsoup +import org.koin.core.component.KoinComponent +import org.koin.core.component.get class LocalRSSRepository( private val dataSource: LocalRSSDataSource, database: Database, account: Account -) : BaseRepository(database, account) { +) : BaseRepository(database, account), KoinComponent { override suspend fun login() { /* useless here */ } @@ -108,6 +112,10 @@ class LocalRSSRepository( etag = null lastModified = null + iconUrl = HtmlParser.getFaviconLink(siteUrl!!, get()).also { feedUrl -> + feedUrl?.let { backgroundColor = FeedColors.getFeedColor(it) } + } + id = database.newFeedDao().insert(this).toInt() } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index eabc87bf..6aa03fa7 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.FavoriteBorder @@ -16,10 +17,12 @@ import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow @@ -67,9 +70,10 @@ fun TimelineItem( Row( verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = R.drawable.ic_rss_feed_grey), + AsyncImage( + model = itemWithFeed.feedIconUrl, contentDescription = null, + placeholder = painterResource(R.drawable.ic_rss_feed_grey), modifier = Modifier.size(MaterialTheme.typography.labelLarge.fontSize.value.dp) ) @@ -94,10 +98,20 @@ fun TimelineItem( } } - Text( - text = DateUtils.formattedDateByLocal(itemWithFeed.item.pubDate!!), - style = MaterialTheme.typography.labelMedium, - ) + Surface( + color = if (itemWithFeed.bgColor != 0) Color(itemWithFeed.bgColor) else MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(48.dp) + ) { + Text( + text = DateUtils.formattedDateByLocal(itemWithFeed.item.pubDate!!), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding( + horizontal = MaterialTheme.spacing.shortSpacing, + vertical = MaterialTheme.spacing.veryShortSpacing + ) + ) + } } ShortSpacer() @@ -140,7 +154,7 @@ fun TimelineItem( contentDescription = null, modifier = Modifier.clickable { onReadLater() } ) - + Icon( imageVector = Icons.Outlined.Share, contentDescription = null, From fffbf18ad94e0b5600f7b8243505797d4b935f47 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Aug 2023 22:44:44 +0200 Subject: [PATCH 42/95] Update android gradle plugin --- api/build.gradle | 6 +++--- app/build.gradle | 6 +++--- appcompose/build.gradle | 6 +++--- build.gradle | 6 +++--- db/build.gradle | 6 +++--- gradle.properties | 3 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- 7 files changed, 18 insertions(+), 17 deletions(-) diff --git a/api/build.gradle b/api/build.gradle index c1aaf9bf..05dfdb5b 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -30,11 +30,11 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } lint { abortOnError false diff --git a/app/build.gradle b/app/build.gradle index e295aa7a..fadffdc3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,11 +38,11 @@ android { compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } buildFeatures { diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 751f7402..1c4e69ec 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -34,12 +34,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } buildFeatures { diff --git a/build.gradle b/build.gradle index 4c674d51..15978c13 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jacoco:org.jacoco.core:0.8.7" } @@ -38,7 +38,7 @@ ext { koin_version = "3.3.3" } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } @@ -47,7 +47,7 @@ jacoco { } -task jacocoFullReport(type: JacocoReport) { +tasks.register('jacocoFullReport', JacocoReport) { group = 'Reporting' description = "Generate Jacoco coverage reports for the debug build" diff --git a/db/build.gradle b/db/build.gradle index 6eb8ff23..ed739e25 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -43,11 +43,11 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '17' } lint { abortOnError false diff --git a/gradle.properties b/gradle.properties index 7363cef4..483fa846 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,9 +9,10 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m -android.databinding.incremental=true kapt.incremental.apt=true org.gradle.parallel=true android.defaults.buildfeatures.buildconfig=false +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7bd21ed8..47e2bb71 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip From 8e8b0abf8084b2ccfcec1ab6ce2fda11a182020e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 15 Aug 2023 22:56:26 +0200 Subject: [PATCH 43/95] Rename utils package to util --- .../java/com/readrops/app/compose/FeedColorsTest.kt | 2 +- .../src/main/java/com/readrops/app/compose/MainActivity.kt | 2 +- .../readrops/app/compose/repositories/LocalRSSRepository.kt | 2 +- .../java/com/readrops/app/compose/timelime/TimelineItem.kt | 6 +++--- .../com/readrops/app/compose/{utils => util}/FeedColors.kt | 2 +- .../com/readrops/app/compose/{utils => util}/theme/Color.kt | 2 +- .../readrops/app/compose/{utils => util}/theme/Spacing.kt | 2 +- .../com/readrops/app/compose/{utils => util}/theme/Theme.kt | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/{utils => util}/FeedColors.kt (97%) rename appcompose/src/main/java/com/readrops/app/compose/{utils => util}/theme/Color.kt (98%) rename appcompose/src/main/java/com/readrops/app/compose/{utils => util}/theme/Spacing.kt (94%) rename appcompose/src/main/java/com/readrops/app/compose/{utils => util}/theme/Theme.kt (98%) diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt index 8271e781..137675f9 100644 --- a/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/FeedColorsTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import com.readrops.api.apiModule import com.readrops.api.utils.ApiUtils -import com.readrops.app.compose.utils.FeedColors +import com.readrops.app.compose.util.FeedColors import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 1a8769fa..03c0df30 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -10,7 +10,7 @@ import cafe.adriel.voyager.transitions.FadeTransition import com.readrops.app.compose.account.selection.AccountSelectionScreen import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.home.HomeScreen -import com.readrops.app.compose.utils.theme.ReadropsTheme +import com.readrops.app.compose.util.theme.ReadropsTheme import org.koin.androidx.viewmodel.ext.android.getViewModel class MainActivity : ComponentActivity() { diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index ac2409e6..9fb3d66e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -4,7 +4,7 @@ import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.services.SyncResult import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.HtmlParser -import com.readrops.app.compose.utils.FeedColors +import com.readrops.app.compose.util.FeedColors import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Item diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index 6aa03fa7..2608420e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -30,9 +30,9 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.readrops.api.utils.DateUtils import com.readrops.app.compose.R -import com.readrops.app.compose.utils.theme.ShortSpacer -import com.readrops.app.compose.utils.theme.VeryShortSpacer -import com.readrops.app.compose.utils.theme.spacing +import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.theme.VeryShortSpacer +import com.readrops.app.compose.util.theme.spacing import com.readrops.db.pojo.ItemWithFeed @Composable diff --git a/appcompose/src/main/java/com/readrops/app/compose/utils/FeedColors.kt b/appcompose/src/main/java/com/readrops/app/compose/util/FeedColors.kt similarity index 97% rename from appcompose/src/main/java/com/readrops/app/compose/utils/FeedColors.kt rename to appcompose/src/main/java/com/readrops/app/compose/util/FeedColors.kt index 3f2fc6af..f487ddc8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/utils/FeedColors.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/FeedColors.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.utils +package com.readrops.app.compose.util import android.content.Context import android.graphics.drawable.BitmapDrawable diff --git a/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Color.kt b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Color.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/utils/theme/Color.kt rename to appcompose/src/main/java/com/readrops/app/compose/util/theme/Color.kt index 196fef21..6b21d590 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Color.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Color.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.utils.theme +package com.readrops.app.compose.util.theme import androidx.compose.ui.graphics.Color diff --git a/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Spacing.kt b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt similarity index 94% rename from appcompose/src/main/java/com/readrops/app/compose/utils/theme/Spacing.kt rename to appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt index cdcf12f2..2d1045ce 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Spacing.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.utils.theme +package com.readrops.app.compose.util.theme import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size diff --git a/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Theme.kt b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Theme.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/utils/theme/Theme.kt rename to appcompose/src/main/java/com/readrops/app/compose/util/theme/Theme.kt index d06d61c7..fa076746 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/utils/theme/Theme.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.utils.theme +package com.readrops.app.compose.util.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme From d13048be70f6d3c2bb2537f4f47d50be0bb1e5fc Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 17 Aug 2023 22:52:15 +0200 Subject: [PATCH 44/95] Improve TimelineItem large layout --- .../repositories/LocalRSSRepository.kt | 8 +- .../app/compose/timelime/TimelineItem.kt | 118 +++++++++++------- .../app/compose/timelime/TimelineTab.kt | 28 ++++- .../readrops/app/compose/util/Extensions.kt | 7 ++ .../com/readrops/app/compose/util/Utils.kt | 11 ++ .../app/compose/util/components/IconText.kt | 50 ++++++++ .../src/main/res/drawable/ic_folder_grey.xml | 5 + .../main/res/drawable/ic_hourglass_empty.xml | 5 + .../src/main/res/drawable/ic_read_later.xml | 5 + 9 files changed, 187 insertions(+), 50 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/Extensions.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/Utils.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt create mode 100644 appcompose/src/main/res/drawable/ic_folder_grey.xml create mode 100644 appcompose/src/main/res/drawable/ic_hourglass_empty.xml create mode 100644 appcompose/src/main/res/drawable/ic_read_later.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt index 9fb3d66e..f3879ade 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/LocalRSSRepository.kt @@ -5,6 +5,7 @@ import com.readrops.api.services.SyncResult import com.readrops.api.utils.ApiUtils import com.readrops.api.utils.HtmlParser import com.readrops.app.compose.util.FeedColors +import com.readrops.app.compose.util.Utils import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Item @@ -63,7 +64,8 @@ class LocalRSSRepository( return Pair(syncResult, ErrorResult(errors)) } - override suspend fun synchronize(): SyncResult = throw NotImplementedError("This method can't be called here") + override suspend fun synchronize(): SyncResult = + throw NotImplementedError("This method can't be called here") override suspend fun insertNewFeeds(urls: List) = withContext(Dispatchers.IO) { @@ -88,9 +90,9 @@ class LocalRSSRepository( } if (item.content != null) { - item.readTime = 0.0 + item.readTime = Utils.readTimeFromString(item.content!!) } else if (item.description != null) { - item.readTime = 0.0 + item.readTime = Utils.readTimeFromString(item.cleanDescription!!) } item.feedId = feed.id diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index 2608420e..0bdd4e28 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -3,7 +3,6 @@ package com.readrops.app.compose.timelime import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.Card @@ -25,15 +23,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.readrops.api.utils.DateUtils import com.readrops.app.compose.R +import com.readrops.app.compose.util.components.IconText import com.readrops.app.compose.util.theme.ShortSpacer import com.readrops.app.compose.util.theme.VeryShortSpacer import com.readrops.app.compose.util.theme.spacing import com.readrops.db.pojo.ItemWithFeed +import kotlin.math.roundToInt @Composable fun TimelineItem( @@ -45,17 +46,11 @@ fun TimelineItem( ) { Card( modifier = Modifier - .padding( - PaddingValues( - horizontal = MaterialTheme.spacing.shortSpacing, - vertical = MaterialTheme.spacing.veryShortSpacing - ) - ) + .padding(horizontal = MaterialTheme.spacing.shortSpacing) .clickable { onClick() } ) { Column( - modifier = Modifier - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() ) { Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -67,51 +62,77 @@ fun TimelineItem( top = MaterialTheme.spacing.shortSpacing, ) ) { - Row( - verticalAlignment = Alignment.CenterVertically, + Column( + modifier = Modifier.weight(1f) ) { - AsyncImage( - model = itemWithFeed.feedIconUrl, - contentDescription = null, - placeholder = painterResource(R.drawable.ic_rss_feed_grey), - modifier = Modifier.size(MaterialTheme.typography.labelLarge.fontSize.value.dp) - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + model = itemWithFeed.feedIconUrl, + contentDescription = null, + placeholder = painterResource(R.drawable.ic_rss_feed_grey), + modifier = Modifier.size(24.dp) + ) - VeryShortSpacer() + VeryShortSpacer() - Column { Text( text = itemWithFeed.feedName, style = MaterialTheme.typography.labelMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, + color = if (itemWithFeed.bgColor != 0) Color(itemWithFeed.bgColor) else MaterialTheme.colorScheme.onSurface, ) - - VeryShortSpacer() - - if (itemWithFeed.folder != null) { - Text( - text = itemWithFeed.folder!!.name!!, - style = MaterialTheme.typography.labelMedium - ) - } } } - Surface( - color = if (itemWithFeed.bgColor != 0) Color(itemWithFeed.bgColor) else MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(48.dp) - ) { - Text( - text = DateUtils.formattedDateByLocal(itemWithFeed.item.pubDate!!), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onPrimary, - modifier = Modifier.padding( - horizontal = MaterialTheme.spacing.shortSpacing, - vertical = MaterialTheme.spacing.veryShortSpacing + Row { + Surface( + color = if (itemWithFeed.bgColor != 0) Color(itemWithFeed.bgColor) else MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(48.dp) + ) { + Text( + text = DateUtils.formattedDateByLocal(itemWithFeed.item.pubDate!!), + style = MaterialTheme.typography.labelMedium, + color = if (itemWithFeed.bgColor != 0) Color.White else MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding( + horizontal = MaterialTheme.spacing.shortSpacing, + vertical = MaterialTheme.spacing.veryShortSpacing + ) ) + } + } + } + + ShortSpacer() + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier + .fillMaxWidth() + .padding(start = MaterialTheme.spacing.shortSpacing) + ) { + if (itemWithFeed.folder != null) { + IconText( + icon = painterResource(id = R.drawable.ic_folder_grey), + text = itemWithFeed.folder!!.name!!, + style = MaterialTheme.typography.labelMedium + ) + + Text( + text = "·", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = MaterialTheme.spacing.veryShortSpacing) ) } + + IconText( + icon = painterResource(id = R.drawable.ic_hourglass_empty), + text = if (itemWithFeed.item.readTime < 1) "> 1 min" else "${itemWithFeed.item.readTime.roundToInt()} mins", + style = MaterialTheme.typography.labelMedium + ) } ShortSpacer() @@ -121,18 +142,31 @@ fun TimelineItem( style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.Bold, modifier = Modifier.padding(horizontal = MaterialTheme.spacing.shortSpacing) ) ShortSpacer() + if (itemWithFeed.item.cleanDescription != null) { + Text( + text = itemWithFeed.item.cleanDescription!!, + style = MaterialTheme.typography.bodyMedium, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = MaterialTheme.spacing.shortSpacing) + ) + + ShortSpacer() + } + if (itemWithFeed.item.imageLink != null) { AsyncImage( model = itemWithFeed.item.imageLink, contentDescription = itemWithFeed.item.title!!, contentScale = ContentScale.Crop, modifier = Modifier - .aspectRatio(3f / 2f) + .aspectRatio(16f / 9f) .fillMaxWidth() ) } @@ -150,7 +184,7 @@ fun TimelineItem( ) Icon( - imageVector = Icons.Outlined.Add, // placeholder icon + painter = painterResource(id = R.drawable.ic_read_later), contentDescription = null, modifier = Modifier.clickable { onReadLater() } ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 59b0d2bf..ee989059 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -25,6 +27,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.readrops.app.compose.item.ItemScreen +import com.readrops.app.compose.util.theme.spacing import org.koin.androidx.compose.getViewModel @@ -46,6 +49,7 @@ object TimelineTab : Tab { val state by viewModel.timelineState.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState() + val scrollState = rememberLazyListState() val swipeToRefreshState = rememberSwipeRefreshState(isRefreshing) val navigator = LocalNavigator.currentOrThrow @@ -65,14 +69,32 @@ object TimelineTab : Tab { modifier = Modifier.padding(paddingValues) ) { when (state) { + is TimelineState.Loading -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator() + } + } + + is TimelineState.Error -> { + + } + is TimelineState.Loaded -> { val items = (state as TimelineState.Loaded).items if (items.isNotEmpty()) { - LazyColumn { + LazyColumn( + state = scrollState, + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing) + ) { items( items = items, key = { it.item.id }, + contentType = { "item_with_feed" } ) { itemWithFeed -> TimelineItem( itemWithFeed = itemWithFeed, @@ -87,10 +109,6 @@ object TimelineTab : Tab { NoItemPlaceholder() } } - - else -> { - NoItemPlaceholder() - } } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/Extensions.kt b/appcompose/src/main/java/com/readrops/app/compose/util/Extensions.kt new file mode 100644 index 00000000..f7af6d84 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/Extensions.kt @@ -0,0 +1,7 @@ +package com.readrops.app.compose.util + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +fun TextStyle.toDp(): Dp = fontSize.value.dp \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/Utils.kt b/appcompose/src/main/java/com/readrops/app/compose/util/Utils.kt new file mode 100644 index 00000000..24244b02 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/Utils.kt @@ -0,0 +1,11 @@ +package com.readrops.app.compose.util + +object Utils { + + private const val AVERAGE_WORDS_PER_MINUTE = 250 + + fun readTimeFromString(value: String): Double { + val nbWords = value.split("\\s+").size + return nbWords.toDouble() / AVERAGE_WORDS_PER_MINUTE + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt new file mode 100644 index 00000000..d5fb784b --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt @@ -0,0 +1,50 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import com.readrops.app.compose.util.theme.spacing +import com.readrops.app.compose.util.toDp + +@Composable +fun IconText( + icon: Painter, + text: String, + style: TextStyle, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current, + padding: Dp = MaterialTheme.spacing.veryShortSpacing, + onClick: (() -> Unit)? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = if (onClick != null) modifier.clickable { onClick() } else modifier, + ) { + Icon( + painter = icon, + tint = tint, + contentDescription = null, + modifier = Modifier.size(style.toDp()), + ) + + Spacer(Modifier.width(padding)) + + Text( + text = text, + style = style, + ) + } +} \ No newline at end of file diff --git a/appcompose/src/main/res/drawable/ic_folder_grey.xml b/appcompose/src/main/res/drawable/ic_folder_grey.xml new file mode 100644 index 00000000..f27a93ac --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_folder_grey.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_hourglass_empty.xml b/appcompose/src/main/res/drawable/ic_hourglass_empty.xml new file mode 100644 index 00000000..76d02ef7 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_hourglass_empty.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_read_later.xml b/appcompose/src/main/res/drawable/ic_read_later.xml new file mode 100644 index 00000000..c1c86f62 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_read_later.xml @@ -0,0 +1,5 @@ + + + From 282e3dd9f86cc90ff4eb8f0e4f0fa3cdae91aca7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 18 Aug 2023 18:17:14 +0200 Subject: [PATCH 45/95] Add TimelineTab topBar icons --- .../app/compose/timelime/TimelineTab.kt | 42 ++++++++++++++++++- .../src/main/res/drawable/ic_done_all.xml | 5 +++ .../src/main/res/drawable/ic_filter_list.xml | 5 +++ appcompose/src/main/res/drawable/ic_sync.xml | 5 +++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 appcompose/src/main/res/drawable/ic_done_all.xml create mode 100644 appcompose/src/main/res/drawable/ic_filter_list.xml create mode 100644 appcompose/src/main/res/drawable/ic_sync.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index ee989059..2e8a1690 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -9,8 +9,13 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -20,12 +25,14 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState +import com.readrops.app.compose.R import com.readrops.app.compose.item.ItemScreen import com.readrops.app.compose.util.theme.spacing import org.koin.androidx.compose.getViewModel @@ -57,9 +64,40 @@ object TimelineTab : Tab { Scaffold( topBar = { TopAppBar( - title = { Text(text = "Timeline") } + title = { Text(text = "Timeline") }, + navigationIcon = { + IconButton(onClick = { }) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = null + ) + } + }, + actions = { + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_filter_list), + contentDescription = null + ) + } + + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_sync), + contentDescription = null + ) + } + } ) - } + }, + floatingActionButton = { + FloatingActionButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_done_all), + contentDescription = null + ) + } + }, ) { paddingValues -> SwipeRefresh( state = swipeToRefreshState, diff --git a/appcompose/src/main/res/drawable/ic_done_all.xml b/appcompose/src/main/res/drawable/ic_done_all.xml new file mode 100644 index 00000000..9635085d --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_done_all.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_filter_list.xml b/appcompose/src/main/res/drawable/ic_filter_list.xml new file mode 100644 index 00000000..b8090430 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_filter_list.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_sync.xml b/appcompose/src/main/res/drawable/ic_sync.xml new file mode 100644 index 00000000..6c2e2b73 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_sync.xml @@ -0,0 +1,5 @@ + + + From 005858b8cd1ab91b88053c287e34120ae3f0d2bd Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 18 Aug 2023 22:45:04 +0200 Subject: [PATCH 46/95] Add initial drawer with default items --- .../app/compose/timelime/TimelineDrawer.kt | 99 +++++++++ .../app/compose/timelime/TimelineTab.kt | 190 ++++++++++-------- .../app/compose/util/theme/Spacing.kt | 3 +- appcompose/src/main/res/drawable/ic_new.xml | 5 + 4 files changed, 210 insertions(+), 87 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt create mode 100644 appcompose/src/main/res/drawable/ic_new.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt new file mode 100644 index 00000000..e92a3023 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt @@ -0,0 +1,99 @@ +package com.readrops.app.compose.timelime + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalDrawerSheet +import androidx.compose.material3.NavigationDrawerItem +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.spacing + +@Composable +fun TimelineDrawer( + viewModel: TimelineViewModel, +) { + ModalDrawerSheet { + Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing)) + + DrawerDefaultItems() + + DrawerDivider() + } +} + +@Composable +fun DrawerDefaultItems() { + NavigationDrawerItem( + label = { Text("Articles") }, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_timeline), + contentDescription = null + ) + }, + selected = true, + onClick = { }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + + NavigationDrawerItem( + label = { Text("New articles") }, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_new), + contentDescription = null + ) + }, + selected = false, + onClick = { }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + + NavigationDrawerItem( + label = { Text("Favorites") }, + icon = { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = null + ) + }, + selected = false, + onClick = { }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + + NavigationDrawerItem( + label = { Text("To read later") }, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_read_later), + contentDescription = null + ) + }, + selected = false, + onClick = { }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) +} + +@Composable +fun DrawerDivider() { + Divider( + thickness = 2.dp, + modifier = Modifier.padding( + vertical = MaterialTheme.spacing.drawerSpacing, + horizontal = 28.dp // M3 guidelines + ) + ) +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 2e8a1690..4f07d3ea 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -12,17 +12,21 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -35,6 +39,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.readrops.app.compose.R import com.readrops.app.compose.item.ItemScreen import com.readrops.app.compose.util.theme.spacing +import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel @@ -56,102 +61,115 @@ object TimelineTab : Tab { val state by viewModel.timelineState.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState() - val scrollState = rememberLazyListState() - val swipeToRefreshState = rememberSwipeRefreshState(isRefreshing) - val navigator = LocalNavigator.currentOrThrow - Scaffold( - topBar = { - TopAppBar( - title = { Text(text = "Timeline") }, - navigationIcon = { - IconButton(onClick = { }) { - Icon( - imageVector = Icons.Default.Menu, - contentDescription = null - ) - } - }, - actions = { - IconButton(onClick = { }) { - Icon( - painter = painterResource(id = R.drawable.ic_filter_list), - contentDescription = null - ) - } + val scrollState = rememberLazyListState() + val swipeToRefreshState = rememberSwipeRefreshState(isRefreshing) + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val scope = rememberCoroutineScope() - IconButton(onClick = { }) { - Icon( - painter = painterResource(id = R.drawable.ic_sync), - contentDescription = null - ) - } - } - ) - }, - floatingActionButton = { - FloatingActionButton(onClick = { }) { - Icon( - painter = painterResource(id = R.drawable.ic_done_all), - contentDescription = null - ) - } - }, - ) { paddingValues -> - SwipeRefresh( - state = swipeToRefreshState, - onRefresh = { - viewModel.refreshTimeline() - }, - modifier = Modifier.padding(paddingValues) - ) { - when (state) { - is TimelineState.Loading -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxSize() - ) { - CircularProgressIndicator() - } - } - - is TimelineState.Error -> { - - } - - is TimelineState.Loaded -> { - val items = (state as TimelineState.Loaded).items - - if (items.isNotEmpty()) { - LazyColumn( - state = scrollState, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing) - ) { - items( - items = items, - key = { it.item.id }, - contentType = { "item_with_feed" } - ) { itemWithFeed -> - TimelineItem( - itemWithFeed = itemWithFeed, - onClick = { navigator.push(ItemScreen()) }, - onFavorite = {}, - onReadLater = {}, - onShare = {}, - ) + ModalNavigationDrawer( + drawerState = drawerState, + drawerContent = { + TimelineDrawer(viewModel = viewModel) + } + ) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(text = "Articles") }, + navigationIcon = { + IconButton( + onClick = { + scope.launch { + drawerState.open() + } } + ) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = null + ) + } + }, + actions = { + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_filter_list), + contentDescription = null + ) + } + + IconButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_sync), + contentDescription = null + ) + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = { }) { + Icon( + painter = painterResource(id = R.drawable.ic_done_all), + contentDescription = null + ) + } + }, + ) { paddingValues -> + SwipeRefresh( + state = swipeToRefreshState, + onRefresh = { + viewModel.refreshTimeline() + }, + modifier = Modifier.padding(paddingValues) + ) { + when (state) { + is TimelineState.Loading -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxSize() + ) { + CircularProgressIndicator() + } + } + + is TimelineState.Error -> { + + } + + is TimelineState.Loaded -> { + val items = (state as TimelineState.Loaded).items + + if (items.isNotEmpty()) { + LazyColumn( + state = scrollState, + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing) + ) { + items( + items = items, + key = { it.item.id }, + contentType = { "item_with_feed" } + ) { itemWithFeed -> + TimelineItem( + itemWithFeed = itemWithFeed, + onClick = { navigator.push(ItemScreen()) }, + onFavorite = {}, + onReadLater = {}, + onShare = {}, + ) + } + } + } else { + NoItemPlaceholder() } - } else { - NoItemPlaceholder() } } } } } - - } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt index 2d1045ce..67f2e448 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt @@ -15,7 +15,8 @@ data class Spacing( val shortSpacing: Dp = 8.dp, val mediumSpacing: Dp = 16.dp, val largeSpacing: Dp = 24.dp, - val veryLargeSpacing: Dp = 48.dp + val veryLargeSpacing: Dp = 48.dp, + val drawerSpacing: Dp = 12.dp ) val LocalSpacing = compositionLocalOf { Spacing() } diff --git a/appcompose/src/main/res/drawable/ic_new.xml b/appcompose/src/main/res/drawable/ic_new.xml new file mode 100644 index 00000000..437fdb50 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_new.xml @@ -0,0 +1,5 @@ + + + From 3f665e6ab51f85550a0e538b4ae1e35ba284c6e8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Aug 2023 12:22:25 +0200 Subject: [PATCH 47/95] Add logic to build drawer folders and feeds list --- appcompose/build.gradle | 2 + .../app/compose/GetFoldersWithFeedsTest.kt | 64 +++++++++++++++++++ .../repositories/GetFoldersWithFeeds.kt | 37 +++++++++++ db/src/main/java/com/readrops/db/Database.kt | 3 + .../com/readrops/db/dao/newdao/NewFeedDao.kt | 6 ++ .../readrops/db/dao/newdao/NewFolderDao.kt | 14 ++++ .../com/readrops/db/dao/newdao/NewItemDao.kt | 3 + 7 files changed, 129 insertions(+) create mode 100644 appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt create mode 100644 db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 1c4e69ec..577541f5 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -106,4 +106,6 @@ dependencies { implementation "io.coil-kt:coil:2.4.0" implementation "io.coil-kt:coil-compose:2.4.0" + + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" } \ No newline at end of file diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt new file mode 100644 index 00000000..b0b5ac2d --- /dev/null +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt @@ -0,0 +1,64 @@ +package com.readrops.app.compose + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import com.readrops.app.compose.repositories.GetFoldersWithFeeds +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.entities.account.AccountType +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import org.joda.time.LocalDateTime +import org.junit.Before +import org.junit.Test +import kotlin.test.assertTrue + +class GetFoldersWithFeedsTest { + + private lateinit var database: Database + private lateinit var getFoldersWithFeeds: GetFoldersWithFeeds + private val account = Account(accountType = AccountType.LOCAL) + + @Before + fun before() { + val context = ApplicationProvider.getApplicationContext() + database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build() + + runTest { + account.id = database.newAccountDao().insert(account).toInt() + + repeat(3) { time -> + database.newFolderDao().insert(Folder(name = "Folder $time", accountId = account.id)) + } + + repeat(2) { time -> + database.newFeedDao().insert(Feed(name = "Feed $time", accountId = account.id)) + } + + repeat(2) { time -> + database.newFeedDao().insert(Feed(name = "Feed ${time+2}", folderId = 1, accountId = account.id)) + } + + repeat(3) { time -> + database.newItemDao().insert(Item(title = "Item $time", feedId = 1, pubDate = LocalDateTime.now())) + } + } + } + + @Test + fun getFoldersWithFeedsTest() = runTest { + getFoldersWithFeeds = GetFoldersWithFeeds(database, StandardTestDispatcher(testScheduler)) + val foldersAndFeeds = getFoldersWithFeeds.get(account.id) + + assertTrue { foldersAndFeeds.size == 4 } + assertTrue { foldersAndFeeds.entries.first().value.size == 2 } + assertTrue { foldersAndFeeds.entries.last().key == null } + assertTrue { foldersAndFeeds[null]!!.size == 2 } + assertTrue { foldersAndFeeds[null]!!.first().unreadCount == 3 } + + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt new file mode 100644 index 00000000..0c8fd786 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -0,0 +1,37 @@ +package com.readrops.app.compose.repositories + +import com.readrops.db.Database +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class GetFoldersWithFeeds( + private val database: Database, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + + suspend fun get(accountId: Int): Map> = withContext(dispatcher) { + val foldersWithFeeds = mutableMapOf>() + val folders = database.newFolderDao().selectFoldersByAccount(accountId) + + for (folder in folders) { + val feeds = database.newFeedDao().selectFeedsByFolder(folder.id) + + for (feed in feeds) { + feed.unreadCount = database.newItemDao().selectUnreadCount(feed.id) + } + + foldersWithFeeds[folder] = feeds + } + + val feedsAlone = database.newFeedDao().selectFeedsAlone(accountId) + for (feed in feedsAlone) { + feed.unreadCount = database.newItemDao().selectUnreadCount(feed.id) + } + + foldersWithFeeds[null] = feedsAlone + foldersWithFeeds.toSortedMap(nullsLast()) + } +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt index 8b6c9834..02ce75ab 100644 --- a/db/src/main/java/com/readrops/db/Database.kt +++ b/db/src/main/java/com/readrops/db/Database.kt @@ -6,6 +6,7 @@ import androidx.room.TypeConverters import com.readrops.db.dao.* import com.readrops.db.dao.newdao.NewAccountDao import com.readrops.db.dao.newdao.NewFeedDao +import com.readrops.db.dao.newdao.NewFolderDao import com.readrops.db.dao.newdao.NewItemDao import com.readrops.db.entities.* import com.readrops.db.entities.account.Account @@ -35,4 +36,6 @@ abstract class Database : RoomDatabase() { abstract fun newItemDao(): NewItemDao abstract fun newAccountDao(): NewAccountDao + + abstract fun newFolderDao(): NewFolderDao } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index 471fefca..d66102fb 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -19,4 +19,10 @@ abstract class NewFeedDao : NewBaseDao { @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") abstract suspend fun feedExists(feedUrl: String, accountId: Int): Boolean + + @Query("Select * From Feed Where folder_id = :folderId") + abstract suspend fun selectFeedsByFolder(folderId: Int): List + + @Query("Select * From Feed Where account_id = :accountId And folder_id Is Null") + abstract suspend fun selectFeedsAlone(accountId: Int): List } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt new file mode 100644 index 00000000..c9e40e2c --- /dev/null +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -0,0 +1,14 @@ +package com.readrops.db.dao.newdao + +import androidx.room.Dao +import androidx.room.Query +import com.readrops.db.entities.Folder + +@Dao +abstract class NewFolderDao : NewBaseDao { + + @Query("Select * From Folder Where account_id = :accountId Order By name ASC") + abstract suspend fun selectFoldersByAccount(accountId: Int): List + + +} \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt index 7d9e97eb..c7703ac8 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -1,6 +1,7 @@ package com.readrops.db.dao.newdao import androidx.room.Dao +import androidx.room.Query import androidx.room.RawQuery import androidx.sqlite.db.SupportSQLiteQuery import com.readrops.db.entities.Feed @@ -16,5 +17,7 @@ abstract class NewItemDao : NewBaseDao { @RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class]) abstract fun selectAll(query: SupportSQLiteQuery): Flow> + @Query("Select count(*) From Item Where feed_id = :feedId And read = 0") + abstract fun selectUnreadCount(feedId: Int): Int } \ No newline at end of file From deb6426edffbb1fa2cd7277daa20049b646adad8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Aug 2023 12:38:10 +0200 Subject: [PATCH 48/95] Close Drawer on back pressed --- .../readrops/app/compose/timelime/TimelineTab.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 4f07d3ea..d38fdad8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose.timelime +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -64,10 +65,19 @@ object TimelineTab : Tab { val navigator = LocalNavigator.currentOrThrow val scrollState = rememberLazyListState() - val swipeToRefreshState = rememberSwipeRefreshState(isRefreshing) + val swipeState = rememberSwipeRefreshState(isRefreshing) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() + BackHandler( + enabled = drawerState.isOpen, + onBack = { + scope.launch { + drawerState.close() + } + } + ) + ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -119,7 +129,7 @@ object TimelineTab : Tab { }, ) { paddingValues -> SwipeRefresh( - state = swipeToRefreshState, + state = swipeState, onRefresh = { viewModel.refreshTimeline() }, From cffc102b209bb68550a8ee96aaaf8c57fb8615fd Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Aug 2023 14:10:17 +0200 Subject: [PATCH 49/95] Add initial drawer state in TimelineViewModel --- .../app/compose/timelime/TimelineDrawer.kt | 38 ++++++++++++++----- .../app/compose/timelime/TimelineTab.kt | 10 ++++- .../app/compose/timelime/TimelineViewModel.kt | 23 +++++++++++ 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt index e92a3023..7889a5a9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt @@ -13,27 +13,45 @@ import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.readrops.app.compose.R import com.readrops.app.compose.util.theme.spacing +enum class DrawerDefaultItemsSelection { + ARTICLES, + NEW, + FAVORITES, + READ_LATER +} + @Composable fun TimelineDrawer( viewModel: TimelineViewModel, + onClickDefaultItem: (DrawerDefaultItemsSelection) -> Unit, ) { + val state by viewModel.drawerState.collectAsState() + ModalDrawerSheet { Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing)) - DrawerDefaultItems() + DrawerDefaultItems( + selectedItem = state.selection, + onClick = { onClickDefaultItem(it) } + ) DrawerDivider() } } @Composable -fun DrawerDefaultItems() { +fun DrawerDefaultItems( + selectedItem: DrawerDefaultItemsSelection, + onClick: (DrawerDefaultItemsSelection) -> Unit, +) { NavigationDrawerItem( label = { Text("Articles") }, icon = { @@ -42,8 +60,8 @@ fun DrawerDefaultItems() { contentDescription = null ) }, - selected = true, - onClick = { }, + selected = selectedItem == DrawerDefaultItemsSelection.ARTICLES, + onClick = { onClick(DrawerDefaultItemsSelection.ARTICLES) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -55,8 +73,8 @@ fun DrawerDefaultItems() { contentDescription = null ) }, - selected = false, - onClick = { }, + selected = selectedItem == DrawerDefaultItemsSelection.NEW, + onClick = { onClick(DrawerDefaultItemsSelection.NEW) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -68,8 +86,8 @@ fun DrawerDefaultItems() { contentDescription = null ) }, - selected = false, - onClick = { }, + selected = selectedItem == DrawerDefaultItemsSelection.FAVORITES, + onClick = { onClick(DrawerDefaultItemsSelection.FAVORITES) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -81,8 +99,8 @@ fun DrawerDefaultItems() { contentDescription = null ) }, - selected = false, - onClick = { }, + selected = selectedItem == DrawerDefaultItemsSelection.READ_LATER, + onClick = { onClick(DrawerDefaultItemsSelection.READ_LATER) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index d38fdad8..65ffe134 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -81,7 +81,15 @@ object TimelineTab : Tab { ModalNavigationDrawer( drawerState = drawerState, drawerContent = { - TimelineDrawer(viewModel = viewModel) + TimelineDrawer( + viewModel = viewModel, + onClickDefaultItem = { + viewModel.updateDrawerDefaultItem(it) + scope.launch { + drawerState.close() + } + } + ) } ) { Scaffold( diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 331169fc..917505d2 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -1,8 +1,11 @@ package com.readrops.app.compose.timelime +import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope import com.readrops.app.compose.base.TabViewModel import com.readrops.db.Database +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder import com.readrops.db.pojo.ItemWithFeed import com.readrops.db.queries.ItemsQueryBuilder import com.readrops.db.queries.QueryFilters @@ -13,6 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch class TimelineViewModel( @@ -26,6 +30,9 @@ class TimelineViewModel( private var _isRefreshing = MutableStateFlow(false) val isRefreshing = _isRefreshing.asStateFlow() + private val _drawerState = MutableStateFlow(DrawerState()) + val drawerState = _drawerState.asStateFlow() + init { viewModelScope.launch(dispatcher) { accountEvent.consumeAsFlow().collectLatest { account -> @@ -50,11 +57,27 @@ class TimelineViewModel( _isRefreshing.value = false } } + + fun updateDrawerDefaultItem(selection: DrawerDefaultItemsSelection) { + _drawerState.update { it.copy(selection = selection) } + } } sealed class TimelineState { object Loading : TimelineState() + + @Immutable data class Error(val exception: Exception) : TimelineState() + + @Immutable data class Loaded(val items: List) : TimelineState() } +@Immutable +data class DrawerState( + val selection: DrawerDefaultItemsSelection = DrawerDefaultItemsSelection.ARTICLES, + val folderSelection: Int = 0, + val feedSelection: Int = 0, + val foldersAndFeeds: Map> = emptyMap() +) + From db2b08953388a2ad9a116b8b7b87757a7be1b03d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Aug 2023 17:31:35 +0200 Subject: [PATCH 50/95] Display folders and feeds in Drawer --- .../readrops/app/compose/ComposeAppModule.kt | 5 +- .../app/compose/timelime/TimelineTab.kt | 14 +- .../app/compose/timelime/TimelineViewModel.kt | 45 +++++- .../compose/timelime/drawer/DrawerFeedItem.kt | 59 ++++++++ .../timelime/drawer/DrawerFolderItem.kt | 140 ++++++++++++++++++ .../timelime/{ => drawer}/TimelineDrawer.kt | 77 +++++++++- .../app/compose/util/theme/Spacing.kt | 5 +- 7 files changed, 331 insertions(+), 14 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFeedItem.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt rename appcompose/src/main/java/com/readrops/app/compose/timelime/{ => drawer}/TimelineDrawer.kt (52%) diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index 4741a29e..b51dba3a 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -4,6 +4,7 @@ import com.readrops.app.compose.account.AccountViewModel import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.repositories.BaseRepository +import com.readrops.app.compose.repositories.GetFoldersWithFeeds import com.readrops.app.compose.repositories.LocalRSSRepository import com.readrops.app.compose.timelime.TimelineViewModel import com.readrops.db.entities.account.Account @@ -12,7 +13,7 @@ import org.koin.dsl.module val composeAppModule = module { - viewModel { TimelineViewModel(get()) } + viewModel { TimelineViewModel(get(), get()) } viewModel { FeedViewModel(get()) } @@ -20,6 +21,8 @@ val composeAppModule = module { viewModel { AccountViewModel(get()) } + single { GetFoldersWithFeeds(get()) } + // repositories factory { (account: Account) -> diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 65ffe134..51418e29 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -39,6 +39,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.readrops.app.compose.R import com.readrops.app.compose.item.ItemScreen +import com.readrops.app.compose.timelime.drawer.TimelineDrawer import com.readrops.app.compose.util.theme.spacing import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel @@ -78,6 +79,7 @@ object TimelineTab : Tab { } ) + val closeDrawer = { scope.launch { drawerState.close() } } ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -85,9 +87,15 @@ object TimelineTab : Tab { viewModel = viewModel, onClickDefaultItem = { viewModel.updateDrawerDefaultItem(it) - scope.launch { - drawerState.close() - } + closeDrawer() + }, + onFolderClick = { + viewModel.updateDrawerFolderSelection(it) + closeDrawer() + }, + onFeedClick = { + viewModel.updateDrawerFeedSelection(it) + closeDrawer() } ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 917505d2..2bb97d8f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -3,6 +3,8 @@ package com.readrops.app.compose.timelime import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope import com.readrops.app.compose.base.TabViewModel +import com.readrops.app.compose.repositories.GetFoldersWithFeeds +import com.readrops.app.compose.timelime.drawer.DrawerDefaultItemsSelection import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder @@ -11,6 +13,8 @@ import com.readrops.db.queries.ItemsQueryBuilder import com.readrops.db.queries.QueryFilters import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -21,6 +25,7 @@ import kotlinx.coroutines.launch class TimelineViewModel( private val database: Database, + private val getFoldersWithFeeds: GetFoldersWithFeeds, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : TabViewModel(database) { @@ -38,11 +43,23 @@ class TimelineViewModel( accountEvent.consumeAsFlow().collectLatest { account -> val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = account.id)) - database.newItemDao().selectAll(query) - .catch { _timelineState.value = TimelineState.Error(Exception(it)) } - .collect { - _timelineState.value = TimelineState.Loaded(it) + val items = async { + database.newItemDao().selectAll(query) + .catch { _timelineState.value = TimelineState.Error(Exception(it)) } + .collect { + _timelineState.value = TimelineState.Loaded(it) + } + } + + val drawer = async { + _drawerState.update { + it.copy( + foldersAndFeeds = getFoldersWithFeeds.get(account.id) + ) } + } + + awaitAll(items, drawer) } } } @@ -59,7 +76,21 @@ class TimelineViewModel( } fun updateDrawerDefaultItem(selection: DrawerDefaultItemsSelection) { - _drawerState.update { it.copy(selection = selection) } + _drawerState.update { + it.copy( + selection = selection, + selectedFolderId = 0, + selectedFeedId = 0, + ) + } + } + + fun updateDrawerFolderSelection(folderId: Int) { + _drawerState.update { it.copy(selectedFolderId = folderId, selectedFeedId = 0) } + } + + fun updateDrawerFeedSelection(feedId: Int) { + _drawerState.update { it.copy(selectedFeedId = feedId, selectedFolderId = 0) } } } @@ -76,8 +107,8 @@ sealed class TimelineState { @Immutable data class DrawerState( val selection: DrawerDefaultItemsSelection = DrawerDefaultItemsSelection.ARTICLES, - val folderSelection: Int = 0, - val feedSelection: Int = 0, + val selectedFolderId: Int = 0, + val selectedFeedId: Int = 0, val foldersAndFeeds: Map> = emptyMap() ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFeedItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFeedItem.kt new file mode 100644 index 00000000..5c937c63 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFeedItem.kt @@ -0,0 +1,59 @@ +package com.readrops.app.compose.timelime.drawer + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.readrops.app.compose.util.theme.DrawerSpacing + +@Composable +fun DrawerFeedItem( + label: @Composable () -> Unit, + icon: @Composable () -> Unit, + badge: @Composable () -> Unit, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val colors = NavigationDrawerItemDefaults.colors() + + Surface( + selected = selected, + onClick = onClick, + color = colors.containerColor(selected = selected).value, + shape = CircleShape, + modifier = modifier + .height(36.dp) + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 16.dp, end = 24.dp) + ) { + val iconColor = colors.iconColor(selected).value + CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) + + DrawerSpacing() + + Box(Modifier.weight(1f)) { + val labelColor = colors.textColor(selected).value + CompositionLocalProvider(LocalContentColor provides labelColor, content = label) + } + + DrawerSpacing() + + val badgeColor = colors.badgeColor(selected).value + CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge) + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt new file mode 100644 index 00000000..ec4f101e --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt @@ -0,0 +1,140 @@ +package com.readrops.app.compose.timelime.drawer + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.NavigationDrawerItemDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.DrawerSpacing +import com.readrops.db.entities.Feed + +@Composable +fun DrawerFolderItem( + label: @Composable () -> Unit, + icon: @Composable () -> Unit, + badge: @Composable () -> Unit, + selected: Boolean, + onClick: () -> Unit, + feeds: List, + selectedFeed: Int, + onFeedClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + val colors = NavigationDrawerItemDefaults.colors() + + var isExpanded by remember { mutableStateOf(false) } + val rotationState by animateFloatAsState( + targetValue = if (isExpanded) 180f else 0f, + label = "drawer item arrow rotation" + ) + + Column( + modifier = Modifier.animateContentSize( + animationSpec = tween( + durationMillis = 300, + easing = LinearOutSlowInEasing, + ) + ) + ) { + Surface( + selected = selected, + onClick = onClick, + color = colors.containerColor(selected = selected).value, + shape = CircleShape, + modifier = modifier + .height(56.dp) + .fillMaxWidth() + .animateContentSize( + animationSpec = tween( + durationMillis = 300, + easing = LinearOutSlowInEasing, + ) + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 16.dp, end = 24.dp) + ) { + val iconColor = colors.iconColor(selected).value + CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) + + DrawerSpacing() + + Box(Modifier.weight(1f)) { + val labelColor = colors.textColor(selected).value + CompositionLocalProvider(LocalContentColor provides labelColor, content = label) + } + + DrawerSpacing() + + val badgeColor = colors.badgeColor(selected).value + CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge) + + DrawerSpacing() + + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = null, + modifier = Modifier + .clickable { isExpanded = isExpanded.not() } + .rotate(rotationState), + ) + } + } + + if (isExpanded && feeds.isNotEmpty()) { + for (feed in feeds) { + DrawerFeedItem( + label = { + Text( + text = feed.name!!, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + icon = { + AsyncImage( + model = feed.iconUrl, + contentDescription = feed.name, + placeholder = painterResource(id = R.drawable.ic_folder_grey), + modifier = Modifier.size(24.dp) + ) + }, + badge = { Text(feed.unreadCount.toString()) }, + selected = feed.id == selectedFeed, + onClick = { onFeedClick(feed.id) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + } + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt similarity index 52% rename from appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt rename to appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt index 7889a5a9..6c7e73fa 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineDrawer.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt @@ -1,8 +1,12 @@ -package com.readrops.app.compose.timelime +package com.readrops.app.compose.timelime.drawer +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Divider @@ -17,8 +21,11 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.readrops.app.compose.R +import com.readrops.app.compose.timelime.TimelineViewModel import com.readrops.app.compose.util.theme.spacing enum class DrawerDefaultItemsSelection { @@ -32,10 +39,17 @@ enum class DrawerDefaultItemsSelection { fun TimelineDrawer( viewModel: TimelineViewModel, onClickDefaultItem: (DrawerDefaultItemsSelection) -> Unit, + onFolderClick: (Int) -> Unit, + onFeedClick: (Int) -> Unit, ) { val state by viewModel.drawerState.collectAsState() + val scrollState = rememberScrollState() - ModalDrawerSheet { + ModalDrawerSheet( + modifier = Modifier + .fillMaxHeight() + .verticalScroll(scrollState) + ) { Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing)) DrawerDefaultItems( @@ -44,6 +58,65 @@ fun TimelineDrawer( ) DrawerDivider() + + Column { + for (folderEntry in state.foldersAndFeeds) { + val folder = folderEntry.key + + if (folder != null) { + DrawerFolderItem( + label = { + Text( + text = folder.name!!, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + icon = { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null + ) + }, + badge = { + Text(folderEntry.value.sumOf { it.unreadCount }.toString()) + }, + selected = state.selectedFolderId == folder.id, + onClick = { onFolderClick(folder.id) }, + feeds = folderEntry.value, + selectedFeed = state.selectedFeedId, + onFeedClick = { onFeedClick(it) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + } else { + val feeds = folderEntry.value + + for (feed in feeds) { + DrawerFeedItem( + label = { + Text( + text = feed.name!!, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + icon = { + AsyncImage( + model = feed.iconUrl, + contentDescription = feed.name, + placeholder = painterResource(id = R.drawable.ic_folder_grey), + modifier = Modifier.size(24.dp) + ) + }, + badge = { Text(feed.unreadCount.toString()) }, + selected = feed.id == state.selectedFeedId, + onClick = { onFeedClick(feed.id) }, + modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) + ) + } + } + } + } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt index 67f2e448..f83def6e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/theme/Spacing.kt @@ -39,4 +39,7 @@ fun MediumSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.mediumSpacing)) fun LargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.largeSpacing)) @Composable -fun VeryLargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.veryLargeSpacing)) \ No newline at end of file +fun VeryLargeSpacer() = Spacer(Modifier.size(MaterialTheme.spacing.veryLargeSpacing)) + +@Composable +fun DrawerSpacing() = Spacer(Modifier.size(MaterialTheme.spacing.drawerSpacing)) \ No newline at end of file From dab044d31326a326664de70b02d13e599ad21689 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Aug 2023 17:45:52 +0200 Subject: [PATCH 51/95] Integrate Drawer open/close state in the viewModel --- .../app/compose/timelime/TimelineTab.kt | 53 ++++++++++--------- .../app/compose/timelime/TimelineViewModel.kt | 26 ++++++++- .../compose/timelime/drawer/TimelineDrawer.kt | 7 +-- 3 files changed, 55 insertions(+), 31 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 51418e29..8ddfe9d8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -25,9 +25,9 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -41,7 +41,6 @@ import com.readrops.app.compose.R import com.readrops.app.compose.item.ItemScreen import com.readrops.app.compose.timelime.drawer.TimelineDrawer import com.readrops.app.compose.util.theme.spacing -import kotlinx.coroutines.launch import org.koin.androidx.compose.getViewModel @@ -60,42 +59,54 @@ object TimelineTab : Tab { @Composable override fun Content() { val viewModel = getViewModel() + val state by viewModel.timelineState.collectAsState() + val drawerState by viewModel.drawerState.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState() val navigator = LocalNavigator.currentOrThrow val scrollState = rememberLazyListState() val swipeState = rememberSwipeRefreshState(isRefreshing) - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val scope = rememberCoroutineScope() - - BackHandler( - enabled = drawerState.isOpen, - onBack = { - scope.launch { - drawerState.close() + val drawerUIState = rememberDrawerState( + initialValue = DrawerValue.Closed, + confirmStateChange = { + if (it == DrawerValue.Closed) { + viewModel.closeDrawer() + } else { + viewModel.openDrawer() } + + true } ) - val closeDrawer = { scope.launch { drawerState.close() } } + BackHandler( + enabled = drawerState.isOpen, + onBack = { viewModel.closeDrawer() } + ) + + LaunchedEffect(drawerState.isOpen) { + if (drawerState.isOpen) { + drawerUIState.open() + } else { + drawerUIState.close() + } + } + ModalNavigationDrawer( - drawerState = drawerState, + drawerState = drawerUIState, drawerContent = { TimelineDrawer( - viewModel = viewModel, + state = drawerState, onClickDefaultItem = { viewModel.updateDrawerDefaultItem(it) - closeDrawer() }, onFolderClick = { viewModel.updateDrawerFolderSelection(it) - closeDrawer() }, onFeedClick = { viewModel.updateDrawerFeedSelection(it) - closeDrawer() } ) } @@ -106,11 +117,7 @@ object TimelineTab : Tab { title = { Text(text = "Articles") }, navigationIcon = { IconButton( - onClick = { - scope.launch { - drawerState.open() - } - } + onClick = { viewModel.openDrawer() } ) { Icon( imageVector = Icons.Default.Menu, @@ -146,9 +153,7 @@ object TimelineTab : Tab { ) { paddingValues -> SwipeRefresh( state = swipeState, - onRefresh = { - viewModel.refreshTimeline() - }, + onRefresh = { viewModel.refreshTimeline() }, modifier = Modifier.padding(paddingValues) ) { when (state) { diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 2bb97d8f..36d23cf4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -75,9 +75,18 @@ class TimelineViewModel( } } + fun openDrawer() { + _drawerState.update { it.copy(isOpen = true) } + } + + fun closeDrawer() { + _drawerState.update { it.copy(isOpen = false) } + } + fun updateDrawerDefaultItem(selection: DrawerDefaultItemsSelection) { _drawerState.update { it.copy( + isOpen = false, selection = selection, selectedFolderId = 0, selectedFeedId = 0, @@ -86,11 +95,23 @@ class TimelineViewModel( } fun updateDrawerFolderSelection(folderId: Int) { - _drawerState.update { it.copy(selectedFolderId = folderId, selectedFeedId = 0) } + _drawerState.update { + it.copy( + isOpen = false, + selectedFolderId = folderId, + selectedFeedId = 0 + ) + } } fun updateDrawerFeedSelection(feedId: Int) { - _drawerState.update { it.copy(selectedFeedId = feedId, selectedFolderId = 0) } + _drawerState.update { + it.copy( + isOpen = false, + selectedFeedId = feedId, + selectedFolderId = 0 + ) + } } } @@ -106,6 +127,7 @@ sealed class TimelineState { @Immutable data class DrawerState( + val isOpen: Boolean = false, val selection: DrawerDefaultItemsSelection = DrawerDefaultItemsSelection.ARTICLES, val selectedFolderId: Int = 0, val selectedFeedId: Int = 0, diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt index 6c7e73fa..c4da2264 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt @@ -17,15 +17,13 @@ import androidx.compose.material3.NavigationDrawerItem import androidx.compose.material3.NavigationDrawerItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.readrops.app.compose.R -import com.readrops.app.compose.timelime.TimelineViewModel +import com.readrops.app.compose.timelime.DrawerState import com.readrops.app.compose.util.theme.spacing enum class DrawerDefaultItemsSelection { @@ -37,12 +35,11 @@ enum class DrawerDefaultItemsSelection { @Composable fun TimelineDrawer( - viewModel: TimelineViewModel, + state: DrawerState, onClickDefaultItem: (DrawerDefaultItemsSelection) -> Unit, onFolderClick: (Int) -> Unit, onFeedClick: (Int) -> Unit, ) { - val state by viewModel.drawerState.collectAsState() val scrollState = rememberScrollState() ModalDrawerSheet( From 698abfe32b1b7e56fa111d5713cf04ec094ced78 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 20 Aug 2023 17:54:56 +0200 Subject: [PATCH 52/95] Replace collectAsState() by collectAsStateWithLifecycle() everywhere --- appcompose/build.gradle | 4 ++++ .../app/compose/account/AccountTab.kt | 20 +++++++++---------- .../selection/AccountSelectionScreen.kt | 4 ++-- .../com/readrops/app/compose/feeds/FeedTab.kt | 4 ++-- .../app/compose/timelime/TimelineTab.kt | 8 ++++---- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/appcompose/build.gradle b/appcompose/build.gradle index 577541f5..f937e8cf 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -108,4 +108,8 @@ dependencies { implementation "io.coil-kt:coil-compose:2.4.0" androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" + + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" + implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index 89cddabd..a47d147f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -11,10 +11,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab @@ -27,8 +27,8 @@ object AccountTab : Tab { override val options: TabOptions @Composable get() = TabOptions( - index = 3u, - title = "Account" + index = 3u, + title = "Account" ) @OptIn(ExperimentalMaterial3Api::class) @@ -36,24 +36,24 @@ object AccountTab : Tab { override fun Content() { val navigator = LocalNavigator.currentOrThrow val viewModel = getViewModel() - val closeHome by viewModel.closeHome.collectAsState() + val closeHome by viewModel.closeHome.collectAsStateWithLifecycle() if (closeHome) { navigator.replaceAll(AccountSelectionScreen()) } Scaffold( - topBar = { - TopAppBar(title = { Text(text = "Account") }) - } + topBar = { + TopAppBar(title = { Text(text = "Account") }) + } ) { paddingValues -> Column( - modifier = Modifier.padding(paddingValues) + modifier = Modifier.padding(paddingValues) ) { Row { Button(onClick = { viewModel.deleteAccount() }) { Text( - text = "Delete" + text = "Delete" ) } @@ -61,7 +61,7 @@ object AccountTab : Tab { Button(onClick = { navigator.push(AccountSelectionScreen()) }) { Text( - text = "New" + text = "New" ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt index 95e120d1..ff2d9b56 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionScreen.kt @@ -10,12 +10,12 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.androidx.AndroidScreen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -30,7 +30,7 @@ class AccountSelectionScreen : AndroidScreen() { @Composable override fun Content() { val viewModel = getViewModel() - val navState by viewModel.navState.collectAsState() + val navState by viewModel.navState.collectAsStateWithLifecycle() val navigator = LocalNavigator.currentOrThrow Column( diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 0395e580..eed5555c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -22,6 +21,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import org.koin.androidx.compose.getViewModel @@ -41,7 +41,7 @@ object FeedTab : Tab { val viewModel = getViewModel() var showDialog by remember { mutableStateOf(false) } - val state by viewModel.feedsState.collectAsState() + val state by viewModel.feedsState.collectAsStateWithLifecycle() if (showDialog) { AddFeedDialog( diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 8ddfe9d8..4b936d1b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -26,11 +26,11 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab @@ -60,9 +60,9 @@ object TimelineTab : Tab { override fun Content() { val viewModel = getViewModel() - val state by viewModel.timelineState.collectAsState() - val drawerState by viewModel.drawerState.collectAsState() - val isRefreshing by viewModel.isRefreshing.collectAsState() + val state by viewModel.timelineState.collectAsStateWithLifecycle() + val drawerState by viewModel.drawerState.collectAsStateWithLifecycle() + val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() val navigator = LocalNavigator.currentOrThrow From 16ed0ef05e205a09b3ece5fbd9c18a7591246aa5 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 21 Aug 2023 18:46:37 +0200 Subject: [PATCH 53/95] Add compact option in TimelineItem --- .../com/readrops/app/compose/timelime/TimelineItem.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index 0bdd4e28..bbc4e56e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -42,10 +42,12 @@ fun TimelineItem( onClick: () -> Unit, onFavorite: () -> Unit, onReadLater: () -> Unit, - onShare: () -> Unit + onShare: () -> Unit, + modifier: Modifier = Modifier, + compactLayout: Boolean = false, ) { Card( - modifier = Modifier + modifier = modifier .padding(horizontal = MaterialTheme.spacing.shortSpacing) .clickable { onClick() } ) { @@ -148,7 +150,7 @@ fun TimelineItem( ShortSpacer() - if (itemWithFeed.item.cleanDescription != null) { + if (itemWithFeed.item.cleanDescription != null && !compactLayout) { Text( text = itemWithFeed.item.cleanDescription!!, style = MaterialTheme.typography.bodyMedium, @@ -160,7 +162,7 @@ fun TimelineItem( ShortSpacer() } - if (itemWithFeed.item.imageLink != null) { + if (itemWithFeed.item.hasImage && !compactLayout) { AsyncImage( model = itemWithFeed.item.imageLink, contentDescription = itemWithFeed.item.title!!, From 36f768044aaa7b1235ad3f968b11abcb039bec6b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 23 Aug 2023 23:03:19 +0200 Subject: [PATCH 54/95] Implement androidx Paging and unify TimeLineViewModel state --- .../app/compose/timelime/TimelineTab.kt | 84 +++++------ .../app/compose/timelime/TimelineViewModel.kt | 141 ++++++++++-------- .../compose/timelime/drawer/TimelineDrawer.kt | 42 +++--- db/build.gradle | 4 + .../main/java/com/readrops/db/dao/ItemDao.kt | 2 +- .../com/readrops/db/dao/newdao/NewItemDao.kt | 4 +- .../com/readrops/db/filters/FilterType.kt | 3 +- .../readrops/db/queries/ItemsQueryBuilder.kt | 14 +- 8 files changed, 153 insertions(+), 141 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 4b936d1b..979f53d1 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -3,10 +3,10 @@ package com.readrops.app.compose.timelime import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab @@ -48,27 +49,22 @@ object TimelineTab : Tab { override val options: TabOptions @Composable - get() { - return TabOptions( - index = 1u, - title = "Timeline", - ) - } + get() = TabOptions( + index = 1u, + title = "Timeline", + ) @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { val viewModel = getViewModel() - val state by viewModel.timelineState.collectAsStateWithLifecycle() - val drawerState by viewModel.drawerState.collectAsStateWithLifecycle() - val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle() val navigator = LocalNavigator.currentOrThrow val scrollState = rememberLazyListState() - val swipeState = rememberSwipeRefreshState(isRefreshing) - val drawerUIState = rememberDrawerState( + val swipeState = rememberSwipeRefreshState(state.isRefreshing) + val drawerState = rememberDrawerState( initialValue = DrawerValue.Closed, confirmStateChange = { if (it == DrawerValue.Closed) { @@ -82,23 +78,23 @@ object TimelineTab : Tab { ) BackHandler( - enabled = drawerState.isOpen, + enabled = state.isDrawerOpen, onBack = { viewModel.closeDrawer() } ) - LaunchedEffect(drawerState.isOpen) { - if (drawerState.isOpen) { - drawerUIState.open() + LaunchedEffect(state.isDrawerOpen) { + if (state.isDrawerOpen) { + drawerState.open() } else { - drawerUIState.close() + drawerState.close() } } ModalNavigationDrawer( - drawerState = drawerUIState, + drawerState = drawerState, drawerContent = { TimelineDrawer( - state = drawerState, + state = state, onClickDefaultItem = { viewModel.updateDrawerDefaultItem(it) }, @@ -156,8 +152,8 @@ object TimelineTab : Tab { onRefresh = { viewModel.refreshTimeline() }, modifier = Modifier.padding(paddingValues) ) { - when (state) { - is TimelineState.Loading -> { + when (val itemState = state.items) { + is ItemState.Loading -> { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -167,34 +163,28 @@ object TimelineTab : Tab { } } - is TimelineState.Error -> { + is ItemState.Error -> TODO() + is ItemState.Loaded -> { + val items = itemState.items.collectAsLazyPagingItems() - } - - is TimelineState.Loaded -> { - val items = (state as TimelineState.Loaded).items - - if (items.isNotEmpty()) { - LazyColumn( - state = scrollState, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing) - ) { - items( - items = items, - key = { it.item.id }, - contentType = { "item_with_feed" } - ) { itemWithFeed -> - TimelineItem( - itemWithFeed = itemWithFeed, - onClick = { navigator.push(ItemScreen()) }, - onFavorite = {}, - onReadLater = {}, - onShare = {}, - ) - } + LazyColumn( + state = scrollState, + contentPadding = PaddingValues(vertical = MaterialTheme.spacing.shortSpacing), + verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.shortSpacing) + ) { + items( + count = items.itemCount, + key = { items[it]!!.item.id }, + contentType = { "item_with_feed" } + ) { itemCount -> + TimelineItem( + itemWithFeed = items[itemCount]!!, + onClick = { navigator.push(ItemScreen()) }, + onFavorite = {}, + onReadLater = {}, + onShare = {}, + ) } - } else { - NoItemPlaceholder() } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 36d23cf4..d0120568 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -2,23 +2,26 @@ package com.readrops.app.compose.timelime import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.GetFoldersWithFeeds -import com.readrops.app.compose.timelime.drawer.DrawerDefaultItemsSelection import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder +import com.readrops.db.filters.FilterType import com.readrops.db.pojo.ItemWithFeed import com.readrops.db.queries.ItemsQueryBuilder import com.readrops.db.queries.QueryFilters import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -29,108 +32,128 @@ class TimelineViewModel( private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : TabViewModel(database) { - private val _timelineState = MutableStateFlow(TimelineState.Loading) + private val _timelineState = MutableStateFlow(TimelineState()) val timelineState = _timelineState.asStateFlow() - private var _isRefreshing = MutableStateFlow(false) - val isRefreshing = _isRefreshing.asStateFlow() - - private val _drawerState = MutableStateFlow(DrawerState()) - val drawerState = _drawerState.asStateFlow() + private val filters = MutableStateFlow(_timelineState.value.filters) init { viewModelScope.launch(dispatcher) { - accountEvent.consumeAsFlow().collectLatest { account -> - val query = ItemsQueryBuilder.buildItemsQuery(QueryFilters(accountId = account.id)) + combine( + accountEvent.consumeAsFlow(), + filters + ) { account, filters -> + Pair(account, filters) + }.collectLatest { (account, filters) -> + val query = ItemsQueryBuilder.buildItemsQuery(filters.copy(accountId = account.id)) - val items = async { - database.newItemDao().selectAll(query) - .catch { _timelineState.value = TimelineState.Error(Exception(it)) } - .collect { - _timelineState.value = TimelineState.Loaded(it) - } - } - - val drawer = async { - _drawerState.update { - it.copy( - foldersAndFeeds = getFoldersWithFeeds.get(account.id) + _timelineState.update { + it.copy( + foldersAndFeeds = getFoldersWithFeeds.get(account.id), + items = ItemState.Loaded( + items = Pager( + config = PagingConfig( + pageSize = 100, + prefetchDistance = 150 + ), + pagingSourceFactory = { + database.newItemDao().selectAll(query) + }, + ).flow + .cachedIn(viewModelScope) ) - } + ) } - - awaitAll(items, drawer) } } } fun refreshTimeline() { - _isRefreshing.value = true + _timelineState.update { it.copy(isRefreshing = true) } viewModelScope.launch(dispatcher) { repository?.synchronize(null) { } - _isRefreshing.value = false + _timelineState.update { it.copy(isRefreshing = false) } } } fun openDrawer() { - _drawerState.update { it.copy(isOpen = true) } + _timelineState.update { it.copy(isDrawerOpen = true) } } fun closeDrawer() { - _drawerState.update { it.copy(isOpen = false) } + _timelineState.update { it.copy(isDrawerOpen = false) } } - fun updateDrawerDefaultItem(selection: DrawerDefaultItemsSelection) { - _drawerState.update { + fun updateDrawerDefaultItem(selection: FilterType) { + _timelineState.update { it.copy( - isOpen = false, - selection = selection, - selectedFolderId = 0, - selectedFeedId = 0, + filters = updateFilters { + it.filters.copy( + filterType = selection + ) + }, + isDrawerOpen = false ) } } fun updateDrawerFolderSelection(folderId: Int) { - _drawerState.update { + _timelineState.update { it.copy( - isOpen = false, - selectedFolderId = folderId, - selectedFeedId = 0 + filters = updateFilters { + it.filters.copy( + filterType = FilterType.FOLDER_FILER, + filterFolderId = folderId, + filterFeedId = 0 + ) + }, + isDrawerOpen = false ) } } fun updateDrawerFeedSelection(feedId: Int) { - _drawerState.update { + _timelineState.update { it.copy( - isOpen = false, - selectedFeedId = feedId, - selectedFolderId = 0 + filters = updateFilters { + it.filters.copy( + filterType = FilterType.FEED_FILTER, + filterFeedId = feedId, + filterFolderId = 0 + ) + }, + isDrawerOpen = false ) } } -} -sealed class TimelineState { - object Loading : TimelineState() + private fun updateFilters(block: () -> QueryFilters): QueryFilters { + val filter = block() + filters.update { filter } - @Immutable - data class Error(val exception: Exception) : TimelineState() - - @Immutable - data class Loaded(val items: List) : TimelineState() + return filter + } } @Immutable -data class DrawerState( - val isOpen: Boolean = false, - val selection: DrawerDefaultItemsSelection = DrawerDefaultItemsSelection.ARTICLES, - val selectedFolderId: Int = 0, - val selectedFeedId: Int = 0, - val foldersAndFeeds: Map> = emptyMap() +data class TimelineState( + val isRefreshing: Boolean = false, + val isDrawerOpen: Boolean = false, + val filters: QueryFilters = QueryFilters(), + val foldersAndFeeds: Map> = emptyMap(), + val items: ItemState = ItemState.Loading ) +sealed class ItemState { + @Immutable + object Loading : ItemState() + + @Immutable + data class Error(val exception: Exception) : ItemState() + + @Immutable + data class Loaded(val items: Flow>) : ItemState() +} diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt index c4da2264..5b3627c2 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt @@ -23,20 +23,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.readrops.app.compose.R -import com.readrops.app.compose.timelime.DrawerState +import com.readrops.app.compose.timelime.TimelineState import com.readrops.app.compose.util.theme.spacing - -enum class DrawerDefaultItemsSelection { - ARTICLES, - NEW, - FAVORITES, - READ_LATER -} +import com.readrops.db.filters.FilterType @Composable fun TimelineDrawer( - state: DrawerState, - onClickDefaultItem: (DrawerDefaultItemsSelection) -> Unit, + state: TimelineState, + onClickDefaultItem: (FilterType) -> Unit, onFolderClick: (Int) -> Unit, onFeedClick: (Int) -> Unit, ) { @@ -50,7 +44,7 @@ fun TimelineDrawer( Spacer(modifier = Modifier.size(MaterialTheme.spacing.drawerSpacing)) DrawerDefaultItems( - selectedItem = state.selection, + selectedItem = state.filters.filterType, onClick = { onClickDefaultItem(it) } ) @@ -78,10 +72,10 @@ fun TimelineDrawer( badge = { Text(folderEntry.value.sumOf { it.unreadCount }.toString()) }, - selected = state.selectedFolderId == folder.id, + selected = state.filters.filterFolderId == folder.id, onClick = { onFolderClick(folder.id) }, feeds = folderEntry.value, - selectedFeed = state.selectedFeedId, + selectedFeed = state.filters.filterFeedId, onFeedClick = { onFeedClick(it) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -106,7 +100,7 @@ fun TimelineDrawer( ) }, badge = { Text(feed.unreadCount.toString()) }, - selected = feed.id == state.selectedFeedId, + selected = feed.id == state.filters.filterFeedId, onClick = { onFeedClick(feed.id) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -119,8 +113,8 @@ fun TimelineDrawer( @Composable fun DrawerDefaultItems( - selectedItem: DrawerDefaultItemsSelection, - onClick: (DrawerDefaultItemsSelection) -> Unit, + selectedItem: FilterType, + onClick: (FilterType) -> Unit, ) { NavigationDrawerItem( label = { Text("Articles") }, @@ -130,8 +124,8 @@ fun DrawerDefaultItems( contentDescription = null ) }, - selected = selectedItem == DrawerDefaultItemsSelection.ARTICLES, - onClick = { onClick(DrawerDefaultItemsSelection.ARTICLES) }, + selected = selectedItem == FilterType.NO_FILTER, + onClick = { onClick(FilterType.NO_FILTER) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -143,8 +137,8 @@ fun DrawerDefaultItems( contentDescription = null ) }, - selected = selectedItem == DrawerDefaultItemsSelection.NEW, - onClick = { onClick(DrawerDefaultItemsSelection.NEW) }, + selected = selectedItem == FilterType.NEW, + onClick = { onClick(FilterType.NEW) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -156,8 +150,8 @@ fun DrawerDefaultItems( contentDescription = null ) }, - selected = selectedItem == DrawerDefaultItemsSelection.FAVORITES, - onClick = { onClick(DrawerDefaultItemsSelection.FAVORITES) }, + selected = selectedItem == FilterType.STARS_FILTER, + onClick = { onClick(FilterType.STARS_FILTER) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) @@ -169,8 +163,8 @@ fun DrawerDefaultItems( contentDescription = null ) }, - selected = selectedItem == DrawerDefaultItemsSelection.READ_LATER, - onClick = { onClick(DrawerDefaultItemsSelection.READ_LATER) }, + selected = selectedItem == FilterType.READ_IT_LATER_FILTER, + onClick = { onClick(FilterType.READ_IT_LATER_FILTER) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) } diff --git a/db/build.gradle b/db/build.gradle index ed739e25..36de6f9a 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -82,6 +82,7 @@ dependencies { kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-rxjava2:$room_version" androidTestImplementation "androidx.room:room-testing:$room_version" + implementation "androidx.room:room-paging:$room_version" implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4' kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4' @@ -98,4 +99,7 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + + api "androidx.paging:paging-runtime:3.1.1" + api "androidx.paging:paging-compose:1.0.0-alpha18" } diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.kt b/db/src/main/java/com/readrops/db/dao/ItemDao.kt index 5cd7ce0d..0f653e84 100644 --- a/db/src/main/java/com/readrops/db/dao/ItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/ItemDao.kt @@ -18,7 +18,7 @@ import io.reactivex.Completable interface ItemDao : BaseDao { @RawQuery(observedEntities = [Item::class, Folder::class, Feed::class, ItemState::class]) - fun selectAll(query: SupportSQLiteQuery): DataSource.Factory + fun selectAll(query: SupportSQLiteQuery): DataSource.Factory @Query("Select * From Item Where id = :itemId") fun select(itemId: Int): Item diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt index c7703ac8..3235ba94 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -1,5 +1,6 @@ package com.readrops.db.dao.newdao +import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Query import androidx.room.RawQuery @@ -9,13 +10,12 @@ import com.readrops.db.entities.Folder import com.readrops.db.entities.Item import com.readrops.db.entities.ItemState import com.readrops.db.pojo.ItemWithFeed -import kotlinx.coroutines.flow.Flow @Dao abstract class NewItemDao : NewBaseDao { @RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class]) - abstract fun selectAll(query: SupportSQLiteQuery): Flow> + abstract fun selectAll(query: SupportSQLiteQuery): PagingSource @Query("Select count(*) From Item Where feed_id = :feedId And read = 0") abstract fun selectUnreadCount(feedId: Int): Int diff --git a/db/src/main/java/com/readrops/db/filters/FilterType.kt b/db/src/main/java/com/readrops/db/filters/FilterType.kt index d6443501..c8d0d62f 100644 --- a/db/src/main/java/com/readrops/db/filters/FilterType.kt +++ b/db/src/main/java/com/readrops/db/filters/FilterType.kt @@ -5,5 +5,6 @@ enum class FilterType { FOLDER_FILER, READ_IT_LATER_FILTER, STARS_FILTER, - NO_FILTER + NO_FILTER, + NEW } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index c5f5bc13..b94d4ec0 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -81,11 +81,11 @@ object ItemsQueryBuilder { } -class QueryFilters( - var showReadItems: Boolean = true, - var filterFeedId: Int = 0, - var filterFolderId: Int = 0, - var accountId: Int = 0, - var filterType: FilterType = FilterType.NO_FILTER, - var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST, +data class QueryFilters( + var showReadItems: Boolean = true, + var filterFeedId: Int = 0, + var filterFolderId: Int = 0, + var accountId: Int = 0, + var filterType: FilterType = FilterType.NO_FILTER, + var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST, ) \ No newline at end of file From 03ee5deab9d1af44088ed4465eb3db55a8889476 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 27 Aug 2023 15:48:33 +0200 Subject: [PATCH 55/95] Improve logic to build drawer folders and feeds list * db requests down to 2 * react to changes when feeds/folders/items read state are modified --- .../repositories/GetFoldersWithFeeds.kt | 52 +++++++++++-------- .../app/compose/timelime/TimelineViewModel.kt | 10 +++- .../com/readrops/db/dao/newdao/NewFeedDao.kt | 8 +-- .../readrops/db/dao/newdao/NewFolderDao.kt | 12 +++-- .../com/readrops/db/pojo/FeedWithFolder.kt | 20 +++++-- 5 files changed, 68 insertions(+), 34 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index 0c8fd786..d52d01b8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -3,35 +3,43 @@ package com.readrops.app.compose.repositories import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine class GetFoldersWithFeeds( private val database: Database, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - suspend fun get(accountId: Int): Map> = withContext(dispatcher) { - val foldersWithFeeds = mutableMapOf>() - val folders = database.newFolderDao().selectFoldersByAccount(accountId) - - for (folder in folders) { - val feeds = database.newFeedDao().selectFeedsByFolder(folder.id) - - for (feed in feeds) { - feed.unreadCount = database.newItemDao().selectUnreadCount(feed.id) + fun get(accountId: Int): Flow>> { + return combine( + flow = database.newFolderDao() + .selectFoldersAndFeeds(accountId), + flow2 = database.newFeedDao() + .selectFeedsWithoutFolder(accountId) + ) { folders, feedsWithoutFolder -> + val foldersWithFeeds = folders.groupBy( + keySelector = { Folder(id = it.folderId, name = it.folderName) }, + valueTransform = { + Feed( + id = it.feedId, + name = it.feedName, + iconUrl = it.feedIcon, + unreadCount = it.unreadCount + ) + } + ).mapValues { listEntry -> + if (listEntry.value.any { it.id == 0 }) { + listOf() + } else { + listEntry.value + } } - foldersWithFeeds[folder] = feeds + foldersWithFeeds + mapOf( + Pair( + null, + feedsWithoutFolder.map { it.feed.apply { unreadCount = it.unreadCount } }) + ) } - - val feedsAlone = database.newFeedDao().selectFeedsAlone(accountId) - for (feed in feedsAlone) { - feed.unreadCount = database.newItemDao().selectUnreadCount(feed.id) - } - - foldersWithFeeds[null] = feedsAlone - foldersWithFeeds.toSortedMap(nullsLast()) } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index d0120568..7d799eaf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -49,7 +49,6 @@ class TimelineViewModel( _timelineState.update { it.copy( - foldersAndFeeds = getFoldersWithFeeds.get(account.id), items = ItemState.Loaded( items = Pager( config = PagingConfig( @@ -64,6 +63,15 @@ class TimelineViewModel( ) ) } + + getFoldersWithFeeds.get(account.id) + .collect { foldersAndFeeds -> + _timelineState.update { + it.copy( + foldersAndFeeds = foldersAndFeeds + ) + } + } } } } diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index d66102fb..450d7ddf 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -3,6 +3,7 @@ package com.readrops.db.dao.newdao import androidx.room.Dao import androidx.room.Query import com.readrops.db.entities.Feed +import com.readrops.db.pojo.FeedWithCount import kotlinx.coroutines.flow.Flow @Dao @@ -20,9 +21,8 @@ abstract class NewFeedDao : NewBaseDao { @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") abstract suspend fun feedExists(feedUrl: String, accountId: Int): Boolean - @Query("Select * From Feed Where folder_id = :folderId") - abstract suspend fun selectFeedsByFolder(folderId: Int): List + @Query("Select *, count(*) as unreadCount From Feed Inner Join Item On Feed.id = Item.feed_id " + + "Where Feed.folder_id is Null And Item.read = 0 And Feed.account_id = :accountId Group by Feed.id") + abstract fun selectFeedsWithoutFolder(accountId: Int): Flow> - @Query("Select * From Feed Where account_id = :accountId And folder_id Is Null") - abstract suspend fun selectFeedsAlone(accountId: Int): List } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index c9e40e2c..782411b1 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -3,12 +3,16 @@ package com.readrops.db.dao.newdao import androidx.room.Dao import androidx.room.Query import com.readrops.db.entities.Folder +import com.readrops.db.pojo.FolderWithFeed +import kotlinx.coroutines.flow.Flow @Dao abstract class NewFolderDao : NewBaseDao { - @Query("Select * From Folder Where account_id = :accountId Order By name ASC") - abstract suspend fun selectFoldersByAccount(accountId: Int): List - - + @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + + "Feed.icon_url As feedIcon, count(*) As unreadCount From Folder Left Join Feed " + + "On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + + "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " + + "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id") + abstract fun selectFoldersAndFeeds(accountId: Int): Flow> } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt index dd78e3d7..41ea9f74 100644 --- a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt +++ b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt @@ -8,6 +8,20 @@ import kotlinx.parcelize.Parcelize @Parcelize data class FeedWithFolder( - @Embedded(prefix = "feed_") val feed: Feed, - @Embedded(prefix = "folder_") val folder: Folder, -) : Parcelable \ No newline at end of file + @Embedded(prefix = "feed_") val feed: Feed, + @Embedded(prefix = "folder_") val folder: Folder, +) : Parcelable + +data class FolderWithFeed( + val folderId: Int, + val folderName: String, + val feedId: Int = 0, + val feedName: String? = null, + val feedIcon: String? = null, + val unreadCount: Int = 0 +) + +data class FeedWithCount( + @Embedded val feed: Feed, + val unreadCount: Int +) \ No newline at end of file From 51806993a1b457e0aec23c0f54f8dc5160474011 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 27 Aug 2023 17:29:19 +0200 Subject: [PATCH 56/95] Add read state update on TimelineItem click and fav/share actions --- .../app/compose/repositories/ARepository.kt | 9 +++++ .../app/compose/timelime/TimelineItem.kt | 5 ++- .../app/compose/timelime/TimelineTab.kt | 17 +++++++--- .../app/compose/timelime/TimelineViewModel.kt | 33 +++++++++++++++++++ db/build.gradle | 4 +-- .../com/readrops/db/dao/newdao/NewItemDao.kt | 6 ++-- .../readrops/db/queries/ItemsQueryBuilder.kt | 2 +- 7 files changed, 66 insertions(+), 10 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index 46efa7fa..7e1452f9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -4,6 +4,7 @@ import com.readrops.api.services.SyncResult 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 data class ErrorResult( @@ -53,4 +54,12 @@ abstract class BaseRepository( open suspend fun addFolder(folder: Folder) {} open suspend fun deleteFolder(folder: Folder) {} + + open suspend fun setItemReadState(item: Item) { + database.newItemDao().updateReadState(item.id, item.isRead) + } + + open suspend fun setItemStarState(item: Item) { + database.newItemDao().updateStarState(item.id, item.isStarred) + } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt index bbc4e56e..0bd45614 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineItem.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.Share import androidx.compose.material3.Card @@ -20,6 +21,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -49,6 +51,7 @@ fun TimelineItem( Card( modifier = modifier .padding(horizontal = MaterialTheme.spacing.shortSpacing) + .alpha(if (itemWithFeed.item.isRead) 0.6f else 1f) .clickable { onClick() } ) { Column( @@ -180,7 +183,7 @@ fun TimelineItem( .padding(MaterialTheme.spacing.shortSpacing) ) { Icon( - imageVector = Icons.Outlined.FavoriteBorder, + imageVector = if (itemWithFeed.item.isStarred) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder, contentDescription = null, modifier = Modifier.clickable { onFavorite() } ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 979f53d1..f5d7c727 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -29,6 +29,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.compose.collectAsLazyPagingItems @@ -61,6 +62,7 @@ object TimelineTab : Tab { val state by viewModel.timelineState.collectAsStateWithLifecycle() val navigator = LocalNavigator.currentOrThrow + val context = LocalContext.current val scrollState = rememberLazyListState() val swipeState = rememberSwipeRefreshState(state.isRefreshing) @@ -177,12 +179,19 @@ object TimelineTab : Tab { key = { items[it]!!.item.id }, contentType = { "item_with_feed" } ) { itemCount -> + val itemWithFeed = items[itemCount]!! + TimelineItem( - itemWithFeed = items[itemCount]!!, - onClick = { navigator.push(ItemScreen()) }, - onFavorite = {}, + itemWithFeed = itemWithFeed, + onClick = { + viewModel.setItemRead(itemWithFeed.item) + navigator.push(ItemScreen()) + }, + onFavorite = { viewModel.updateStarState(itemWithFeed.item) }, onReadLater = {}, - onShare = {}, + onShare = { + viewModel.shareItem(itemWithFeed.item, context) + }, ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 7d799eaf..dd41b6e5 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -1,5 +1,7 @@ package com.readrops.app.compose.timelime +import android.content.Context +import android.content.Intent import androidx.compose.runtime.Immutable import androidx.lifecycle.viewModelScope import androidx.paging.Pager @@ -11,6 +13,7 @@ import com.readrops.app.compose.repositories.GetFoldersWithFeeds 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.filters.FilterType import com.readrops.db.pojo.ItemWithFeed import com.readrops.db.queries.ItemsQueryBuilder @@ -144,6 +147,36 @@ class TimelineViewModel( return filter } + + fun setItemRead(item: Item) { + item.isRead = true + updateItemReadState(item) + } + + private fun updateItemReadState(item: Item) { + viewModelScope.launch(dispatcher) { + repository?.setItemReadState(item) + } + } + + fun updateStarState(item: Item) { + viewModelScope.launch(dispatcher) { + with(item) { + isStarred = isStarred.not() + repository?.setItemStarState(this) + } + } + } + + fun shareItem(item: Item, context: Context) { + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, item.link) + }.also { + context.startActivity(Intent.createChooser(it, null)) + } + } } @Immutable diff --git a/db/build.gradle b/db/build.gradle index 36de6f9a..f4a17b4c 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -100,6 +100,6 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' - api "androidx.paging:paging-runtime:3.1.1" - api "androidx.paging:paging-compose:1.0.0-alpha18" + api "androidx.paging:paging-runtime-ktx:3.2.0" + api "androidx.paging:paging-compose:3.2.0" } diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt index 3235ba94..ed9aaa05 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -17,7 +17,9 @@ abstract class NewItemDao : NewBaseDao { @RawQuery(observedEntities = [Item::class, Feed::class, Folder::class, ItemState::class]) abstract fun selectAll(query: SupportSQLiteQuery): PagingSource - @Query("Select count(*) From Item Where feed_id = :feedId And read = 0") - abstract fun selectUnreadCount(feedId: Int): Int + @Query("Update Item Set read = :read Where id = :itemId") + abstract suspend fun updateReadState(itemId: Int, read: Boolean) + @Query("Update Item Set starred = :starred Where id = :itemId") + abstract suspend fun updateStarState(itemId: Int, starred: Boolean) } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt index b94d4ec0..bab87346 100644 --- a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt +++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt @@ -7,7 +7,7 @@ import com.readrops.db.filters.ListSortType object ItemsQueryBuilder { - private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "clean_description", "image_link", "pub_date", + private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "clean_description", "image_link", "pub_date", "link", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name") From 5a6a6d4b23980dc526794eef8bdd4cb829cc899e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 27 Aug 2023 22:31:47 +0200 Subject: [PATCH 57/95] Update compose BOM --- appcompose/build.gradle | 8 ++++---- .../main/java/com/readrops/app/compose/MainActivity.kt | 6 +++--- build.gradle | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/appcompose/build.gradle b/appcompose/build.gradle index f937e8cf..a8c9d9bc 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -5,12 +5,12 @@ plugins { android { namespace 'com.readrops.app.compose' - compileSdk 33 + compileSdk rootProject.ext.compileSdkVersion defaultConfig { applicationId "com.readrops.app.compose" - minSdk 21 - targetSdk 33 + minSdk rootProject.ext.minSdkVersion + targetSdk rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" @@ -68,7 +68,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - def composeBom = platform('androidx.compose:compose-bom:2023.06.01') + def composeBom = platform('androidx.compose:compose-bom:2023.08.00') implementation composeBom androidTestImplementation composeBom diff --git a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt index 03c0df30..2bcf35ef 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/MainActivity.kt @@ -5,8 +5,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.material3.* +import cafe.adriel.voyager.navigator.CurrentScreen import cafe.adriel.voyager.navigator.Navigator -import cafe.adriel.voyager.transitions.FadeTransition import com.readrops.app.compose.account.selection.AccountSelectionScreen import com.readrops.app.compose.account.selection.AccountSelectionViewModel import com.readrops.app.compose.home.HomeScreen @@ -26,8 +26,8 @@ class MainActivity : ComponentActivity() { ReadropsTheme { Navigator( screen = if (accountExists) HomeScreen() else AccountSelectionScreen() - ) { navigator -> - FadeTransition(navigator) + ) { + CurrentScreen() } } } diff --git a/build.gradle b/build.gradle index 15978c13..79ca1cd2 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jacoco:org.jacoco.core:0.8.7" } @@ -30,10 +30,10 @@ allprojects { } ext { - compileSdkVersion = 33 + compileSdkVersion = 34 minSdkVersion = 21 - targetSdkVersion = 33 - buildToolsVersion = "33.0.2" + targetSdkVersion = 34 + buildToolsVersion = "34.0.0" koin_version = "3.3.3" } From 413dba4db5fd8dd91fa60f21aa7fe09277198504 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 27 Aug 2023 22:34:53 +0200 Subject: [PATCH 58/95] Fix id conflict in feedsWithoutFolder query --- db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index 450d7ddf..0f0493e2 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -21,7 +21,7 @@ abstract class NewFeedDao : NewBaseDao { @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") abstract suspend fun feedExists(feedUrl: String, accountId: Int): Boolean - @Query("Select *, count(*) as unreadCount From Feed Inner Join Item On Feed.id = Item.feed_id " + + @Query("Select Feed.*, count(*) as unreadCount From Feed Inner Join Item On Feed.id = Item.feed_id " + "Where Feed.folder_id is Null And Item.read = 0 And Feed.account_id = :accountId Group by Feed.id") abstract fun selectFeedsWithoutFolder(accountId: Int): Flow> From 304f3c02e0fd996d05b34082d0f0230b236aa206 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 11 Jan 2024 22:16:30 +0100 Subject: [PATCH 59/95] Add base of pagination in TimelineTab --- appcompose/build.gradle | 6 +- .../app/compose/timelime/TimelineTab.kt | 57 ++++++++----------- .../app/compose/timelime/TimelineViewModel.kt | 47 +++++++-------- .../compose/util/components/Placeholder.kt | 47 +++++++++++++++ db/build.gradle | 8 +-- 5 files changed, 100 insertions(+), 65 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt diff --git a/appcompose/build.gradle b/appcompose/build.gradle index a8c9d9bc..a30d5e98 100644 --- a/appcompose/build.gradle +++ b/appcompose/build.gradle @@ -68,7 +68,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - def composeBom = platform('androidx.compose:compose-bom:2023.08.00') + def composeBom = platform('androidx.compose:compose-bom:2023.10.01') implementation composeBom androidTestImplementation composeBom @@ -107,7 +107,9 @@ dependencies { implementation "io.coil-kt:coil:2.4.0" implementation "io.coil-kt:coil-compose:2.4.0" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1" implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1" diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index f5d7c727..5b643202 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -1,15 +1,12 @@ package com.readrops.app.compose.timelime +import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.CircularProgressIndicator @@ -27,11 +24,12 @@ import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -42,6 +40,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.readrops.app.compose.R import com.readrops.app.compose.item.ItemScreen import com.readrops.app.compose.timelime.drawer.TimelineDrawer +import com.readrops.app.compose.util.components.CenteredColumn import com.readrops.app.compose.util.theme.spacing import org.koin.androidx.compose.getViewModel @@ -60,11 +59,14 @@ object TimelineTab : Tab { override fun Content() { val viewModel = getViewModel() val state by viewModel.timelineState.collectAsStateWithLifecycle() + val items = state.itemState.collectAsLazyPagingItems() val navigator = LocalNavigator.currentOrThrow val context = LocalContext.current val scrollState = rememberLazyListState() + + // Use the depreciated refresh swipe as the material 3 one isn't available yet val swipeState = rememberSwipeRefreshState(state.isRefreshing) val drawerState = rememberDrawerState( initialValue = DrawerValue.Closed, @@ -131,7 +133,9 @@ object TimelineTab : Tab { ) } - IconButton(onClick = { }) { + IconButton( + onClick = { viewModel.refreshTimeline() } + ) { Icon( painter = painterResource(id = R.drawable.ic_sync), contentDescription = null @@ -154,21 +158,16 @@ object TimelineTab : Tab { onRefresh = { viewModel.refreshTimeline() }, modifier = Modifier.padding(paddingValues) ) { - when (val itemState = state.items) { - is ItemState.Loading -> { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxSize() - ) { + when { + items.isLoading() -> { + Log.d("TAG", "loading") + CenteredColumn { CircularProgressIndicator() } } - is ItemState.Error -> TODO() - is ItemState.Loaded -> { - val items = itemState.items.collectAsLazyPagingItems() - + items.isError() -> Text(text = "error") + else -> { LazyColumn( state = scrollState, contentPadding = PaddingValues(vertical = MaterialTheme.spacing.shortSpacing), @@ -176,7 +175,7 @@ object TimelineTab : Tab { ) { items( count = items.itemCount, - key = { items[it]!!.item.id }, + //key = { items[it]!! }, contentType = { "item_with_feed" } ) { itemCount -> val itemWithFeed = items[itemCount]!! @@ -192,6 +191,7 @@ object TimelineTab : Tab { onShare = { viewModel.shareItem(itemWithFeed.item, context) }, + compactLayout = true ) } } @@ -203,20 +203,11 @@ object TimelineTab : Tab { } } -@Composable -fun NoItemPlaceholder() { - val scrollState = rememberScrollState() - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - ) { - Text( - text = "No item", - style = MaterialTheme.typography.displayMedium - ) - } +fun LazyPagingItems.isLoading(): Boolean { + return loadState.append is LoadState.Loading //|| loadState.refresh is LoadState.Loading +} + +fun LazyPagingItems.isError(): Boolean { + return loadState.append is LoadState.Error //|| loadState.refresh is LoadState.Error } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index dd41b6e5..c0b05173 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -46,24 +47,23 @@ class TimelineViewModel( accountEvent.consumeAsFlow(), filters ) { account, filters -> + filters.accountId = account.id Pair(account, filters) }.collectLatest { (account, filters) -> - val query = ItemsQueryBuilder.buildItemsQuery(filters.copy(accountId = account.id)) + val query = ItemsQueryBuilder.buildItemsQuery(filters) _timelineState.update { it.copy( - items = ItemState.Loaded( - items = Pager( - config = PagingConfig( - pageSize = 100, - prefetchDistance = 150 - ), - pagingSourceFactory = { - database.newItemDao().selectAll(query) - }, - ).flow - .cachedIn(viewModelScope) - ) + itemState = Pager( + config = PagingConfig( + pageSize = 10, + prefetchDistance = 10 + ), + pagingSourceFactory = { + database.newItemDao().selectAll(query) + }, + ).flow + .cachedIn(viewModelScope) ) } @@ -86,7 +86,12 @@ class TimelineViewModel( } - _timelineState.update { it.copy(isRefreshing = false) } + _timelineState.update { + it.copy( + isRefreshing = false, + endSynchronizing = true + ) + } } } @@ -183,18 +188,8 @@ class TimelineViewModel( data class TimelineState( val isRefreshing: Boolean = false, val isDrawerOpen: Boolean = false, + val endSynchronizing: Boolean = false, val filters: QueryFilters = QueryFilters(), val foldersAndFeeds: Map> = emptyMap(), - val items: ItemState = ItemState.Loading + val itemState: Flow> = emptyFlow() ) - -sealed class ItemState { - @Immutable - object Loading : ItemState() - - @Immutable - data class Error(val exception: Exception) : ItemState() - - @Immutable - data class Loaded(val items: Flow>) : ItemState() -} diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt new file mode 100644 index 00000000..38b6d933 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt @@ -0,0 +1,47 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import com.readrops.app.compose.util.toDp + + +@Composable +fun CenteredColumn( + content: @Composable () -> Unit +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + content() + } +} + +@Composable +fun Placeholder( + text: String, + painter: Painter, +) { + CenteredColumn { + Icon( + painter = painter, + contentDescription = null, + modifier = Modifier.size(MaterialTheme.typography.displayMedium.toDp() * 1.5f) + ) + + Text( + text = text, + style = MaterialTheme.typography.displayMedium + ) + } +} \ No newline at end of file diff --git a/db/build.gradle b/db/build.gradle index f4a17b4c..302f00ca 100644 --- a/db/build.gradle +++ b/db/build.gradle @@ -97,9 +97,9 @@ dependencies { api "io.insert-koin:koin-androidx-compose:3.4.2" api "io.insert-koin:koin-android-compat:$rootProject.ext.koin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - api "androidx.paging:paging-runtime-ktx:3.2.0" - api "androidx.paging:paging-compose:3.2.0" + api "androidx.paging:paging-runtime-ktx:3.2.1" + api "androidx.paging:paging-compose:3.2.1" } From 6b50d40800241bb52a01d7422ce49b2e1d57d059 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 11 Jan 2024 23:37:48 +0100 Subject: [PATCH 60/95] Improve feed item lookingin FeedTab and add initial feed bottom sheet --- .../app/compose/feeds/FeedBottomSheet.kt | 141 ++++++++++++++++++ .../readrops/app/compose/feeds/FeedItem.kt | 58 ++++--- .../com/readrops/app/compose/feeds/FeedTab.kt | 87 ++++++++--- .../main/res/drawable/ic_open_in_browser.xml | 9 ++ 4 files changed, 254 insertions(+), 41 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt create mode 100644 appcompose/src/main/res/drawable/ic_open_in_browser.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt new file mode 100644 index 00000000..b855e63d --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt @@ -0,0 +1,141 @@ +package com.readrops.app.compose.feeds + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Create +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.VeryShortSpacer +import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.entities.Feed + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FeedModalBottomSheet( + feed: Feed, + onDismissRequest: () -> Unit, + onOpen: () -> Unit, + onModify: () -> Unit, + onDelete: () -> Unit, +) { + ModalBottomSheet( + onDismissRequest = { onDismissRequest() } + ) { + Column { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding( + horizontal = 24.dp + ) + ) { + AsyncImage( + model = feed.iconUrl, + contentDescription = feed.name!!, + modifier = Modifier.size(48.dp) + ) + + MediumSpacer() + + Column { + Text( + text = feed.name!!, + style = MaterialTheme.typography.headlineSmall, + ) + + VeryShortSpacer() + + Text( + text = "folder name if it exists", + style = MaterialTheme.typography.labelSmall + ) + } + } + + MediumSpacer() + + Divider( + modifier = Modifier.padding( + horizontal = MaterialTheme.spacing.shortSpacing + ) + ) + + MediumSpacer() + + BottomSheetOption( + text = "Open", + icon = ImageVector.vectorResource(id = R.drawable.ic_open_in_browser), + onClick = onOpen + ) + + BottomSheetOption( + text = "Modify", + icon = Icons.Default.Create, + onClick = onModify + ) + + BottomSheetOption( + text = "Delete", + icon = Icons.Default.Delete, + onClick = onDelete + ) + } + + LargeSpacer() + } +} + +@Composable +fun BottomSheetOption( + text: String, + icon: ImageVector, + onClick: () -> Unit, +) { + Box( + modifier = Modifier.clickable { onClick() } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp + ) + + ) { + Icon( + imageVector = icon, + contentDescription = text + ) + + MediumSpacer() + + Text( + text = text, + style = MaterialTheme.typography.bodyMedium + ) + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt index 0ea04f2b..af27626b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt @@ -1,34 +1,54 @@ package com.readrops.app.compose.feeds -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.toDp import com.readrops.db.entities.Feed +@OptIn(ExperimentalFoundationApi::class) @Composable -fun FeedItem(feed: Feed) { - val context = LocalContext.current - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - ) { - /*CoilImage(imageRequest = { - ImageRequest.Builder(context) - .data(feed.url) - .build() - })*/ - - Text( - text = feed.name!!, - style = MaterialTheme.typography.headlineSmall +fun FeedItem( + feed: Feed, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { + Box( + modifier = Modifier.combinedClickable( + onClick = onClick, + onLongClick = onLongClick ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + AsyncImage( + model = feed.iconUrl, + contentDescription = feed.name!!, + modifier = Modifier.size(MaterialTheme.typography.bodyLarge.toDp()) + ) + + ShortSpacer() + + Text( + text = feed.name!!, + style = MaterialTheme.typography.bodyLarge + ) + } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index eed5555c..c9fc4029 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar @@ -20,10 +22,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel object FeedTab : Tab { @@ -31,37 +37,66 @@ object FeedTab : Tab { override val options: TabOptions @Composable get() = TabOptions( - index = 2u, - title = "Feeds" + index = 2u, + title = "Feeds" ) @OptIn(ExperimentalMaterial3Api::class) @Composable override fun Content() { + val haptic = LocalHapticFeedback.current + val uriHandler = LocalUriHandler.current val viewModel = getViewModel() - var showDialog by remember { mutableStateOf(false) } val state by viewModel.feedsState.collectAsStateWithLifecycle() + var showDialog by remember { mutableStateOf(false) } + + var selectedFeed by remember { mutableStateOf(null) } + var showBottomSheet by remember { mutableStateOf(false) } + + if (showBottomSheet) { + FeedModalBottomSheet( + feed = selectedFeed!!, + onDismissRequest = { showBottomSheet = false }, + onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) }, + onModify = { }, + onDelete = {}, + ) + } if (showDialog) { AddFeedDialog( - onDismiss = { showDialog = false }, - onValidate = { - showDialog = false - viewModel.insertFeed(it) - } + onDismiss = { showDialog = false }, + onValidate = { + showDialog = false + viewModel.insertFeed(it) + } ) } Scaffold( - topBar = { - TopAppBar(title = { Text(text = "Feeds") }) - } + topBar = { + TopAppBar( + title = { + Text(text = "Feeds") + }, + actions = { + IconButton( + onClick = {} + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + } + ) + }, ) { paddingValues -> Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) ) { when (state) { is FeedsState.LoadedState -> { @@ -70,10 +105,18 @@ object FeedTab : Tab { if (feeds.isNotEmpty()) { LazyColumn { items( - items = feeds + items = feeds ) { feed -> FeedItem( - feed = feed, + feed = feed, + onClick = { + selectedFeed = feed + showBottomSheet = true + }, + onLongClick = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + uriHandler.openUri(feed.siteUrl!!) + } ) } } @@ -90,14 +133,14 @@ object FeedTab : Tab { } FloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - onClick = { showDialog = true } + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + onClick = { showDialog = true } ) { Icon( - imageVector = Icons.Default.Add, - contentDescription = null + imageVector = Icons.Default.Add, + contentDescription = null ) } } diff --git a/appcompose/src/main/res/drawable/ic_open_in_browser.xml b/appcompose/src/main/res/drawable/ic_open_in_browser.xml new file mode 100644 index 00000000..f416ca58 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_open_in_browser.xml @@ -0,0 +1,9 @@ + + + From 5cd7ead78f7e816680633443fb369582c40f7fc2 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 12 Jan 2024 19:28:25 +0100 Subject: [PATCH 61/95] Show feeds as sub items of folders in FeedTab --- .../readrops/app/compose/ComposeAppModule.kt | 2 +- .../com/readrops/app/compose/feeds/FeedTab.kt | 46 +++++--- .../app/compose/feeds/FeedViewModel.kt | 18 ++- .../app/compose/feeds/FolderExpandableItem.kt | 107 ++++++++++++++++++ .../repositories/GetFoldersWithFeeds.kt | 1 + .../readrops/db/dao/newdao/NewFolderDao.kt | 2 +- .../com/readrops/db/pojo/FeedWithFolder.kt | 1 + 7 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index b51dba3a..25c84ac1 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -15,7 +15,7 @@ val composeAppModule = module { viewModel { TimelineViewModel(get(), get()) } - viewModel { FeedViewModel(get()) } + viewModel { FeedViewModel(get(), get()) } viewModel { AccountSelectionViewModel(get()) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index c9fc4029..2d67b35b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose.feeds +import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -25,10 +26,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.readrops.app.compose.R +import com.readrops.app.compose.util.components.Placeholder import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel @@ -58,7 +62,9 @@ object FeedTab : Tab { FeedModalBottomSheet( feed = selectedFeed!!, onDismissRequest = { showBottomSheet = false }, - onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) }, + onOpen = { + Log.d("TAG", "Content: ") + uriHandler.openUri(selectedFeed!!.siteUrl!!) }, onModify = { }, onDelete = {}, ) @@ -100,26 +106,34 @@ object FeedTab : Tab { ) { when (state) { is FeedsState.LoadedState -> { - val feeds = (state as FeedsState.LoadedState).feeds + val foldersAndFeeds = (state as FeedsState.LoadedState).foldersAndFeeds - if (feeds.isNotEmpty()) { + if (foldersAndFeeds.isNotEmpty()) { LazyColumn { items( - items = feeds - ) { feed -> - FeedItem( - feed = feed, - onClick = { - selectedFeed = feed - showBottomSheet = true - }, - onLongClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - uriHandler.openUri(feed.siteUrl!!) - } - ) + items = foldersAndFeeds.toList() + ) { folderWithFeeds -> + if (folderWithFeeds.first != null) { + FolderExpandableItem( + folder = folderWithFeeds.first!!, + feeds = folderWithFeeds.second, + onFeedClick = { feed -> + selectedFeed = feed + showBottomSheet = true + }, + onFeedLongClick = { feed -> + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + uriHandler.openUri(feed.siteUrl!!) + } + ) + } } } + } else { + Placeholder( + text = "No feed", + painter = painterResource(R.drawable.ic_rss_feed_grey) + ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index d067b415..c1733491 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -2,16 +2,21 @@ package com.readrops.app.compose.feeds import androidx.lifecycle.viewModelScope import com.readrops.app.compose.base.TabViewModel +import com.readrops.app.compose.repositories.GetFoldersWithFeeds import com.readrops.db.Database import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.launch class FeedViewModel( - private val database: Database, + database: Database, + private val getFoldersWithFeeds: GetFoldersWithFeeds ) : TabViewModel(database) { private val _feedsState = MutableStateFlow(FeedsState.InitialState) @@ -19,9 +24,12 @@ class FeedViewModel( init { viewModelScope.launch(context = Dispatchers.IO) { - database.newFeedDao().selectFeeds() - .catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) } - .collect { _feedsState.value = FeedsState.LoadedState(it) } + accountEvent.consumeAsFlow() + .flatMapConcat { account -> + getFoldersWithFeeds.get(account.id) + } + .catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) } + .collect { _feedsState.value = FeedsState.LoadedState(it) } } } @@ -35,5 +43,5 @@ class FeedViewModel( sealed class FeedsState { object InitialState : FeedsState() data class ErrorState(val exception: Exception) : FeedsState() - data class LoadedState(val feeds: List) : FeedsState() + data class LoadedState(val foldersAndFeeds: Map>) : FeedsState() } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt new file mode 100644 index 00000000..9e509135 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt @@ -0,0 +1,107 @@ +package com.readrops.app.compose.feeds + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder + +@Composable +fun FolderExpandableItem( + folder: Folder, + feeds: List, + onFeedClick: (Feed) -> Unit, + onFeedLongClick: (Feed) -> Unit, +) { + var isExpanded by remember { mutableStateOf(false) } + val rotationState by animateFloatAsState( + targetValue = if (isExpanded) 180f else 0f, + label = "folder item arrow rotation" + ) + + Column( + modifier = Modifier + .animateContentSize( + animationSpec = tween( + durationMillis = 300, + easing = LinearOutSlowInEasing, + ) + ) + ) { + Column( + modifier = Modifier + .clickable { isExpanded = isExpanded.not() } + .padding( + horizontal = MaterialTheme.spacing.shortSpacing, + vertical = MaterialTheme.spacing.veryShortSpacing, + ) + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxSize() + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.ic_folder_grey), + contentDescription = folder.name + ) + + MediumSpacer() + + Text( + text = folder.name!!, + style = MaterialTheme.typography.headlineSmall + ) + } + + Row { + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = null, + modifier = Modifier + .rotate(rotationState), + ) + } + } + } + + Column { + if (isExpanded) { + for (feed in feeds) { + FeedItem( + feed = feed, + onClick = { onFeedClick(feed) }, + onLongClick = { onFeedLongClick(feed) }, + ) + } + } + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index d52d01b8..72154f4f 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -24,6 +24,7 @@ class GetFoldersWithFeeds( id = it.feedId, name = it.feedName, iconUrl = it.feedIcon, + siteUrl = it.feedUrl, unreadCount = it.unreadCount ) } diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index 782411b1..fe81283b 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow abstract class NewFolderDao : NewBaseDao { @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + - "Feed.icon_url As feedIcon, count(*) As unreadCount From Folder Left Join Feed " + + "Feed.icon_url As feedIcon, Feed.siteUrl as feedUrl, count(*) As unreadCount From Folder Left Join Feed " + "On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " + "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id") diff --git a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt index 41ea9f74..ea84c72d 100644 --- a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt +++ b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt @@ -18,6 +18,7 @@ data class FolderWithFeed( val feedId: Int = 0, val feedName: String? = null, val feedIcon: String? = null, + val feedUrl: String? = null, val unreadCount: Int = 0 ) From 7d17740713d05a32d7f83aefc93ee1756258bef9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 12 Jan 2024 19:49:35 +0100 Subject: [PATCH 62/95] Improve FeedBottomSheet looking and add Update color option --- .../app/compose/feeds/FeedBottomSheet.kt | 32 ++++++++++++------- .../com/readrops/app/compose/feeds/FeedTab.kt | 10 +++--- appcompose/src/main/res/drawable/ic_color.xml | 5 +++ 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 appcompose/src/main/res/drawable/ic_color.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt index b855e63d..87eaea42 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.readrops.app.compose.R import com.readrops.app.compose.util.theme.LargeSpacer @@ -30,14 +29,17 @@ import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.VeryShortSpacer import com.readrops.app.compose.util.theme.spacing import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder @OptIn(ExperimentalMaterial3Api::class) @Composable fun FeedModalBottomSheet( feed: Feed, + folder: Folder?, onDismissRequest: () -> Unit, onOpen: () -> Unit, onModify: () -> Unit, + onUpdateColor: () -> Unit, onDelete: () -> Unit, ) { ModalBottomSheet( @@ -48,13 +50,13 @@ fun FeedModalBottomSheet( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding( - horizontal = 24.dp + horizontal = MaterialTheme.spacing.largeSpacing ) ) { AsyncImage( model = feed.iconUrl, contentDescription = feed.name!!, - modifier = Modifier.size(48.dp) + modifier = Modifier.size(MaterialTheme.spacing.veryLargeSpacing) ) MediumSpacer() @@ -65,12 +67,14 @@ fun FeedModalBottomSheet( style = MaterialTheme.typography.headlineSmall, ) - VeryShortSpacer() + if (folder != null) { + VeryShortSpacer() - Text( - text = "folder name if it exists", - style = MaterialTheme.typography.labelSmall - ) + Text( + text = folder.name!!, + style = MaterialTheme.typography.labelSmall + ) + } } } @@ -78,7 +82,7 @@ fun FeedModalBottomSheet( Divider( modifier = Modifier.padding( - horizontal = MaterialTheme.spacing.shortSpacing + horizontal = MaterialTheme.spacing.mediumSpacing ) ) @@ -96,6 +100,12 @@ fun FeedModalBottomSheet( onClick = onModify ) + BottomSheetOption( + text = "Update color", + icon = ImageVector.vectorResource(R.drawable.ic_color), + onClick = onUpdateColor + ) + BottomSheetOption( text = "Delete", icon = Icons.Default.Delete, @@ -120,8 +130,8 @@ fun BottomSheetOption( modifier = Modifier .fillMaxWidth() .padding( - horizontal = 16.dp, - vertical = 8.dp + horizontal = MaterialTheme.spacing.mediumSpacing, + vertical = MaterialTheme.spacing.shortSpacing ) ) { diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 2d67b35b..fcf06e9c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -1,6 +1,5 @@ package com.readrops.app.compose.feeds -import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -34,6 +33,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.util.components.Placeholder import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder import org.koin.androidx.compose.getViewModel object FeedTab : Tab { @@ -56,16 +56,17 @@ object FeedTab : Tab { var showDialog by remember { mutableStateOf(false) } var selectedFeed by remember { mutableStateOf(null) } + var selectedFolder by remember { mutableStateOf(null) } var showBottomSheet by remember { mutableStateOf(false) } if (showBottomSheet) { FeedModalBottomSheet( feed = selectedFeed!!, + folder = selectedFolder, onDismissRequest = { showBottomSheet = false }, - onOpen = { - Log.d("TAG", "Content: ") - uriHandler.openUri(selectedFeed!!.siteUrl!!) }, + onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) }, onModify = { }, + onUpdateColor = {}, onDelete = {}, ) } @@ -119,6 +120,7 @@ object FeedTab : Tab { feeds = folderWithFeeds.second, onFeedClick = { feed -> selectedFeed = feed + selectedFolder = folderWithFeeds.first showBottomSheet = true }, onFeedLongClick = { feed -> diff --git a/appcompose/src/main/res/drawable/ic_color.xml b/appcompose/src/main/res/drawable/ic_color.xml new file mode 100644 index 00000000..c3d2c7b3 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_color.xml @@ -0,0 +1,5 @@ + + + From 76cff80e68a15f0ff6dcbfce5b7d8086e35d044b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 12 Jan 2024 21:23:20 +0100 Subject: [PATCH 63/95] Improve FeedTab global behaviour and looking --- .../com/readrops/app/compose/feeds/FeedBottomSheet.kt | 6 +++++- .../main/java/com/readrops/app/compose/feeds/FeedItem.kt | 9 ++++++--- .../readrops/app/compose/feeds/FolderExpandableItem.kt | 8 ++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt index 87eaea42..4b084d2d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage import com.readrops.app.compose.R import com.readrops.app.compose.util.theme.LargeSpacer @@ -65,6 +66,8 @@ fun FeedModalBottomSheet( Text( text = feed.name!!, style = MaterialTheme.typography.headlineSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) if (folder != null) { @@ -72,7 +75,7 @@ fun FeedModalBottomSheet( Text( text = folder.name!!, - style = MaterialTheme.typography.labelSmall + style = MaterialTheme.typography.labelLarge ) } } @@ -127,6 +130,7 @@ fun BottomSheetOption( modifier = Modifier.clickable { onClick() } ) { Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .padding( diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt index af27626b..9c18d7f9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt @@ -12,9 +12,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.theme.spacing import com.readrops.app.compose.util.toDp import com.readrops.db.entities.Feed @@ -35,7 +36,7 @@ fun FeedItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(8.dp) + .padding(MaterialTheme.spacing.shortSpacing) ) { AsyncImage( model = feed.iconUrl, @@ -47,7 +48,9 @@ fun FeedItem( Text( text = feed.name!!, - style = MaterialTheme.typography.bodyLarge + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt index 9e509135..a945c3ab 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import com.readrops.app.compose.R import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.spacing @@ -67,6 +68,7 @@ fun FolderExpandableItem( ) { Row( verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) ) { Icon( painter = painterResource(R.drawable.ic_folder_grey), @@ -77,7 +79,9 @@ fun FolderExpandableItem( Text( text = folder.name!!, - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.headlineSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } @@ -86,7 +90,7 @@ fun FolderExpandableItem( imageVector = Icons.Default.ArrowDropDown, contentDescription = null, modifier = Modifier - .rotate(rotationState), + .rotate(rotationState) ) } } From 672de764ded68d44a8c9ecf0a52a7e689d9a2909 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 14 Jan 2024 00:57:22 +0100 Subject: [PATCH 64/95] Improve AddFeedDialog looking and behaviour - Add error management for URL TextField - Add account dropdown --- .../api/localfeed/LocalRSSDataSource.kt | 5 +- .../readrops/app/compose/ComposeAppModule.kt | 2 +- .../app/compose/feeds/AddFeedDialog.kt | 167 +++++++++++++++--- .../com/readrops/app/compose/feeds/FeedTab.kt | 8 +- .../app/compose/feeds/FeedViewModel.kt | 134 +++++++++++++- .../readrops/db/dao/newdao/NewAccountDao.kt | 3 + 6 files changed, 278 insertions(+), 41 deletions(-) diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt index 867f17bc..cf2a4bc6 100644 --- a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt +++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt @@ -1,6 +1,5 @@ package com.readrops.api.localfeed -import android.accounts.NetworkErrorException import androidx.annotation.WorkerThread import com.gitlab.mvysny.konsumexml.Konsumer import com.gitlab.mvysny.konsumexml.konsumeXml @@ -22,7 +21,6 @@ import okio.Buffer import org.koin.core.component.KoinComponent import org.koin.core.component.get import java.io.IOException -import java.lang.Exception import java.net.HttpURLConnection class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent { @@ -75,7 +73,8 @@ class LocalRSSDataSource(private val httpClient: OkHttpClient) : KoinComponent { val rootKonsumer = nextElement(LocalRSSHelper.RSS_ROOT_NAMES) rootKonsumer?.let { type = LocalRSSHelper.guessRSSType(rootKonsumer) } } catch (e: Exception) { - throw UnknownFormatException(e.message) + close() + return false } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt index 25c84ac1..67de2c86 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/ComposeAppModule.kt @@ -15,7 +15,7 @@ val composeAppModule = module { viewModel { TimelineViewModel(get(), get()) } - viewModel { FeedViewModel(get(), get()) } + viewModel { FeedViewModel(get(), get(), get()) } viewModel { AccountSelectionViewModel(get()) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt index d1bf7b85..a46d6049 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt @@ -1,14 +1,25 @@ package com.readrops.app.compose.feeds import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Card +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextField +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -16,44 +27,144 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.theme.spacing +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AddFeedDialog( + viewModel: FeedViewModel, onDismiss: () -> Unit, - onValidate: (String) -> Unit, ) { - var url by remember { mutableStateOf("") } + val state by viewModel.addFeedDialogState.collectAsStateWithLifecycle() + + var isExpanded by remember { mutableStateOf(false) } + + if (state.closeDialog) { + onDismiss() + } Dialog( onDismissRequest = onDismiss ) { - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .padding(16.dp) + Card( + shape = RoundedCornerShape(16.dp), ) { - Text( - text = "Add new feed", - style = MaterialTheme.typography.headlineSmall - ) - - Spacer(modifier = Modifier.size(8.dp)) - - TextField( - value = url, - onValueChange = { url = it } - ) - - Spacer(modifier = Modifier.size(8.dp)) - - Button( - onClick = { onValidate(url) }, - modifier = Modifier.align(Alignment.CenterHorizontally) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(MaterialTheme.spacing.largeSpacing) ) { - Text(text = "Validate") + Icon( + painter = painterResource(id = R.drawable.ic_rss_feed_grey), + contentDescription = null, + modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) + ) + + MediumSpacer() + + Text( + text = "Add new feed", + style = MaterialTheme.typography.headlineSmall + ) + + MediumSpacer() + + OutlinedTextField( + value = state.url, + label = { + Text(text = "URL") + }, + onValueChange = { viewModel.setAddFeedDialogURL(it) }, + singleLine = true, + trailingIcon = { + if (state.url.isNotEmpty()) { + IconButton( + onClick = { viewModel.setAddFeedDialogURL("") } + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = null + ) + } + } + }, + isError = state.isError(), + supportingText = { Text(state.errorText) } + ) + + ShortSpacer() + + ExposedDropdownMenuBox( + expanded = isExpanded, + onExpandedChange = { isExpanded = isExpanded.not() } + ) { + ExposedDropdownMenu( + expanded = isExpanded, + onDismissRequest = { isExpanded = false } + ) { + for (account in state.accounts) { + DropdownMenuItem( + text = { Text(text = account.accountName!!) }, + onClick = { + isExpanded = false + viewModel.setAddFeedDialogSelectedAccount(account) + }, + leadingIcon = { + Icon( + painter = painterResource( + id = if (state.selectedAccount.isLocal){ + R.drawable.ic_rss_feed_grey} + else + state.selectedAccount.accountType!!.iconRes + ), + contentDescription = null + ) + } + ) + } + } + + OutlinedTextField( + value = state.selectedAccount.accountName!!, + readOnly = true, + onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) + }, + leadingIcon = { + Icon( + painter = painterResource( + id = if (state.selectedAccount.isLocal){ + R.drawable.ic_rss_feed_grey} + else + state.selectedAccount.accountType!!.iconRes + ), + contentDescription = null + ) + }, + modifier = Modifier.menuAnchor() + ) + } + + LargeSpacer() + + TextButton( + onClick = { viewModel.addFeedDialogValidate() }, + ) { + Text(text = "Validate") + } } } } -} \ No newline at end of file +} diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index fcf06e9c..0f21a765 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -73,11 +73,11 @@ object FeedTab : Tab { if (showDialog) { AddFeedDialog( - onDismiss = { showDialog = false }, - onValidate = { + viewModel = viewModel, + onDismiss = { showDialog = false - viewModel.insertFeed(it) - } + viewModel.resetAddFeedDialogState() + }, ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index c1733491..e42a4877 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -1,27 +1,38 @@ package com.readrops.app.compose.feeds +import android.util.Patterns import androidx.lifecycle.viewModelScope +import com.readrops.api.localfeed.LocalRSSDataSource +import com.readrops.api.utils.HtmlParser import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.GetFoldersWithFeeds 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.core.component.KoinComponent +import org.koin.core.component.get class FeedViewModel( database: Database, - private val getFoldersWithFeeds: GetFoldersWithFeeds -) : TabViewModel(database) { + private val getFoldersWithFeeds: GetFoldersWithFeeds, + private val localRSSDataSource: LocalRSSDataSource, +) : TabViewModel(database), KoinComponent { private val _feedsState = MutableStateFlow(FeedsState.InitialState) val feedsState = _feedsState.asStateFlow() + private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState()) + val addFeedDialogState = _addFeedDialogState.asStateFlow() + init { viewModelScope.launch(context = Dispatchers.IO) { accountEvent.consumeAsFlow() @@ -31,11 +42,95 @@ class FeedViewModel( .catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) } .collect { _feedsState.value = FeedsState.LoadedState(it) } } + + viewModelScope.launch(context = Dispatchers.IO) { + database.newAccountDao() + .selectAllAccounts() + .collect { accounts -> + _addFeedDialogState.update { dialogState -> + dialogState.copy( + accounts = accounts, + selectedAccount = accounts.find { it.isCurrentAccount }!! + ) + } + } + } } - fun insertFeed(url: String) { - viewModelScope.launch(context = Dispatchers.IO) { - repository?.insertNewFeeds(listOf(url)) + fun setAddFeedDialogURL(url: String) { + _addFeedDialogState.update { + it.copy( + url = url, + error = null, + ) + } + } + + fun setAddFeedDialogSelectedAccount(account: Account) { + _addFeedDialogState.update { + it.copy( + selectedAccount = account + ) + } + } + + fun addFeedDialogValidate() { + val url = _addFeedDialogState.value.url + + if (url.isEmpty()) { + _addFeedDialogState.update { + it.copy( + error = AddFeedDialogState.AddFeedError.EmptyUrl + ) + } + + return + } else if (!Patterns.WEB_URL.matcher(url).matches()) { + _addFeedDialogState.update { + it.copy( + error = AddFeedDialogState.AddFeedError.BadUrl + ) + } + + return + } + + viewModelScope.launch(Dispatchers.IO) { + if (localRSSDataSource.isUrlRSSResource(url)) { + // TODO add support for all account types + repository?.insertNewFeeds(listOf(url)) + + _addFeedDialogState.update { + it.copy(closeDialog = true) + } + } else { + val rssUrls = HtmlParser.getFeedLink(url, get()) + + if (rssUrls.isEmpty()) { + _addFeedDialogState.update { + it.copy( + error = AddFeedDialogState.AddFeedError.NoRSSFeed + ) + } + } else { + // TODO add support for all account types + repository?.insertNewFeeds(rssUrls.map { it.url }) + + _addFeedDialogState.update { + it.copy(closeDialog = true) + } + } + } + } + } + + fun resetAddFeedDialogState() { + _addFeedDialogState.update { + it.copy( + url = "", + error = null, + closeDialog = false + ) } } } @@ -44,4 +139,33 @@ sealed class FeedsState { object InitialState : FeedsState() data class ErrorState(val exception: Exception) : FeedsState() data class LoadedState(val foldersAndFeeds: Map>) : FeedsState() +} + + +data class AddFeedDialogState( + val url: String = "", + val selectedAccount: Account = Account(accountName = ""), + val accounts: List = listOf(), + val error: AddFeedError? = null, + val closeDialog: Boolean = false, +) { + fun isError() = error != null + + val errorText: String + get() = when (error) { + is AddFeedError.EmptyUrl -> "Field can't be empty" + AddFeedError.BadUrl -> "Input is not a valid URL" + AddFeedError.NoConnection -> "" + AddFeedError.NoRSSFeed -> "No RSS feed found" + AddFeedError.UnreachableUrl -> "" + else -> "" + } + + sealed class AddFeedError { + object EmptyUrl : AddFeedError() + object BadUrl : AddFeedError() + object UnreachableUrl : AddFeedError() + object NoRSSFeed : AddFeedError() + object NoConnection : AddFeedError() + } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt index b62cef67..e5390ffe 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewAccountDao.kt @@ -8,6 +8,9 @@ import kotlinx.coroutines.flow.Flow @Dao interface NewAccountDao : NewBaseDao { + @Query("Select * From Account") + fun selectAllAccounts(): Flow> + @Query("Select Count(*) From Account") suspend fun selectAccountCount(): Int From 69788de077220914eab44f7ba4a9daeb8fb29d71 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 14 Jan 2024 01:01:45 +0100 Subject: [PATCH 65/95] Show feeds without folders in FeedTab --- .../com/readrops/app/compose/feeds/FeedTab.kt | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 0f21a765..e8fdd996 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -114,20 +114,35 @@ object FeedTab : Tab { items( items = foldersAndFeeds.toList() ) { folderWithFeeds -> + + fun onFeedClick(feed: Feed) { + selectedFeed = feed + selectedFolder = folderWithFeeds.first + showBottomSheet = true + } + + fun onFeedLongClick(feed: Feed) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + uriHandler.openUri(feed.siteUrl!!) + } + if (folderWithFeeds.first != null) { FolderExpandableItem( folder = folderWithFeeds.first!!, feeds = folderWithFeeds.second, - onFeedClick = { feed -> - selectedFeed = feed - selectedFolder = folderWithFeeds.first - showBottomSheet = true - }, - onFeedLongClick = { feed -> - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - uriHandler.openUri(feed.siteUrl!!) - } + onFeedClick = { feed -> onFeedClick(feed) }, + onFeedLongClick = { feed -> onFeedLongClick(feed) } ) + } else { + val feeds = folderWithFeeds.second + + for (feed in feeds) { + FeedItem( + feed = feed, + onClick = { onFeedClick(feed) }, + onLongClick = { onFeedLongClick(feed) }, + ) + } } } } From 215399d3acb0b9e17135bc38c9e123247f96f0d3 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 17 Jan 2024 15:48:07 +0100 Subject: [PATCH 66/95] Improve state management in FeedTab --- .../readrops/app/compose/feeds/FeedState.kt | 51 +++++++++++++ .../com/readrops/app/compose/feeds/FeedTab.kt | 73 ++++++++----------- .../app/compose/feeds/FeedViewModel.kt | 57 +++++---------- 3 files changed, 101 insertions(+), 80 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt new file mode 100644 index 00000000..b686f69e --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -0,0 +1,51 @@ +package com.readrops.app.compose.feeds + +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder +import com.readrops.db.entities.account.Account + +data class FeedState( + val foldersAndFeeds: FolderAndFeedsState = FolderAndFeedsState.InitialState, + val dialog: DialogState? = null, +) + +sealed interface DialogState { + object AddFeed : DialogState + class DeleteFeed(val feed: Feed) : DialogState + class UpdateFeed(val feed: Feed) : DialogState + class FeedSheet(val feed: Feed, val folder: Folder?) : DialogState +} + +sealed class FolderAndFeedsState { + object InitialState : FolderAndFeedsState() + data class ErrorState(val exception: Exception) : FolderAndFeedsState() + data class LoadedState(val values: Map>) : FolderAndFeedsState() +} + +data class AddFeedDialogState( + val url: String = "", + val selectedAccount: Account = Account(accountName = ""), + val accounts: List = listOf(), + val error: AddFeedError? = null, + val closeDialog: Boolean = false, +) { + fun isError() = error != null + + val errorText: String + get() = when (error) { + is AddFeedError.EmptyUrl -> "Field can't be empty" + AddFeedError.BadUrl -> "Input is not a valid URL" + AddFeedError.NoConnection -> "" + AddFeedError.NoRSSFeed -> "No RSS feed found" + AddFeedError.UnreachableUrl -> "" + else -> "" + } + + sealed class AddFeedError { + object EmptyUrl : AddFeedError() + object BadUrl : AddFeedError() + object UnreachableUrl : AddFeedError() + object NoRSSFeed : AddFeedError() + object NoConnection : AddFeedError() + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index e8fdd996..d1fb95c7 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -17,9 +17,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType @@ -33,7 +30,6 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.util.components.Placeholder import com.readrops.db.entities.Feed -import com.readrops.db.entities.Folder import org.koin.androidx.compose.getViewModel object FeedTab : Tab { @@ -53,32 +49,31 @@ object FeedTab : Tab { val viewModel = getViewModel() val state by viewModel.feedsState.collectAsStateWithLifecycle() - var showDialog by remember { mutableStateOf(false) } - var selectedFeed by remember { mutableStateOf(null) } - var selectedFolder by remember { mutableStateOf(null) } - var showBottomSheet by remember { mutableStateOf(false) } - - if (showBottomSheet) { - FeedModalBottomSheet( - feed = selectedFeed!!, - folder = selectedFolder, - onDismissRequest = { showBottomSheet = false }, - onOpen = { uriHandler.openUri(selectedFeed!!.siteUrl!!) }, - onModify = { }, - onUpdateColor = {}, - onDelete = {}, - ) - } - - if (showDialog) { - AddFeedDialog( - viewModel = viewModel, - onDismiss = { - showDialog = false - viewModel.resetAddFeedDialogState() - }, - ) + when (val dialog = state.dialog) { + is DialogState.AddFeed -> { + AddFeedDialog( + viewModel = viewModel, + onDismiss = { + viewModel.closeDialog() + viewModel.resetAddFeedDialogState() + }, + ) + } + is DialogState.DeleteFeed -> {} + is DialogState.FeedSheet -> { + FeedModalBottomSheet( + feed = dialog.feed, + folder = dialog.folder, + onDismissRequest = { viewModel.closeDialog() }, + onOpen = { uriHandler.openUri(dialog.feed.siteUrl!!) }, + onModify = { }, + onUpdateColor = {}, + onDelete = {}, + ) + } + is DialogState.UpdateFeed -> {} + null -> {} } Scaffold( @@ -105,9 +100,9 @@ object FeedTab : Tab { .fillMaxSize() .padding(paddingValues) ) { - when (state) { - is FeedsState.LoadedState -> { - val foldersAndFeeds = (state as FeedsState.LoadedState).foldersAndFeeds + when (state.foldersAndFeeds) { + is FolderAndFeedsState.LoadedState -> { + val foldersAndFeeds = (state.foldersAndFeeds as FolderAndFeedsState.LoadedState).values if (foldersAndFeeds.isNotEmpty()) { LazyColumn { @@ -115,12 +110,6 @@ object FeedTab : Tab { items = foldersAndFeeds.toList() ) { folderWithFeeds -> - fun onFeedClick(feed: Feed) { - selectedFeed = feed - selectedFolder = folderWithFeeds.first - showBottomSheet = true - } - fun onFeedLongClick(feed: Feed) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) uriHandler.openUri(feed.siteUrl!!) @@ -130,7 +119,7 @@ object FeedTab : Tab { FolderExpandableItem( folder = folderWithFeeds.first!!, feeds = folderWithFeeds.second, - onFeedClick = { feed -> onFeedClick(feed) }, + onFeedClick = { feed -> viewModel.openFeedSheet(feed, folderWithFeeds.first) }, onFeedLongClick = { feed -> onFeedLongClick(feed) } ) } else { @@ -139,7 +128,7 @@ object FeedTab : Tab { for (feed in feeds) { FeedItem( feed = feed, - onClick = { onFeedClick(feed) }, + onClick = { viewModel.openFeedSheet(feed, null) }, onLongClick = { onFeedLongClick(feed) }, ) } @@ -154,7 +143,7 @@ object FeedTab : Tab { } } - is FeedsState.ErrorState -> { + is FolderAndFeedsState.ErrorState -> { } @@ -167,7 +156,7 @@ object FeedTab : Tab { modifier = Modifier .align(Alignment.BottomEnd) .padding(16.dp), - onClick = { showDialog = true } + onClick = { viewModel.openAddFeedDialog() } ) { Icon( imageVector = Icons.Default.Add, diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index e42a4877..c2e20f58 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -27,8 +27,8 @@ class FeedViewModel( private val localRSSDataSource: LocalRSSDataSource, ) : TabViewModel(database), KoinComponent { - private val _feedsState = MutableStateFlow(FeedsState.InitialState) - val feedsState = _feedsState.asStateFlow() + private val _feedState = MutableStateFlow(FeedState()) + val feedsState = _feedState.asStateFlow() private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState()) val addFeedDialogState = _addFeedDialogState.asStateFlow() @@ -39,8 +39,16 @@ class FeedViewModel( .flatMapConcat { account -> getFoldersWithFeeds.get(account.id) } - .catch { _feedsState.value = FeedsState.ErrorState(Exception(it)) } - .collect { _feedsState.value = FeedsState.LoadedState(it) } + .catch { throwable -> + _feedState.update { + it.copy(foldersAndFeeds = FolderAndFeedsState.ErrorState(Exception(throwable))) + } + } + .collect { foldersAndFeeds -> + _feedState.update { + it.copy(foldersAndFeeds = FolderAndFeedsState.LoadedState(foldersAndFeeds)) + } + } } viewModelScope.launch(context = Dispatchers.IO) { @@ -57,6 +65,13 @@ class FeedViewModel( } } + fun closeDialog() = _feedState.update { it.copy(dialog = null) } + + fun openAddFeedDialog() = _feedState.update { it.copy(dialog = DialogState.AddFeed) } + + fun openFeedSheet(feed: Feed, folder: Folder?) = + _feedState.update { it.copy(dialog = DialogState.FeedSheet(feed, folder)) } + fun setAddFeedDialogURL(url: String) { _addFeedDialogState.update { it.copy( @@ -135,37 +150,3 @@ class FeedViewModel( } } -sealed class FeedsState { - object InitialState : FeedsState() - data class ErrorState(val exception: Exception) : FeedsState() - data class LoadedState(val foldersAndFeeds: Map>) : FeedsState() -} - - -data class AddFeedDialogState( - val url: String = "", - val selectedAccount: Account = Account(accountName = ""), - val accounts: List = listOf(), - val error: AddFeedError? = null, - val closeDialog: Boolean = false, -) { - fun isError() = error != null - - val errorText: String - get() = when (error) { - is AddFeedError.EmptyUrl -> "Field can't be empty" - AddFeedError.BadUrl -> "Input is not a valid URL" - AddFeedError.NoConnection -> "" - AddFeedError.NoRSSFeed -> "No RSS feed found" - AddFeedError.UnreachableUrl -> "" - else -> "" - } - - sealed class AddFeedError { - object EmptyUrl : AddFeedError() - object BadUrl : AddFeedError() - object UnreachableUrl : AddFeedError() - object NoRSSFeed : AddFeedError() - object NoConnection : AddFeedError() - } -} \ No newline at end of file From a6d753ef8ac654b734b9bf950ad42253addf9a66 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 17 Jan 2024 16:42:03 +0100 Subject: [PATCH 67/95] Add DeleteFeedDialog in FeedTab --- .../readrops/app/compose/feeds/FeedState.kt | 3 ++ .../com/readrops/app/compose/feeds/FeedTab.kt | 47 ++++++++++++++++--- .../app/compose/feeds/FeedViewModel.kt | 10 ++-- .../feeds/{ => dialogs}/AddFeedDialog.kt | 3 +- .../compose/feeds/dialogs/DeleteFeedDialog.kt | 45 ++++++++++++++++++ .../feeds/{ => dialogs}/FeedBottomSheet.kt | 2 +- 6 files changed, 97 insertions(+), 13 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/feeds/{ => dialogs}/AddFeedDialog.kt (98%) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt rename appcompose/src/main/java/com/readrops/app/compose/feeds/{ => dialogs}/FeedBottomSheet.kt (98%) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index b686f69e..64236cfe 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -11,8 +11,11 @@ data class FeedState( sealed interface DialogState { object AddFeed : DialogState + object AddFolder : DialogState class DeleteFeed(val feed: Feed) : DialogState + class DeleteFolder(val folder: Folder) : DialogState class UpdateFeed(val feed: Feed) : DialogState + class UpdateFolder(val folder: Folder) : DialogState class FeedSheet(val feed: Feed, val folder: Folder?) : DialogState } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index d1fb95c7..d87be57a 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -28,6 +28,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R +import com.readrops.app.compose.feeds.dialogs.AddFeedDialog +import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog +import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.compose.util.components.Placeholder import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel @@ -60,19 +63,37 @@ object FeedTab : Tab { }, ) } - is DialogState.DeleteFeed -> {} + + is DialogState.DeleteFeed -> { + DeleteFeedDialog( + feed = dialog.feed, + onDismiss = { viewModel.closeDialog() }, + onDelete = { + viewModel.deleteFeed(dialog.feed) + viewModel.closeDialog() + } + ) + } + is DialogState.FeedSheet -> { FeedModalBottomSheet( feed = dialog.feed, folder = dialog.folder, onDismissRequest = { viewModel.closeDialog() }, - onOpen = { uriHandler.openUri(dialog.feed.siteUrl!!) }, + onOpen = { + uriHandler.openUri(dialog.feed.siteUrl!!) + viewModel.closeDialog() + }, onModify = { }, onUpdateColor = {}, - onDelete = {}, + onDelete = { viewModel.openDialog(DialogState.DeleteFeed(dialog.feed)) }, ) } + is DialogState.UpdateFeed -> {} + DialogState.AddFolder -> {} + is DialogState.DeleteFolder -> {} + is DialogState.UpdateFolder -> {} null -> {} } @@ -102,7 +123,8 @@ object FeedTab : Tab { ) { when (state.foldersAndFeeds) { is FolderAndFeedsState.LoadedState -> { - val foldersAndFeeds = (state.foldersAndFeeds as FolderAndFeedsState.LoadedState).values + val foldersAndFeeds = + (state.foldersAndFeeds as FolderAndFeedsState.LoadedState).values if (foldersAndFeeds.isNotEmpty()) { LazyColumn { @@ -119,7 +141,14 @@ object FeedTab : Tab { FolderExpandableItem( folder = folderWithFeeds.first!!, feeds = folderWithFeeds.second, - onFeedClick = { feed -> viewModel.openFeedSheet(feed, folderWithFeeds.first) }, + onFeedClick = { feed -> + viewModel.openDialog( + DialogState.FeedSheet( + feed, + folderWithFeeds.first + ) + ) + }, onFeedLongClick = { feed -> onFeedLongClick(feed) } ) } else { @@ -128,7 +157,11 @@ object FeedTab : Tab { for (feed in feeds) { FeedItem( feed = feed, - onClick = { viewModel.openFeedSheet(feed, null) }, + onClick = { + viewModel.openDialog( + DialogState.FeedSheet(feed, null) + ) + }, onLongClick = { onFeedLongClick(feed) }, ) } @@ -156,7 +189,7 @@ object FeedTab : Tab { modifier = Modifier .align(Alignment.BottomEnd) .padding(16.dp), - onClick = { viewModel.openAddFeedDialog() } + onClick = { viewModel.openDialog(DialogState.AddFeed) } ) { Icon( imageVector = Icons.Default.Add, diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index c2e20f58..71f2f735 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -8,7 +8,6 @@ import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.GetFoldersWithFeeds 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -67,10 +66,13 @@ class FeedViewModel( fun closeDialog() = _feedState.update { it.copy(dialog = null) } - fun openAddFeedDialog() = _feedState.update { it.copy(dialog = DialogState.AddFeed) } + fun openDialog(state: DialogState) = _feedState.update { it.copy(dialog = state) } - fun openFeedSheet(feed: Feed, folder: Folder?) = - _feedState.update { it.copy(dialog = DialogState.FeedSheet(feed, folder)) } + fun deleteFeed(feed: Feed) { + viewModelScope.launch(Dispatchers.IO) { + repository?.deleteFeed(feed) + } + } fun setAddFeedDialogURL(url: String) { _addFeedDialogState.update { diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index a46d6049..ef67a381 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.feeds +package com.readrops.app.compose.feeds.dialogs import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.readrops.app.compose.R +import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.util.theme.LargeSpacer import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.ShortSpacer diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt new file mode 100644 index 00000000..1b708b70 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt @@ -0,0 +1,45 @@ +package com.readrops.app.compose.feeds.dialogs + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import com.readrops.db.entities.Feed + +@Composable +fun DeleteFeedDialog( + feed: Feed, + onDismiss: () -> Unit, + onDelete: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + ) + }, + title = { + Text(text = "Delete feed") + }, + text = { + Text(text = "Do you want to delete feed ${feed.name}?") + }, + confirmButton = { + TextButton(onClick = onDelete) { + Text(text = "Delete") + } + }, + dismissButton = { + TextButton( + onClick = onDismiss + ) { + Text(text = "Cancel") + } + }, + ) +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt similarity index 98% rename from appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt index 4b084d2d..bb6b34d4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedBottomSheet.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt @@ -1,4 +1,4 @@ -package com.readrops.app.compose.feeds +package com.readrops.app.compose.feeds.dialogs import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement From caf55451d3baf39c4aa59369a573eaa6bdc8eff6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 18 Jan 2024 18:45:34 +0100 Subject: [PATCH 68/95] Add UpdateFeedDialog in FeedTab --- .../readrops/app/compose/base/TabViewModel.kt | 6 +- .../readrops/app/compose/feeds/FeedState.kt | 41 +++- .../com/readrops/app/compose/feeds/FeedTab.kt | 11 +- .../app/compose/feeds/FeedViewModel.kt | 180 ++++++++++++++---- .../compose/feeds/dialogs/AddFeedDialog.kt | 6 +- .../compose/feeds/dialogs/FeedBottomSheet.kt | 6 +- .../compose/feeds/dialogs/UpdateFeedDialog.kt | 161 ++++++++++++++++ .../repositories/GetFoldersWithFeeds.kt | 3 +- .../app/compose/timelime/TimelineViewModel.kt | 3 +- .../readrops/db/dao/newdao/NewFolderDao.kt | 5 +- .../com/readrops/db/pojo/FeedWithFolder.kt | 1 + 11 files changed, 363 insertions(+), 60 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt index 22a24f99..8d8a2fd9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/base/TabViewModel.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.viewModelScope import com.readrops.app.compose.repositories.BaseRepository import com.readrops.db.Database import com.readrops.db.entities.account.Account -import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent @@ -26,7 +26,7 @@ abstract class TabViewModel( protected var currentAccount: Account? = null - protected val accountEvent = Channel() + protected val accountEvent = MutableSharedFlow() init { viewModelScope.launch { @@ -38,7 +38,7 @@ abstract class TabViewModel( currentAccount = account repository = get(parameters = { parametersOf(account) }) - accountEvent.send(account) + accountEvent.emit(account) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index 64236cfe..713480ac 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -3,6 +3,7 @@ package com.readrops.app.compose.feeds 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 data class FeedState( val foldersAndFeeds: FolderAndFeedsState = FolderAndFeedsState.InitialState, @@ -14,7 +15,7 @@ sealed interface DialogState { object AddFolder : DialogState class DeleteFeed(val feed: Feed) : DialogState class DeleteFolder(val folder: Folder) : DialogState - class UpdateFeed(val feed: Feed) : DialogState + class UpdateFeed(val feed: Feed, val folder: Folder?) : DialogState class UpdateFolder(val folder: Folder) : DialogState class FeedSheet(val feed: Feed, val folder: Folder?) : DialogState } @@ -30,9 +31,8 @@ data class AddFeedDialogState( val selectedAccount: Account = Account(accountName = ""), val accounts: List = listOf(), val error: AddFeedError? = null, - val closeDialog: Boolean = false, ) { - fun isError() = error != null + val isError: Boolean get() = error != null val errorText: String get() = when (error) { @@ -51,4 +51,39 @@ data class AddFeedDialogState( object NoRSSFeed : AddFeedError() object NoConnection : AddFeedError() } +} + +data class UpdateFeedDialogState( + val feedName: String = "", + val feedNameError: Error? = null, + val feedUrl: String = "", + val feedUrlError: Error? = null, + val accountType: AccountType? = null, + val selectedFolder: Folder? = null, + val folders: List = listOf(), + val isAccountDropDownExpanded: Boolean = false, +) { + + sealed class Error { + object EmptyField : Error() + object BadUrl : Error() + object NoRSSUrl : Error() + } + + val isFeedNameError + get() = feedNameError != null + + val isFeedUrlError + get() = feedUrlError != null + + fun errorText(error: Error?): String = when (error) { + Error.BadUrl -> "Input is not a valid URL" + Error.EmptyField -> "Field can't be empty" + Error.NoRSSUrl -> "The provided URL is not a valid RSS feed" + else -> "" + } + + val isFeedUrlReadOnly: Boolean + get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable + } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index d87be57a..02fc6fda 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -31,6 +31,7 @@ import com.readrops.app.compose.R import com.readrops.app.compose.feeds.dialogs.AddFeedDialog import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet +import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog import com.readrops.app.compose.util.components.Placeholder import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel @@ -84,13 +85,19 @@ object FeedTab : Tab { uriHandler.openUri(dialog.feed.siteUrl!!) viewModel.closeDialog() }, - onModify = { }, + onUpdate = { viewModel.openDialog(DialogState.UpdateFeed(dialog.feed, dialog.folder)) }, onUpdateColor = {}, onDelete = { viewModel.openDialog(DialogState.DeleteFeed(dialog.feed)) }, ) } - is DialogState.UpdateFeed -> {} + is DialogState.UpdateFeed -> { + UpdateFeedDialog( + viewModel = viewModel, + onDismissRequest = { viewModel.closeDialog() } + ) + } + DialogState.AddFolder -> {} is DialogState.DeleteFolder -> {} is DialogState.UpdateFolder -> {} diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 71f2f735..e7e64132 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -8,12 +8,12 @@ import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.GetFoldersWithFeeds 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -32,9 +32,12 @@ class FeedViewModel( private val _addFeedDialogState = MutableStateFlow(AddFeedDialogState()) val addFeedDialogState = _addFeedDialogState.asStateFlow() + private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) + val updateFeedDialogState = _updateFeedDialogState.asStateFlow() + init { viewModelScope.launch(context = Dispatchers.IO) { - accountEvent.consumeAsFlow() + accountEvent .flatMapConcat { account -> getFoldersWithFeeds.get(account.id) } @@ -62,11 +65,39 @@ class FeedViewModel( } } } + + viewModelScope.launch(context = Dispatchers.IO) { + accountEvent + .flatMapConcat { account -> + database.newFolderDao() + .selectFolders(account.id) + } + .collect { folders -> + _updateFeedDialogState.update { + it.copy( + folders = folders, + accountType = currentAccount!!.accountType + ) + } + } + } } fun closeDialog() = _feedState.update { it.copy(dialog = null) } - fun openDialog(state: DialogState) = _feedState.update { it.copy(dialog = state) } + fun openDialog(state: DialogState) { + if (state is DialogState.UpdateFeed) { + _updateFeedDialogState.update { + it.copy( + feedName = state.feed.name!!, + feedUrl = state.feed.url!!, + selectedFolder = state.folder + ) + } + } + + _feedState.update { it.copy(dialog = state) } + } fun deleteFeed(feed: Feed) { viewModelScope.launch(Dispatchers.IO) { @@ -74,6 +105,8 @@ class FeedViewModel( } } + // Add feed + fun setAddFeedDialogURL(url: String) { _addFeedDialogState.update { it.copy( @@ -94,47 +127,41 @@ class FeedViewModel( fun addFeedDialogValidate() { val url = _addFeedDialogState.value.url - if (url.isEmpty()) { - _addFeedDialogState.update { - it.copy( - error = AddFeedDialogState.AddFeedError.EmptyUrl - ) - } - - return - } else if (!Patterns.WEB_URL.matcher(url).matches()) { - _addFeedDialogState.update { - it.copy( - error = AddFeedDialogState.AddFeedError.BadUrl - ) - } - - return - } - - viewModelScope.launch(Dispatchers.IO) { - if (localRSSDataSource.isUrlRSSResource(url)) { - // TODO add support for all account types - repository?.insertNewFeeds(listOf(url)) - + when { + url.isEmpty() -> { _addFeedDialogState.update { - it.copy(closeDialog = true) + it.copy(error = AddFeedDialogState.AddFeedError.EmptyUrl) } - } else { - val rssUrls = HtmlParser.getFeedLink(url, get()) - if (rssUrls.isEmpty()) { - _addFeedDialogState.update { - it.copy( - error = AddFeedDialogState.AddFeedError.NoRSSFeed - ) - } - } else { + return + } + + !Patterns.WEB_URL.matcher(url).matches() -> { + _addFeedDialogState.update { + it.copy(error = AddFeedDialogState.AddFeedError.BadUrl) + } + + return + } + + else -> viewModelScope.launch(Dispatchers.IO) { + if (localRSSDataSource.isUrlRSSResource(url)) { // TODO add support for all account types - repository?.insertNewFeeds(rssUrls.map { it.url }) + repository?.insertNewFeeds(listOf(url)) - _addFeedDialogState.update { - it.copy(closeDialog = true) + closeDialog() + } else { + val rssUrls = HtmlParser.getFeedLink(url, get()) + + if (rssUrls.isEmpty()) { + _addFeedDialogState.update { + it.copy(error = AddFeedDialogState.AddFeedError.NoRSSFeed) + } + } else { + // TODO add support for all account types + repository?.insertNewFeeds(rssUrls.map { it.url }) + + closeDialog() } } } @@ -146,9 +173,82 @@ class FeedViewModel( it.copy( url = "", error = null, - closeDialog = false ) } } + + // add feed + + // update feed + + fun setAccountDropDownState(isExpanded: Boolean) { + _updateFeedDialogState.update { + it.copy(isAccountDropDownExpanded = isExpanded) + } + } + + fun setSelectedFolder(folder: Folder) { + _updateFeedDialogState.update { + it.copy(selectedFolder = folder) + } + } + + fun setUpdateFeedDialogStateFeedName(feedName: String) { + _updateFeedDialogState.update { + it.copy( + feedName = feedName, + feedNameError = null, + ) + } + } + + fun setUpdateFeedDialogFeedUrl(feedUrl: String) { + _updateFeedDialogState.update { + it.copy( + feedUrl = feedUrl, + feedUrlError = null, + ) + } + } + + fun updateFeedDialogValidate() { + val feedName = _updateFeedDialogState.value.feedName + val feedUrl = _updateFeedDialogState.value.feedUrl + + when { + feedName.isEmpty() -> { + _updateFeedDialogState.update { + it.copy(feedNameError = UpdateFeedDialogState.Error.EmptyField) + } + return + } + + feedUrl.isEmpty() -> { + _updateFeedDialogState.update { + it.copy(feedUrlError = UpdateFeedDialogState.Error.EmptyField) + } + return + } + + !Patterns.WEB_URL.matcher(feedUrl).matches() -> { + _updateFeedDialogState.update { + it.copy(feedUrlError = UpdateFeedDialogState.Error.BadUrl) + } + return + } + + else -> { + viewModelScope.launch(Dispatchers.IO) { + // TODO add logig to update feed + //repository?.updateFeed() + closeDialog() + } + } + } + + } + + + // update feed } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index ef67a381..e9c0d564 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -48,10 +48,6 @@ fun AddFeedDialog( var isExpanded by remember { mutableStateOf(false) } - if (state.closeDialog) { - onDismiss() - } - Dialog( onDismissRequest = onDismiss ) { @@ -100,7 +96,7 @@ fun AddFeedDialog( } } }, - isError = state.isError(), + isError = state.isError, supportingText = { Text(state.errorText) } ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt index bb6b34d4..e99b5bbe 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt @@ -39,7 +39,7 @@ fun FeedModalBottomSheet( folder: Folder?, onDismissRequest: () -> Unit, onOpen: () -> Unit, - onModify: () -> Unit, + onUpdate: () -> Unit, onUpdateColor: () -> Unit, onDelete: () -> Unit, ) { @@ -98,9 +98,9 @@ fun FeedModalBottomSheet( ) BottomSheetOption( - text = "Modify", + text = "Update", icon = Icons.Default.Create, - onClick = onModify + onClick = onUpdate ) BottomSheetOption( diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt new file mode 100644 index 00000000..390925cb --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt @@ -0,0 +1,161 @@ +package com.readrops.app.compose.feeds.dialogs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.readrops.app.compose.R +import com.readrops.app.compose.feeds.FeedViewModel +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateFeedDialog( + viewModel: FeedViewModel, + onDismissRequest: () -> Unit +) { + val state by viewModel.updateFeedDialogState.collectAsStateWithLifecycle() + + Dialog( + onDismissRequest = onDismissRequest + ) { + Card( + shape = RoundedCornerShape(16.dp) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(MaterialTheme.spacing.largeSpacing) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_rss_feed_grey), + contentDescription = null, + modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) + ) + + MediumSpacer() + + Text( + text = "Update feed", + style = MaterialTheme.typography.headlineSmall + ) + + MediumSpacer() + + OutlinedTextField( + value = state.feedName, + onValueChange = { viewModel.setUpdateFeedDialogStateFeedName(it) }, + label = { Text(text = "Feed name") }, + singleLine = true, + isError = state.isFeedNameError, + supportingText = { + if (state.isFeedNameError) { + Text( + text = state.errorText(state.feedNameError) + ) + } + } + ) + + MediumSpacer() + + OutlinedTextField( + value = state.feedUrl, + onValueChange = { viewModel.setUpdateFeedDialogFeedUrl(it) }, + label = { Text(text = "Feed URL") }, + singleLine = true, + readOnly = state.isFeedUrlReadOnly, + isError = state.isFeedUrlError, + supportingText = { + if (state.isFeedUrlError) { + Text( + text = state.errorText(state.feedUrlError) + ) + } + } + ) + + MediumSpacer() + + ExposedDropdownMenuBox( + expanded = state.isAccountDropDownExpanded, + onExpandedChange = { viewModel.setAccountDropDownState(state.isAccountDropDownExpanded.not()) } + ) { + ExposedDropdownMenu( + expanded = state.isAccountDropDownExpanded, + onDismissRequest = { viewModel.setAccountDropDownState(false) } + ) { + for (folder in state.folders) { + DropdownMenuItem( + text = { Text(text = folder.name!!) }, + onClick = { + viewModel.setSelectedFolder(folder) + viewModel.setAccountDropDownState(false) + }, + leadingIcon = { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null, + ) + } + ) + } + } + + OutlinedTextField( + value = state.selectedFolder?.name.orEmpty(), + readOnly = true, + onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = state.isAccountDropDownExpanded) + }, + leadingIcon = { + if (state.selectedFolder != null) { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null, + ) + } + }, + modifier = Modifier.menuAnchor() + ) + } + + LargeSpacer() + + TextButton( + onClick = { viewModel.updateFeedDialogValidate() }, + ) { + Text(text = "Validate") + } + } + } + } +} diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index 72154f4f..5c69775d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -24,7 +24,8 @@ class GetFoldersWithFeeds( id = it.feedId, name = it.feedName, iconUrl = it.feedIcon, - siteUrl = it.feedUrl, + url = it.feedUrl, + siteUrl = it.feedSiteUrl, unreadCount = it.unreadCount ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index c0b05173..4d4b06c6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -44,7 +43,7 @@ class TimelineViewModel( init { viewModelScope.launch(dispatcher) { combine( - accountEvent.consumeAsFlow(), + accountEvent, filters ) { account, filters -> filters.accountId = account.id diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index fe81283b..ae5eaec7 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -10,9 +10,12 @@ import kotlinx.coroutines.flow.Flow abstract class NewFolderDao : NewBaseDao { @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + - "Feed.icon_url As feedIcon, Feed.siteUrl as feedUrl, count(*) As unreadCount From Folder Left Join Feed " + + "Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount From Folder Left Join Feed " + "On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " + "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id") abstract fun selectFoldersAndFeeds(accountId: Int): Flow> + + @Query("Select * From Folder Where account_id = :accountId") + abstract fun selectFolders(accountId: Int): Flow> } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt index ea84c72d..d71c5318 100644 --- a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt +++ b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt @@ -19,6 +19,7 @@ data class FolderWithFeed( val feedName: String? = null, val feedIcon: String? = null, val feedUrl: String? = null, + val feedSiteUrl: String? = null, val unreadCount: Int = 0 ) From 2631712361418f67a001b2edeb7b26e4b90b6c0d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 19 Jan 2024 21:35:55 +0100 Subject: [PATCH 69/95] Add AddFolderDialog in FeedTab --- .../readrops/app/compose/feeds/FeedState.kt | 8 +- .../com/readrops/app/compose/feeds/FeedTab.kt | 67 +++++++++++++---- .../app/compose/feeds/FeedViewModel.kt | 44 ++++++++++- .../compose/feeds/dialogs/AddFolderDialog.kt | 57 ++++++++++++++ .../app/compose/repositories/ARepository.kt | 8 +- .../app/compose/util/components/BaseDialog.kt | 74 +++++++++++++++++++ .../src/main/res/drawable/ic_new_folder.xml | 5 ++ .../com/readrops/db/dao/newdao/NewFeedDao.kt | 3 + 8 files changed, 244 insertions(+), 22 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt create mode 100644 appcompose/src/main/res/drawable/ic_new_folder.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index 713480ac..f18a8911 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -86,4 +86,10 @@ data class UpdateFeedDialogState( val isFeedUrlReadOnly: Boolean get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable -} \ No newline at end of file +} + +data class AddFolderState( + val name: String = "", + val isEmpty: Boolean = false, + val errorText: String = "Field can't be empty" +) \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 02fc6fda..de4f6e5d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -1,8 +1,10 @@ package com.readrops.app.compose.feeds import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons @@ -12,12 +14,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback @@ -29,10 +31,12 @@ import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.feeds.dialogs.AddFeedDialog +import com.readrops.app.compose.feeds.dialogs.AddFolderDialog import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog import com.readrops.app.compose.util.components.Placeholder +import com.readrops.app.compose.util.theme.spacing import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel @@ -85,7 +89,14 @@ object FeedTab : Tab { uriHandler.openUri(dialog.feed.siteUrl!!) viewModel.closeDialog() }, - onUpdate = { viewModel.openDialog(DialogState.UpdateFeed(dialog.feed, dialog.folder)) }, + onUpdate = { + viewModel.openDialog( + DialogState.UpdateFeed( + dialog.feed, + dialog.folder + ) + ) + }, onUpdateColor = {}, onDelete = { viewModel.openDialog(DialogState.DeleteFeed(dialog.feed)) }, ) @@ -98,7 +109,17 @@ object FeedTab : Tab { ) } - DialogState.AddFolder -> {} + DialogState.AddFolder -> { + AddFolderDialog( + viewModel = viewModel, + onDismiss = { + viewModel.closeDialog() + viewModel.resetAddFolderState() + } + + ) + } + is DialogState.DeleteFolder -> {} is DialogState.UpdateFolder -> {} null -> {} @@ -122,6 +143,34 @@ object FeedTab : Tab { } ) }, + floatingActionButton = { + Column { + FloatingActionButton( + modifier = Modifier + .padding( + end = 0.dp, + bottom = MaterialTheme.spacing.mediumSpacing + ) + .size(40.dp), + onClick = { viewModel.openDialog(DialogState.AddFolder) } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_new_folder), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + + FloatingActionButton( + onClick = { viewModel.openDialog(DialogState.AddFeed) } + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null + ) + } + } + } ) { paddingValues -> Box( modifier = Modifier @@ -191,18 +240,6 @@ object FeedTab : Tab { } } - - FloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - onClick = { viewModel.openDialog(DialogState.AddFeed) } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null - ) - } } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index e7e64132..f2ee15d6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -35,6 +35,9 @@ class FeedViewModel( private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) val updateFeedDialogState = _updateFeedDialogState.asStateFlow() + private val _addFolderState = MutableStateFlow(AddFolderState()) + val addFolderState = _addFolderState.asStateFlow() + init { viewModelScope.launch(context = Dispatchers.IO) { accountEvent @@ -245,10 +248,47 @@ class FeedViewModel( } } } - } - // update feed + + // add folder + + fun setFolderName(name: String) = _addFolderState.update { + it.copy( + name = name, + isEmpty = false + ) + } + + fun addFolderValidate() { + val name = _addFolderState.value.name + + if (name.isEmpty()) { + _addFolderState.update { + it.copy(isEmpty = true) + } + + return + } + + viewModelScope.launch(Dispatchers.IO) { + repository?.addFolder(Folder(name = name, accountId = currentAccount?.id!!)) + + closeDialog() + resetAddFolderState() + } + } + + fun resetAddFolderState() { + _addFolderState.update { + it.copy( + name = "", + isEmpty = false + ) + } + } + + // add folder } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt new file mode 100644 index 00000000..29076e5c --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt @@ -0,0 +1,57 @@ +package com.readrops.app.compose.feeds.dialogs + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.painterResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.readrops.app.compose.R +import com.readrops.app.compose.feeds.FeedViewModel +import com.readrops.app.compose.util.components.BaseDialog + +@Composable +fun AddFolderDialog( + viewModel: FeedViewModel, + onDismiss: () -> Unit +) { + val state by viewModel.addFolderState.collectAsStateWithLifecycle() + + BaseDialog( + title = "Add Folder", + icon = painterResource(id = R.drawable.ic_new_folder), + onDismiss = { onDismiss() }, + onValidate = { viewModel.addFolderValidate() } + ) { + OutlinedTextField( + value = state.name, + label = { + Text(text = "URL") + }, + onValueChange = { viewModel.setFolderName(it) }, + singleLine = true, + trailingIcon = { + if (state.name.isNotEmpty()) { + IconButton( + onClick = { viewModel.setFolderName("") } + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = null + ) + } + } + }, + isError = state.isEmpty, + supportingText = { + if (state.isEmpty) { + Text(text = state.errorText) + } + } + ) + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index 7e1452f9..e0d4b270 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -47,13 +47,13 @@ abstract class BaseRepository( account: Account, ) : ARepository(database, account) { - open suspend fun updateFeed(feed: Feed) {} + open suspend fun updateFeed(feed: Feed) = database.newFeedDao().updateFeedFields(feed.id, feed.name!!, feed.url!!, feed.folderId!!) - open suspend fun deleteFeed(feed: Feed) {} + open suspend fun deleteFeed(feed: Feed) = database.newFeedDao().delete(feed) - open suspend fun addFolder(folder: Folder) {} + open suspend fun addFolder(folder: Folder) = database.newFolderDao().insert(folder) - open suspend fun deleteFolder(folder: Folder) {} + open suspend fun deleteFolder(folder: Folder) = database.newFolderDao().delete(folder) open suspend fun setItemReadState(item: Item) { database.newItemDao().updateReadState(item.id, item.isRead) diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt new file mode 100644 index 00000000..3fefece1 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt @@ -0,0 +1,74 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing + +@Composable +fun BaseDialog( + title: String, + icon: Painter, + onDismiss: () -> Unit, + onValidate: () -> Unit, + content: @Composable () -> Unit +) { + Dialog( + onDismissRequest = onDismiss + ) { + Card( + shape = RoundedCornerShape(16.dp) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(MaterialTheme.spacing.largeSpacing) + ) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) + ) + + MediumSpacer() + + Text( + text = title, + style = MaterialTheme.typography.headlineSmall + ) + + MediumSpacer() + + content() + + LargeSpacer() + + TextButton( + onClick = { onValidate() }, + ) { + Text(text = "Validate") + } + } + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/res/drawable/ic_new_folder.xml b/appcompose/src/main/res/drawable/ic_new_folder.xml new file mode 100644 index 00000000..43d025db --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_new_folder.xml @@ -0,0 +1,5 @@ + + + diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index 0f0493e2..bb2fb36f 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -25,4 +25,7 @@ abstract class NewFeedDao : NewBaseDao { "Where Feed.folder_id is Null And Item.read = 0 And Feed.account_id = :accountId Group by Feed.id") abstract fun selectFeedsWithoutFolder(accountId: Int): Flow> + @Query("Update Feed set name = :feedName, url = :feedUrl, folder_id = :folderId Where id = :feedId") + abstract fun updateFeedFields(feedId: Int, feedName: String, feedUrl: String, folderId: Int) + } \ No newline at end of file From a3f78094f18dbfa91717858cf56af1f25064238c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Fri, 19 Jan 2024 22:38:41 +0100 Subject: [PATCH 70/95] Merge all TextField errors in TextFieldError class --- .../readrops/app/compose/feeds/FeedState.kt | 47 ++++--------------- .../app/compose/feeds/FeedViewModel.kt | 19 ++++---- .../compose/feeds/dialogs/AddFeedDialog.kt | 2 +- .../compose/feeds/dialogs/AddFolderDialog.kt | 8 +--- .../compose/feeds/dialogs/UpdateFeedDialog.kt | 4 +- .../compose/util/components/TextFieldUtils.kt | 24 ++++++++++ 6 files changed, 47 insertions(+), 57 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/TextFieldUtils.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index f18a8911..f09472be 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose.feeds +import com.readrops.app.compose.util.components.TextFieldError import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import com.readrops.db.entities.account.Account @@ -30,66 +31,34 @@ data class AddFeedDialogState( val url: String = "", val selectedAccount: Account = Account(accountName = ""), val accounts: List = listOf(), - val error: AddFeedError? = null, + val error: TextFieldError? = null, ) { val isError: Boolean get() = error != null - - val errorText: String - get() = when (error) { - is AddFeedError.EmptyUrl -> "Field can't be empty" - AddFeedError.BadUrl -> "Input is not a valid URL" - AddFeedError.NoConnection -> "" - AddFeedError.NoRSSFeed -> "No RSS feed found" - AddFeedError.UnreachableUrl -> "" - else -> "" - } - - sealed class AddFeedError { - object EmptyUrl : AddFeedError() - object BadUrl : AddFeedError() - object UnreachableUrl : AddFeedError() - object NoRSSFeed : AddFeedError() - object NoConnection : AddFeedError() - } } data class UpdateFeedDialogState( val feedName: String = "", - val feedNameError: Error? = null, + val feedNameError: TextFieldError? = null, val feedUrl: String = "", - val feedUrlError: Error? = null, + val feedUrlError: TextFieldError? = null, val accountType: AccountType? = null, val selectedFolder: Folder? = null, val folders: List = listOf(), val isAccountDropDownExpanded: Boolean = false, ) { - - sealed class Error { - object EmptyField : Error() - object BadUrl : Error() - object NoRSSUrl : Error() - } - val isFeedNameError get() = feedNameError != null val isFeedUrlError get() = feedUrlError != null - fun errorText(error: Error?): String = when (error) { - Error.BadUrl -> "Input is not a valid URL" - Error.EmptyField -> "Field can't be empty" - Error.NoRSSUrl -> "The provided URL is not a valid RSS feed" - else -> "" - } - val isFeedUrlReadOnly: Boolean get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable - } data class AddFolderState( val name: String = "", - val isEmpty: Boolean = false, - val errorText: String = "Field can't be empty" -) \ No newline at end of file + val nameError: TextFieldError? = null, +) { + val isError = nameError != null +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index f2ee15d6..b4dc08c8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -6,6 +6,7 @@ import com.readrops.api.localfeed.LocalRSSDataSource import com.readrops.api.utils.HtmlParser import com.readrops.app.compose.base.TabViewModel import com.readrops.app.compose.repositories.GetFoldersWithFeeds +import com.readrops.app.compose.util.components.TextFieldError import com.readrops.db.Database import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder @@ -133,7 +134,7 @@ class FeedViewModel( when { url.isEmpty() -> { _addFeedDialogState.update { - it.copy(error = AddFeedDialogState.AddFeedError.EmptyUrl) + it.copy(error = TextFieldError.EmptyField) } return @@ -141,7 +142,7 @@ class FeedViewModel( !Patterns.WEB_URL.matcher(url).matches() -> { _addFeedDialogState.update { - it.copy(error = AddFeedDialogState.AddFeedError.BadUrl) + it.copy(error = TextFieldError.BadUrl) } return @@ -158,7 +159,7 @@ class FeedViewModel( if (rssUrls.isEmpty()) { _addFeedDialogState.update { - it.copy(error = AddFeedDialogState.AddFeedError.NoRSSFeed) + it.copy(error = TextFieldError.NoRSSFeed) } } else { // TODO add support for all account types @@ -221,21 +222,21 @@ class FeedViewModel( when { feedName.isEmpty() -> { _updateFeedDialogState.update { - it.copy(feedNameError = UpdateFeedDialogState.Error.EmptyField) + it.copy(feedNameError = TextFieldError.EmptyField) } return } feedUrl.isEmpty() -> { _updateFeedDialogState.update { - it.copy(feedUrlError = UpdateFeedDialogState.Error.EmptyField) + it.copy(feedUrlError = TextFieldError.EmptyField) } return } !Patterns.WEB_URL.matcher(feedUrl).matches() -> { _updateFeedDialogState.update { - it.copy(feedUrlError = UpdateFeedDialogState.Error.BadUrl) + it.copy(feedUrlError = TextFieldError.BadUrl) } return } @@ -257,7 +258,7 @@ class FeedViewModel( fun setFolderName(name: String) = _addFolderState.update { it.copy( name = name, - isEmpty = false + nameError = null, ) } @@ -266,7 +267,7 @@ class FeedViewModel( if (name.isEmpty()) { _addFolderState.update { - it.copy(isEmpty = true) + it.copy(nameError = TextFieldError.EmptyField) } return @@ -284,7 +285,7 @@ class FeedViewModel( _addFolderState.update { it.copy( name = "", - isEmpty = false + nameError = null, ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index e9c0d564..aa6d6ec1 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -97,7 +97,7 @@ fun AddFeedDialog( } }, isError = state.isError, - supportingText = { Text(state.errorText) } + supportingText = { Text(state.error?.errorText().orEmpty()) } ) ShortSpacer() diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt index 29076e5c..c014d653 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt @@ -46,12 +46,8 @@ fun AddFolderDialog( } } }, - isError = state.isEmpty, - supportingText = { - if (state.isEmpty) { - Text(text = state.errorText) - } - } + isError = state.isError, + supportingText = { Text(text = state.nameError?.errorText().orEmpty()) } ) } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt index 390925cb..63d389ca 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt @@ -78,7 +78,7 @@ fun UpdateFeedDialog( supportingText = { if (state.isFeedNameError) { Text( - text = state.errorText(state.feedNameError) + text = state.feedNameError?.errorText().orEmpty() ) } } @@ -96,7 +96,7 @@ fun UpdateFeedDialog( supportingText = { if (state.isFeedUrlError) { Text( - text = state.errorText(state.feedUrlError) + text = state.feedUrlError?.errorText().orEmpty() ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/TextFieldUtils.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/TextFieldUtils.kt new file mode 100644 index 00000000..6d9dfa3b --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/TextFieldUtils.kt @@ -0,0 +1,24 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.runtime.Composable + + +sealed class TextFieldError { + object EmptyField : TextFieldError() + object BadUrl : TextFieldError() + object UnreachableUrl : TextFieldError() + object NoRSSFeed : TextFieldError() + object NoRSSUrl : TextFieldError() + + @Composable + fun errorText(): String = + // TODO replace by string resources + when (this) { + BadUrl -> "Input is not a valid URL" + EmptyField -> "Field can't be empty" + NoRSSFeed -> "No RSS feed found" + NoRSSUrl -> "The provided URL is not a valid RSS feed" + UnreachableUrl -> "Unreachable URL" + } +} + From 6f01333065b4288a305d6ce5d6fd6b7525cfaa05 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Feb 2024 17:51:58 +0100 Subject: [PATCH 71/95] Add update and delete folder dialogs --- .../readrops/app/compose/feeds/FeedState.kt | 6 +- .../com/readrops/app/compose/feeds/FeedTab.kt | 63 +++++++++++++++---- .../app/compose/feeds/FeedViewModel.kt | 63 ++++++++++++++----- .../app/compose/feeds/FolderExpandableItem.kt | 52 ++++++++++----- ...lderDialog.kt => AddUpdateFolderDialog.kt} | 18 +++--- .../app/compose/repositories/ARepository.kt | 2 + .../repositories/GetFoldersWithFeeds.kt | 2 +- .../readrops/db/dao/newdao/NewFolderDao.kt | 4 +- .../com/readrops/db/pojo/FeedWithFolder.kt | 3 +- 9 files changed, 158 insertions(+), 55 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/{AddFolderDialog.kt => AddUpdateFolderDialog.kt} (76%) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index f09472be..54bdfd4e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -56,9 +56,11 @@ data class UpdateFeedDialogState( get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable } -data class AddFolderState( - val name: String = "", +data class AddUpdateFolderState( + val folder: Folder = Folder(), val nameError: TextFieldError? = null, ) { + val name = folder.name + val isError = nameError != null } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index de4f6e5d..ea9c78a5 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton @@ -21,6 +22,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalUriHandler @@ -31,7 +33,7 @@ import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.feeds.dialogs.AddFeedDialog -import com.readrops.app.compose.feeds.dialogs.AddFolderDialog +import com.readrops.app.compose.feeds.dialogs.AddUpdateFolderDialog import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog @@ -110,27 +112,54 @@ object FeedTab : Tab { } DialogState.AddFolder -> { - AddFolderDialog( + AddUpdateFolderDialog( viewModel = viewModel, onDismiss = { viewModel.closeDialog() viewModel.resetAddFolderState() + }, + onValidate = { + viewModel.addFolderValidate() } + ) + } + is DialogState.DeleteFolder -> { + TwoChoicesDialog( + title = "Delete folder", + text = "Do you want to delete folder ${dialog.folder.name}?", + icon = rememberVectorPainter(image = Icons.Default.Delete), + confirmText = "Delete", + dismissText = "Cancel", + onDismiss = { viewModel.closeDialog() }, + onConfirm = { + viewModel.deleteFolder(dialog.folder) + viewModel.closeDialog() + } ) } is DialogState.DeleteFolder -> {} - is DialogState.UpdateFolder -> {} + AddUpdateFolderDialog( + updateFolder = true, + viewModel = viewModel, + onDismiss = { + viewModel.closeDialog() + viewModel.resetAddFolderState() + }, + onValidate = { + viewModel.updateFolderValidate() + } + ) + } + null -> {} } Scaffold( topBar = { TopAppBar( - title = { - Text(text = "Feeds") - }, + title = { Text(text = "Feeds") }, actions = { IconButton( onClick = {} @@ -187,25 +216,33 @@ object FeedTab : Tab { items( items = foldersAndFeeds.toList() ) { folderWithFeeds -> - fun onFeedLongClick(feed: Feed) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) uriHandler.openUri(feed.siteUrl!!) } if (folderWithFeeds.first != null) { + val folder = folderWithFeeds.first!! + FolderExpandableItem( - folder = folderWithFeeds.first!!, + folder = folder, feeds = folderWithFeeds.second, onFeedClick = { feed -> viewModel.openDialog( - DialogState.FeedSheet( - feed, - folderWithFeeds.first - ) + DialogState.FeedSheet(feed, folder) ) }, - onFeedLongClick = { feed -> onFeedLongClick(feed) } + onFeedLongClick = { feed -> onFeedLongClick(feed) }, + onUpdateFolder = { + viewModel.openDialog( + DialogState.UpdateFolder(folder) + ) + }, + onDeleteFolder = { + viewModel.openDialog( + DialogState.DeleteFolder(folder) + ) + } ) } else { val feeds = folderWithFeeds.second diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index b4dc08c8..63c8d165 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -36,7 +36,7 @@ class FeedViewModel( private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) val updateFeedDialogState = _updateFeedDialogState.asStateFlow() - private val _addFolderState = MutableStateFlow(AddFolderState()) + private val _addFolderState = MutableStateFlow(AddUpdateFolderState()) val addFolderState = _addFolderState.asStateFlow() init { @@ -100,6 +100,14 @@ class FeedViewModel( } } + if (state is DialogState.UpdateFolder) { + _addFolderState.update { + it.copy( + folder = state.folder + ) + } + } + _feedState.update { it.copy(dialog = state) } } @@ -109,6 +117,12 @@ class FeedViewModel( } } + fun deleteFolder(folder: Folder) { + viewModelScope.launch(Dispatchers.IO) { + repository?.deleteFolder(folder) + } + } + // Add feed fun setAddFeedDialogURL(url: String) { @@ -257,13 +271,43 @@ class FeedViewModel( fun setFolderName(name: String) = _addFolderState.update { it.copy( - name = name, + folder = it.folder.copy(name = name), nameError = null, ) } fun addFolderValidate() { - val name = _addFolderState.value.name + val name = _addFolderState.value.name.orEmpty() + + if (name.isEmpty()) { + _addFolderState.update { it.copy(nameError = TextFieldError.EmptyField) } + + return + } + + viewModelScope.launch(Dispatchers.IO) { + repository?.addFolder(_addFolderState.value.folder.apply { accountId = currentAccount!!.id }) + + closeDialog() + resetAddFolderState() + } + } + + fun resetAddFolderState() { + _addFolderState.update { + it.copy( + folder = Folder(), + nameError = null, + ) + } + } + + // add folder + + // update folder + + fun updateFolderValidate() { + val name = _addFolderState.value.name.orEmpty() if (name.isEmpty()) { _addFolderState.update { @@ -274,22 +318,13 @@ class FeedViewModel( } viewModelScope.launch(Dispatchers.IO) { - repository?.addFolder(Folder(name = name, accountId = currentAccount?.id!!)) + repository?.updateFolder(_addFolderState.value.folder) closeDialog() resetAddFolderState() } } - fun resetAddFolderState() { - _addFolderState.update { - it.copy( - name = "", - nameError = null, - ) - } - } - - // add folder + // update folder } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt index a945c3ab..50b629de 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt @@ -2,17 +2,20 @@ package com.readrops.app.compose.feeds import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,7 +25,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.readrops.app.compose.R @@ -37,12 +39,11 @@ fun FolderExpandableItem( feeds: List, onFeedClick: (Feed) -> Unit, onFeedLongClick: (Feed) -> Unit, + onUpdateFolder: () -> Unit, + onDeleteFolder: () -> Unit ) { var isExpanded by remember { mutableStateOf(false) } - val rotationState by animateFloatAsState( - targetValue = if (isExpanded) 180f else 0f, - label = "folder item arrow rotation" - ) + var isDropDownMenuExpanded by remember { mutableStateOf(false) } Column( modifier = Modifier @@ -85,13 +86,36 @@ fun FolderExpandableItem( ) } - Row { - Icon( - imageVector = Icons.Default.ArrowDropDown, - contentDescription = null, - modifier = Modifier - .rotate(rotationState) - ) + Box { + IconButton( + onClick = { isDropDownMenuExpanded = isDropDownMenuExpanded.not() } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + ) + } + + DropdownMenu( + expanded = isDropDownMenuExpanded, + onDismissRequest = { isDropDownMenuExpanded = false } + ) { + DropdownMenuItem( + text = { Text(text = "Update") }, + onClick = { + isDropDownMenuExpanded = false + onUpdateFolder() + } + ) + + DropdownMenuItem( + text = { Text(text = "Delete") }, + onClick = { + isDropDownMenuExpanded = false + onDeleteFolder() + } + ) + } } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddUpdateFolderDialog.kt similarity index 76% rename from appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddUpdateFolderDialog.kt index c014d653..c7789adc 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFolderDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddUpdateFolderDialog.kt @@ -15,27 +15,29 @@ import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.util.components.BaseDialog @Composable -fun AddFolderDialog( +fun AddUpdateFolderDialog( + updateFolder: Boolean = false, viewModel: FeedViewModel, - onDismiss: () -> Unit + onDismiss: () -> Unit, + onValidate: () -> Unit ) { val state by viewModel.addFolderState.collectAsStateWithLifecycle() BaseDialog( - title = "Add Folder", - icon = painterResource(id = R.drawable.ic_new_folder), - onDismiss = { onDismiss() }, - onValidate = { viewModel.addFolderValidate() } + title = if (updateFolder) "Update Folder" else "Add Folder", + icon = painterResource(id = if (updateFolder) R.drawable.ic_folder_grey else R.drawable.ic_new_folder), + onDismiss = onDismiss, + onValidate = onValidate ) { OutlinedTextField( - value = state.name, + value = state.name.orEmpty(), label = { Text(text = "URL") }, onValueChange = { viewModel.setFolderName(it) }, singleLine = true, trailingIcon = { - if (state.name.isNotEmpty()) { + if (!state.name.isNullOrEmpty()) { IconButton( onClick = { viewModel.setFolderName("") } ) { diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index e0d4b270..24217225 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -53,6 +53,8 @@ abstract class BaseRepository( open suspend fun addFolder(folder: Folder) = database.newFolderDao().insert(folder) + open suspend fun updateFolder(folder: Folder) = database.newFolderDao().update(folder) + open suspend fun deleteFolder(folder: Folder) = database.newFolderDao().delete(folder) open suspend fun setItemReadState(item: Item) { diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index 5c69775d..e140879d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -18,7 +18,7 @@ class GetFoldersWithFeeds( .selectFeedsWithoutFolder(accountId) ) { folders, feedsWithoutFolder -> val foldersWithFeeds = folders.groupBy( - keySelector = { Folder(id = it.folderId, name = it.folderName) }, + keySelector = { Folder(id = it.folderId, name = it.folderName, accountId = it.accountId) }, valueTransform = { Feed( id = it.feedId, diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index ae5eaec7..44a0211d 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.flow.Flow abstract class NewFolderDao : NewBaseDao { @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + - "Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount From Folder Left Join Feed " + - "On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + + "Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount, Folder.account_id as accountId " + + "From Folder Left Join Feed On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " + "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id") abstract fun selectFoldersAndFeeds(accountId: Int): Flow> diff --git a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt index d71c5318..52afda29 100644 --- a/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt +++ b/db/src/main/java/com/readrops/db/pojo/FeedWithFolder.kt @@ -20,7 +20,8 @@ data class FolderWithFeed( val feedIcon: String? = null, val feedUrl: String? = null, val feedSiteUrl: String? = null, - val unreadCount: Int = 0 + val unreadCount: Int = 0, + val accountId: Int = 0 ) data class FeedWithCount( From f0ac2de8e49543452fdacb9113af747ecff40b50 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Feb 2024 17:56:45 +0100 Subject: [PATCH 72/95] Improve dialog for folder and feed deletion in FeedTab --- .../com/readrops/app/compose/feeds/FeedTab.kt | 14 +++--- .../compose/feeds/dialogs/DeleteFeedDialog.kt | 45 ------------------- .../util/components/TwoChoicesDialog.kt | 42 +++++++++++++++++ 3 files changed, 51 insertions(+), 50 deletions(-) delete mode 100644 appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/TwoChoicesDialog.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index ea9c78a5..2b32b767 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -34,10 +34,10 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.feeds.dialogs.AddFeedDialog import com.readrops.app.compose.feeds.dialogs.AddUpdateFolderDialog -import com.readrops.app.compose.feeds.dialogs.DeleteFeedDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog import com.readrops.app.compose.util.components.Placeholder +import com.readrops.app.compose.util.components.TwoChoicesDialog import com.readrops.app.compose.util.theme.spacing import com.readrops.db.entities.Feed import org.koin.androidx.compose.getViewModel @@ -72,10 +72,14 @@ object FeedTab : Tab { } is DialogState.DeleteFeed -> { - DeleteFeedDialog( - feed = dialog.feed, + TwoChoicesDialog( + title = "Delete feed", + text = "Do you want to delete feed ${dialog.feed.name}?", + icon = rememberVectorPainter(image = Icons.Default.Delete), + confirmText = "Delete", + dismissText = "Cancel", onDismiss = { viewModel.closeDialog() }, - onDelete = { + onConfirm = { viewModel.deleteFeed(dialog.feed) viewModel.closeDialog() } @@ -139,7 +143,7 @@ object FeedTab : Tab { ) } - is DialogState.DeleteFolder -> {} + is DialogState.UpdateFolder -> { AddUpdateFolderDialog( updateFolder = true, viewModel = viewModel, diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt deleted file mode 100644 index 1b708b70..00000000 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/DeleteFeedDialog.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.readrops.app.compose.feeds.dialogs - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import com.readrops.db.entities.Feed - -@Composable -fun DeleteFeedDialog( - feed: Feed, - onDismiss: () -> Unit, - onDelete: () -> Unit, -) { - AlertDialog( - onDismissRequest = onDismiss, - icon = { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - ) - }, - title = { - Text(text = "Delete feed") - }, - text = { - Text(text = "Do you want to delete feed ${feed.name}?") - }, - confirmButton = { - TextButton(onClick = onDelete) { - Text(text = "Delete") - } - }, - dismissButton = { - TextButton( - onClick = onDismiss - ) { - Text(text = "Cancel") - } - }, - ) -} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/TwoChoicesDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/TwoChoicesDialog.kt new file mode 100644 index 00000000..85b848b1 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/TwoChoicesDialog.kt @@ -0,0 +1,42 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter + +@Composable +fun TwoChoicesDialog( + title: String, + text: String, + icon: Painter, + confirmText: String, + dismissText: String, + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = icon, + contentDescription = null, + ) + }, + title = { Text(text = title) }, + text = { Text(text = text) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(text = confirmText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss + ) { + Text(text = dismissText) + } + }, + ) +} \ No newline at end of file From e176cdbdb165658384a584e4984c700d4363a769 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Feb 2024 18:24:49 +0100 Subject: [PATCH 73/95] Improve some naming in feed package --- .../readrops/app/compose/feeds/FeedState.kt | 2 +- .../com/readrops/app/compose/feeds/FeedTab.kt | 14 ++-- .../app/compose/feeds/FeedViewModel.kt | 71 +++++++------------ ...dUpdateFolderDialog.kt => FolderDialog.kt} | 4 +- 4 files changed, 34 insertions(+), 57 deletions(-) rename appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/{AddUpdateFolderDialog.kt => FolderDialog.kt} (94%) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index 54bdfd4e..f4f74dc4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -56,7 +56,7 @@ data class UpdateFeedDialogState( get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable } -data class AddUpdateFolderState( +data class FolderState( val folder: Folder = Folder(), val nameError: TextFieldError? = null, ) { diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 2b32b767..77734cb8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -33,8 +33,8 @@ import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.feeds.dialogs.AddFeedDialog -import com.readrops.app.compose.feeds.dialogs.AddUpdateFolderDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet +import com.readrops.app.compose.feeds.dialogs.FolderDialog import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog import com.readrops.app.compose.util.components.Placeholder import com.readrops.app.compose.util.components.TwoChoicesDialog @@ -116,14 +116,14 @@ object FeedTab : Tab { } DialogState.AddFolder -> { - AddUpdateFolderDialog( + FolderDialog( viewModel = viewModel, onDismiss = { viewModel.closeDialog() - viewModel.resetAddFolderState() + viewModel.resetFolderState() }, onValidate = { - viewModel.addFolderValidate() + viewModel.folderValidate() } ) } @@ -144,15 +144,15 @@ object FeedTab : Tab { } is DialogState.UpdateFolder -> { - AddUpdateFolderDialog( + FolderDialog( updateFolder = true, viewModel = viewModel, onDismiss = { viewModel.closeDialog() - viewModel.resetAddFolderState() + viewModel.resetFolderState() }, onValidate = { - viewModel.updateFolderValidate() + viewModel.folderValidate(updateFolder = true) } ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 63c8d165..20e6d228 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -12,6 +12,7 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import com.readrops.db.entities.account.Account import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch @@ -21,6 +22,7 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get +@OptIn(ExperimentalCoroutinesApi::class) class FeedViewModel( database: Database, private val getFoldersWithFeeds: GetFoldersWithFeeds, @@ -36,8 +38,8 @@ class FeedViewModel( private val _updateFeedDialogState = MutableStateFlow(UpdateFeedDialogState()) val updateFeedDialogState = _updateFeedDialogState.asStateFlow() - private val _addFolderState = MutableStateFlow(AddUpdateFolderState()) - val addFolderState = _addFolderState.asStateFlow() + private val _folderState = MutableStateFlow(FolderState()) + val folderState = _folderState.asStateFlow() init { viewModelScope.launch(context = Dispatchers.IO) { @@ -101,7 +103,7 @@ class FeedViewModel( } if (state is DialogState.UpdateFolder) { - _addFolderState.update { + _folderState.update { it.copy( folder = state.folder ) @@ -135,11 +137,7 @@ class FeedViewModel( } fun setAddFeedDialogSelectedAccount(account: Account) { - _addFeedDialogState.update { - it.copy( - selectedAccount = account - ) - } + _addFeedDialogState.update { it.copy(selectedAccount = account) } } fun addFeedDialogValidate() { @@ -267,64 +265,43 @@ class FeedViewModel( // update feed - // add folder + // add/update folder - fun setFolderName(name: String) = _addFolderState.update { + fun setFolderName(name: String) = _folderState.update { it.copy( folder = it.folder.copy(name = name), nameError = null, ) } - fun addFolderValidate() { - val name = _addFolderState.value.name.orEmpty() + fun resetFolderState() = _folderState.update { + it.copy( + folder = Folder(), + nameError = null, + ) + } + + fun folderValidate(updateFolder: Boolean = false) { + val name = _folderState.value.name.orEmpty() if (name.isEmpty()) { - _addFolderState.update { it.copy(nameError = TextFieldError.EmptyField) } + _folderState.update { it.copy(nameError = TextFieldError.EmptyField) } return } viewModelScope.launch(Dispatchers.IO) { - repository?.addFolder(_addFolderState.value.folder.apply { accountId = currentAccount!!.id }) - - closeDialog() - resetAddFolderState() - } - } - - fun resetAddFolderState() { - _addFolderState.update { - it.copy( - folder = Folder(), - nameError = null, - ) - } - } - - // add folder - - // update folder - - fun updateFolderValidate() { - val name = _addFolderState.value.name.orEmpty() - - if (name.isEmpty()) { - _addFolderState.update { - it.copy(nameError = TextFieldError.EmptyField) + if (updateFolder) { + repository?.updateFolder(_folderState.value.folder) + } else { + repository?.addFolder(_folderState.value.folder.apply { accountId = currentAccount!!.id }) } - return - } - - viewModelScope.launch(Dispatchers.IO) { - repository?.updateFolder(_addFolderState.value.folder) - closeDialog() - resetAddFolderState() + resetFolderState() } } - // update folder + // add/update folder } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddUpdateFolderDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt similarity index 94% rename from appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddUpdateFolderDialog.kt rename to appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt index c7789adc..7c48f21e 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddUpdateFolderDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt @@ -15,13 +15,13 @@ import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.util.components.BaseDialog @Composable -fun AddUpdateFolderDialog( +fun FolderDialog( updateFolder: Boolean = false, viewModel: FeedViewModel, onDismiss: () -> Unit, onValidate: () -> Unit ) { - val state by viewModel.addFolderState.collectAsStateWithLifecycle() + val state by viewModel.folderState.collectAsStateWithLifecycle() BaseDialog( title = if (updateFolder) "Update Folder" else "Add Folder", From a3c9e0a89ea20377b08cd2facce0bdbe6baca4fe Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 11 Feb 2024 18:48:45 +0100 Subject: [PATCH 74/95] Improve error handling when adding a new feed in FeedTab --- .../app/compose/feeds/FeedViewModel.kt | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 20e6d228..d2ca97f6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get +import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) class FeedViewModel( @@ -161,23 +162,30 @@ class FeedViewModel( } else -> viewModelScope.launch(Dispatchers.IO) { - if (localRSSDataSource.isUrlRSSResource(url)) { - // TODO add support for all account types - repository?.insertNewFeeds(listOf(url)) - - closeDialog() - } else { - val rssUrls = HtmlParser.getFeedLink(url, get()) - - if (rssUrls.isEmpty()) { - _addFeedDialogState.update { - it.copy(error = TextFieldError.NoRSSFeed) - } - } else { + try { + if (localRSSDataSource.isUrlRSSResource(url)) { // TODO add support for all account types - repository?.insertNewFeeds(rssUrls.map { it.url }) + repository?.insertNewFeeds(listOf(url)) closeDialog() + } else { + val rssUrls = HtmlParser.getFeedLink(url, get()) + + if (rssUrls.isEmpty()) { + _addFeedDialogState.update { + it.copy(error = TextFieldError.NoRSSFeed) + } + } else { + // TODO add support for all account types + repository?.insertNewFeeds(rssUrls.map { it.url }) + + closeDialog() + } + } + } catch (e: Exception) { + when (e) { + is UnknownHostException -> _addFeedDialogState.update { it.copy(error = TextFieldError.UnreachableUrl) } + else -> _addFeedDialogState.update { it.copy(error = TextFieldError.NoRSSFeed) } } } } @@ -303,5 +311,4 @@ class FeedViewModel( } // add/update folder -} - +} \ No newline at end of file From bfd54e1b19e7284dfa1ce8c65e883dc25a22778c Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 13 Feb 2024 22:21:18 +0100 Subject: [PATCH 75/95] Add unfold more/unfold less action in FeedTab --- .../java/com/readrops/app/compose/feeds/FeedState.kt | 1 + .../java/com/readrops/app/compose/feeds/FeedTab.kt | 9 ++++++--- .../com/readrops/app/compose/feeds/FeedViewModel.kt | 7 ++++++- .../app/compose/feeds/FolderExpandableItem.kt | 12 +++++++++--- appcompose/src/main/res/drawable/ic_unfold_less.xml | 5 +++++ appcompose/src/main/res/drawable/ic_unfold_more.xml | 5 +++++ 6 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 appcompose/src/main/res/drawable/ic_unfold_less.xml create mode 100644 appcompose/src/main/res/drawable/ic_unfold_more.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index f4f74dc4..67ea09da 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -9,6 +9,7 @@ import com.readrops.db.entities.account.AccountType data class FeedState( val foldersAndFeeds: FolderAndFeedsState = FolderAndFeedsState.InitialState, val dialog: DialogState? = null, + val areFoldersExpanded: Boolean = false ) sealed interface DialogState { diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 77734cb8..6feb67c4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -166,10 +165,13 @@ object FeedTab : Tab { title = { Text(text = "Feeds") }, actions = { IconButton( - onClick = {} + onClick = { viewModel.setFolderExpandState(state.areFoldersExpanded.not()) } ) { Icon( - imageVector = Icons.Default.MoreVert, + painter = painterResource(id = if (state.areFoldersExpanded) + R.drawable.ic_unfold_less + else + R.drawable.ic_unfold_more), contentDescription = null ) } @@ -231,6 +233,7 @@ object FeedTab : Tab { FolderExpandableItem( folder = folder, feeds = folderWithFeeds.second, + isExpanded = state.areFoldersExpanded, onFeedClick = { feed -> viewModel.openDialog( DialogState.FeedSheet(feed, folder) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index d2ca97f6..26e8152d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -90,6 +90,9 @@ class FeedViewModel( } } + fun setFolderExpandState(isExpanded: Boolean) = + _feedState.update { it.copy(areFoldersExpanded = isExpanded) } + fun closeDialog() = _feedState.update { it.copy(dialog = null) } fun openDialog(state: DialogState) { @@ -302,7 +305,9 @@ class FeedViewModel( if (updateFolder) { repository?.updateFolder(_folderState.value.folder) } else { - repository?.addFolder(_folderState.value.folder.apply { accountId = currentAccount!!.id }) + repository?.addFolder(_folderState.value.folder.apply { + accountId = currentAccount!!.id + }) } closeDialog() diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt index 50b629de..3810c16c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,14 +38,19 @@ import com.readrops.db.entities.Folder fun FolderExpandableItem( folder: Folder, feeds: List, + isExpanded: Boolean = false, onFeedClick: (Feed) -> Unit, onFeedLongClick: (Feed) -> Unit, onUpdateFolder: () -> Unit, onDeleteFolder: () -> Unit ) { - var isExpanded by remember { mutableStateOf(false) } + var isFolderExpanded by remember { mutableStateOf(false) } var isDropDownMenuExpanded by remember { mutableStateOf(false) } + LaunchedEffect(isExpanded) { + isFolderExpanded = isExpanded + } + Column( modifier = Modifier .animateContentSize( @@ -56,7 +62,7 @@ fun FolderExpandableItem( ) { Column( modifier = Modifier - .clickable { isExpanded = isExpanded.not() } + .clickable { isFolderExpanded = isFolderExpanded.not() } .padding( horizontal = MaterialTheme.spacing.shortSpacing, vertical = MaterialTheme.spacing.veryShortSpacing, @@ -121,7 +127,7 @@ fun FolderExpandableItem( } Column { - if (isExpanded) { + if (isFolderExpanded) { for (feed in feeds) { FeedItem( feed = feed, diff --git a/appcompose/src/main/res/drawable/ic_unfold_less.xml b/appcompose/src/main/res/drawable/ic_unfold_less.xml new file mode 100644 index 00000000..5b06bf91 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_unfold_less.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_unfold_more.xml b/appcompose/src/main/res/drawable/ic_unfold_more.xml new file mode 100644 index 00000000..df47d078 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_unfold_more.xml @@ -0,0 +1,5 @@ + + + From 5036e021f5e9ce0540e52acbe42ea1d0570373a4 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 13 Feb 2024 23:06:23 +0100 Subject: [PATCH 76/95] Add initial and error states handling in FeedTab --- .../com/readrops/app/compose/feeds/FeedTab.kt | 11 ++-- .../components/CenteredProgressIndicator.kt | 14 ++++++ .../compose/util/components/ErrorMessage.kt | 50 +++++++++++++++++++ appcompose/src/main/res/drawable/ic_error.xml | 5 ++ 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/CenteredProgressIndicator.kt create mode 100644 appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorMessage.kt create mode 100644 appcompose/src/main/res/drawable/ic_error.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 6feb67c4..e86cc6e7 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -35,6 +35,8 @@ import com.readrops.app.compose.feeds.dialogs.AddFeedDialog import com.readrops.app.compose.feeds.dialogs.FeedModalBottomSheet import com.readrops.app.compose.feeds.dialogs.FolderDialog import com.readrops.app.compose.feeds.dialogs.UpdateFeedDialog +import com.readrops.app.compose.util.components.CenteredProgressIndicator +import com.readrops.app.compose.util.components.ErrorMessage import com.readrops.app.compose.util.components.Placeholder import com.readrops.app.compose.util.components.TwoChoicesDialog import com.readrops.app.compose.util.theme.spacing @@ -276,12 +278,13 @@ object FeedTab : Tab { } } - is FolderAndFeedsState.ErrorState -> { - + is FolderAndFeedsState.InitialState -> { + CenteredProgressIndicator() } - else -> { - + is FolderAndFeedsState.ErrorState -> { + val exception = (state.foldersAndFeeds as FolderAndFeedsState.ErrorState).exception + ErrorMessage(exception = exception) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/CenteredProgressIndicator.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/CenteredProgressIndicator.kt new file mode 100644 index 00000000..496bcfc6 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/CenteredProgressIndicator.kt @@ -0,0 +1,14 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun CenteredProgressIndicator( + modifier: Modifier = Modifier +) { + CenteredColumn { + CircularProgressIndicator(modifier) + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorMessage.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorMessage.kt new file mode 100644 index 00000000..4e035636 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/ErrorMessage.kt @@ -0,0 +1,50 @@ +package com.readrops.app.compose.util.components + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.theme.VeryShortSpacer +import com.readrops.app.compose.util.theme.spacing + +@Composable +fun ErrorMessage( + exception: Exception? +) { + CenteredColumn { + Icon( + painter = painterResource(id = R.drawable.ic_error), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(MaterialTheme.spacing.veryLargeSpacing) + ) + + ShortSpacer() + + Text( + text = "An error occurred", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.error, + ) + + VeryShortSpacer() + + if (exception != null) { + val name = exception.javaClass.simpleName + val message = exception.message + + Text( + text = "$name: $message", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + ) + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/res/drawable/ic_error.xml b/appcompose/src/main/res/drawable/ic_error.xml new file mode 100644 index 00000000..391e8b4c --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_error.xml @@ -0,0 +1,5 @@ + + + From 768522eee6b4f17bb55ffcead2b6ab53d17c5563 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 13 Feb 2024 23:23:19 +0100 Subject: [PATCH 77/95] Improve small FAB appearance in FeedTab --- .../java/com/readrops/app/compose/feeds/FeedTab.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index e86cc6e7..940e58d8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -16,6 +16,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -182,13 +183,12 @@ object FeedTab : Tab { }, floatingActionButton = { Column { - FloatingActionButton( + SmallFloatingActionButton( modifier = Modifier .padding( - end = 0.dp, - bottom = MaterialTheme.spacing.mediumSpacing - ) - .size(40.dp), + start = MaterialTheme.spacing.veryShortSpacing, + bottom = MaterialTheme.spacing.shortSpacing + ), onClick = { viewModel.openDialog(DialogState.AddFolder) } ) { Icon( From 423a2265103d3ee15b3edd2f0e8bca0bfa4e7bf8 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 14 Feb 2024 21:24:14 +0100 Subject: [PATCH 78/95] Transfer strings from old app package to the new one --- appcompose/src/main/res/values-de/strings.xml | 138 ++++++++++++++++ appcompose/src/main/res/values-fr/strings.xml | 144 +++++++++++++++++ appcompose/src/main/res/values-in/strings.xml | 137 ++++++++++++++++ appcompose/src/main/res/values-it/strings.xml | 138 ++++++++++++++++ .../src/main/res/values-nb-rNO/strings.xml | 69 ++++++++ appcompose/src/main/res/values/strings.xml | 148 +++++++++++++++++- 6 files changed, 773 insertions(+), 1 deletion(-) create mode 100644 appcompose/src/main/res/values-de/strings.xml create mode 100644 appcompose/src/main/res/values-fr/strings.xml create mode 100644 appcompose/src/main/res/values-in/strings.xml create mode 100644 appcompose/src/main/res/values-it/strings.xml create mode 100644 appcompose/src/main/res/values-nb-rNO/strings.xml diff --git a/appcompose/src/main/res/values-de/strings.xml b/appcompose/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..7807062c --- /dev/null +++ b/appcompose/src/main/res/values-de/strings.xml @@ -0,0 +1,138 @@ + + + Favoriten + Browser-Ansicht + Synchronisieren + Bildunterschrift anzeigen + Zurück + Öffnen + Damit die Benachrichtigungen angezeigt werden, muss die automatische Synchronisierung aktiviert sein. +\nWollen Sie die Einstellungen öffnen\? + Automatische Synchronisierung ist deaktiviert + Alle Feed-Benachrichtigungen aktivieren + Benachrichtigungen + Benachrichtigungen einschalten + %1$s neue Artikel + Synchronisierung der Konten + Täglich + 12 Stunden + 6 Stunden + 3 Stunden + 2 Stunden + 1 Stunde + 30 Min. + Manuell + Automatische Synchronisierung + Zum Herunterladen des Bildes ist eine Speichererlaubnis erforderlich + Neuer Feed + Exportieren von Feeds und Ordnern + Dunkel + Hell + Farbschema + Bild teilen + Bild herunterladen + Bildoptionen + Oder + Berechtigungen + Erneut versuchen + Der Export von Abonnements erfordert die Genehmigung für externen Speicher + OPML-Export + OPML-Import + Bei der Verarbeitung der Datei ist ein Fehler aufgetreten + Dieser Vorgang kann viel Zeit in Anspruch nehmen, da jeder Feed abgefragt werden muss. + Verarbeitung der OPML-Datei + OPML-Import/Export + URL teilen + Aktualisieren + Externer Browser + Webview + Artikel öffnen in + Feedfarben neu laden + Global + Feedfarben + Abrufen von Feedfarben + Fehler für Feed %1$s + Kein Feed gefunden + Keine Artikel + Löschen + %1$s Feed + %1$s Feeds + Lokal + Unbegrenzt + Maximale Anzahl von Artikeln pro Feed + Anwendung unter der GPLv3-Lizenz veröffentlicht + Neues Konto + Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Anmeldedaten + Älteste zuerst + Neueste zuerst + Anmeldeinformationen + Der Ordner existiert nicht auf dem Server + Falsches Format für den neuen Ordner + Der Ordner existiert bereits + Ein Fehler ist aufgetreten + Der Feed %1$s existiert nicht auf dem Server + Der Feed %1$s wurde gelöscht + Konto löschen\? + Konto löschen + Ordner löschen\? + Ordner bearbeiten + Feeds + Ordner + Feeds und Ordner verwalten + Konto + Feeds und Ordner + Wählen Sie ein Konto + Keine Feeds + Konto hinzufügen + Kontoeinstellungen + Dies ist Ihr FreshRSS-API-Passwort (Konfiguration > Profil) + Passwort + Identifikationsdaten + Kontobezeichnung + Konto-URL + Alle auswählen + Als gelesen markieren + Als ungelesen markieren + Filtern + Gelesene Artikel anzeigen + Später zu lesen + Artikel + Unbekannter Fehler für Feed %1$s + Falsches Format für Feed %1$s + Fehler beim Analysieren des Feeds %1$s + Netzwerkfehler beim Zugriff auf den Feed %1$s + Feed %1$s erfolgreich hinzugefügt + Ergebnisse + Aktualisierung des Feeds: %1$s + Laden + Feed löschen\? + Keine Ordner + Abbrechen + Ordner + Feed bearbeiten + Feedname + Feed-Ordner + Ordner hinzufügen + URL öffnen + Artikel teilen + %1$s Min. + 1 Min. + Weniger als eine Minute + von %1$s + Unbekannter Host + Verbindungsfehler + Keine Feed-URL gefunden + Eingabe ist keine gültige URL + Feld darf nicht leer sein + Validieren + Feed-URL + Feed hinzufügen + Über + Einstellungen + Ordner hinzufügen + Feed hinzufügen + Menü schließen + Menü öffnen + Nicht gelesene Artikel + Zum Lesen + \ No newline at end of file diff --git a/appcompose/src/main/res/values-fr/strings.xml b/appcompose/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..3384ba26 --- /dev/null +++ b/appcompose/src/main/res/values-fr/strings.xml @@ -0,0 +1,144 @@ + + + À lire + Articles non lus + Ouvrir le menu + Fermer le menu + Ajouter un flux + Ajouter un dossier + Paramètres + À propos + Ajouter un flux + Adresse du flux + Valider + Le champ ne peut pas être vide + La valeur n\'est pas une adresse web valide + Aucune adresse de flux trouvée + Erreur de connexion au site + Site inconnu + par %1$s + %1$s min + Moins d\'une minute + 1 min + Partager l\'article + Ouvrir le lien + Ajouter un dossier + Dossier du flux + Nom du flux + Modifier le flux + Dossier + Aucun dossier + Retour + Supprimer le flux ? + Charger + Mise à jour du flux : %1$s + Résultats + Le flux %1$s a été ajouté + Erreur d\'accès au flux %1$s + Erreur de traitement pour le flux %1$s + Mauvais format pour le flux %1$s + Erreur inconnue pour le flux %1$s + Articles + À lire plus tard + Afficher les articles lus + Filtrer + Marquer comme non lu + Marquer comme lu + Tout sélectionner + URL du compte + Nom du compte + Identifiant + Mot de passe + Paramètres du compte + Ajouter un compte + Aucun flux + Choisir un compte + Flux et dossiers + Compte + Gérer les flux et dossiers + Dossiers + Flux + Modifier le dossier + Supprimer le dossier ? + Supprimer le compte + Supprimer le compte ? + Le flux %1$s a été supprimé + Le flux %1$s n\'existe pas sur le serveur + Une erreur s\'est produite + Le dossier existe déjà + Mauvais format pour le nouveau dossier + Le dossier n\'existe pas sur le serveur + Identifiants + Du plus récent au plus ancien + Du plus ancien au plus récent + La connexion a échoué. Veuillez vérifier vos identifiants + Nouveau compte + Appli distribuée sous la licence GPLv3 + Nombre maximum d\'articles par flux + Illimité + Local + %1$s flux + %1$s flux + Supprimer + Aucun item + Aucun flux trouvé + Erreur pour le flux %1$s + Récupération des couleurs des flux + Couleurs des flux + Général + Recharger les couleurs des flux + Ouvrir les articles avec + Vue web + Navigateur externe + Actualiser + Partager le lien + Importation/Exportation OPML + Traitement du fichier OPML + Cette opération peut prendre un certain temps car il faut interroger chaque flux. + Une erreur s\'est produite lors du traitement du fichier + Import OPML + Export OPML + L\'export des soubscriptions nécessite l\'accès au stockage + Réessayer + Permissions + Ou + Options de l\'image + Télécharger l\'image + Partager l\'image + Thème + Clair + Sombre + Export des flux et dossiers + Nouveau flux + Le téléchargement de l\'image nécessite l\'accès au stockage + Synchronisation automatique + Manuel + 30 min + 1 heure + 2 heures + 3 heures + 6 heures + 12 heures + Chaque jour + Synchronisation des comptes + %1$s nouveaux articles + Activer les notifications + Notifications + Activer toutes les notifications des flux + La synchronisation automatique est désactivée + Les notifications nécessitent l\'activation de la synchronisation automatique pour fonctionner.\nVoulez-vous ouvrir les paramètres ? + Ouvrir + Retour + Afficher la légende + Votre mot de passe d\'API (Configuration > Profil) + Synchroniser + Vue navigateur + Favoris + Code source + Journal des modifications + App distribuée sous la licence GPLv3 + Thème du système + Cacher les flux sans nouveaux items + Marquer les items comme lus pendant le défilement + + \ No newline at end of file diff --git a/appcompose/src/main/res/values-in/strings.xml b/appcompose/src/main/res/values-in/strings.xml new file mode 100644 index 00000000..88f6a676 --- /dev/null +++ b/appcompose/src/main/res/values-in/strings.xml @@ -0,0 +1,137 @@ + + + To read + Non read articles + Buka menu + Tutup menu + Tambah feed + Tambah folder + Pengaturan + Tentang + Tambah feed + Url feed + Validasi + Ruas tidak boleh kosong + URL tidak valid + Url feed tidak ditemukan + Galat koneksi + Hos tidak diketahui + oleh %1$s + %1$s mnt + Kurang dari 1 menit + 1 mnt + Bagikan Artikel + Buka url + Tambah folder + Folder feed + Nama feed + Sunting feed + Folder + Tidak ada folder + Batal + Hapus feed ? + Muat + Memperbarui feed : %1$s + Hasil + Feed %1$s berhasil ditambahkan + Jaringan terputus saat mengakses feed %1$s + Terjadi kesalahan saat mengurai feed %1$s + Kesalahan format untuk feed %1$s + Kesalahan tidak diketahui untuk feed %1$s + Artikel + Baca nanti + Tampilkan artikel dibaca + Filter + Tandai belum dibaca + Tandai dibaca + Pilih semua + Url akun + Nama akun + Masuk + Sandi + Ini adalah sandi API FreshRSS Anda (Konfigurasi > Profil) + Pengaturan akun + Tambah akun + Tidak ada feed + Pilih akun + Feed dan folder + Akun + Kelola feed dan folder + Folder + Feed + Sunting folder + Hapus folder ? + Hapus akun + Hapus akun ? + Feed %1$s telah dihapus + Feed %1$s tidak ada pada server + Telah terjadi keslahan + Folder sudah ada + Format tidak valid untuk folder baru + Folder tidak ada pada server + Kredensial + Terbaru > terlama + Terlama > terbaru + Gagal masuk. Silakan periksa kredensial Anda + Akun baru + Jumlah maksimum item per feed + Tidak terbatas + Lokal + %1$s feed + %1$s feed + Hapus + Aplikasi dirilis dengan lisensi GPLv3 + Tidak ada item + Feed tidak ditemukan + Galat feed %1$s + Ambil warna feed + Warna Feed + Global + Muat ulang warna feed + Buka item di + Webview + Peramban eksternal + Aktualisasikan + Bagikan URL + Impor/Ekspor OPML + Memproses berkas OPML + Proses ini bisa memakan waktu lama karena feed harus diproses satu persatu. + Telah terjadi kesalahan saat memproses berkas + Impor OPML + Ekspor OPML + Ekspor berkas OPML membutuhkan izin akses penyimpanan eksternal + Coba lagi + Perizinan + Atau + Opsi Gambar + Unduh gambar + Bagikan gambar + Tema + Terang + Gelap + Ekspor feed dan folder + Feed baru + Untuk mengunduh gambar, dibutuhkan izin akses penyimpanan + Sinkronisasi otomatis + Manual + 30 mnt + 1 jam + 2 jam + 3 jam + 6 jam + 12 jam + Sehari sekali + Sinkronisasi akun + %1$s artikel baru + Aktifkan notifikasi + Notifikasi + Aktifkan semua notifikasi feed + Sinkronisasi otomatis dinonaktifkan + Agar bisa ditampilkan, notifikasi membutuhkan sinkronisasi otomatis untuk diaktifkan.\nApakah Anda ingin membuka pengaturan ? + Buka + Kembali + Tampilkan takarir + Sinkronkan + Navigator view + Favorit + \ No newline at end of file diff --git a/appcompose/src/main/res/values-it/strings.xml b/appcompose/src/main/res/values-it/strings.xml new file mode 100644 index 00000000..058dafcb --- /dev/null +++ b/appcompose/src/main/res/values-it/strings.xml @@ -0,0 +1,138 @@ + + + Browser esterno + Preferiti + Vista browser + Sincronizza + Mostra didascalia + Indietro + Apri + Per essere visualizzate, le notifiche hanno bisogno che la sincronizzazione automatica sia attivata. +\nVuoi aprire le impostazioni\? + La sincronizzazione automatica è disabilitata + Abilita tutte le notifiche dei flussi + Notifiche + Abilita le notifiche + %1$s nuovi articoli + Sincronizzazione degli account + Ogni giorno + 12 ore + 6 ore + 3 ore + 2 ore + 1 ora + 30 min + Manuale + Sincronizzazione automatica + Per scaricare l\'immagine, è necessaria l\'autorizzazione di archiviazione + Nuovo flusso + Esportazione dei flussi e delle cartelle + Scuro + Chiaro + Tema + Condividi l\'immagine + Scarica l\'immagine + Opzioni dell\'immagine + O + Autorizzazioni + Riprova + L\'esportazione delle sottoscrizioni necessita di un permesso di archiviazione esterna + Esportazione OPML + Importazione OPML + Si è verificato un errore durante l\'elaborazione del file + Questa operazione può richiedere un tempo significativo, poiché ogni flusso deve essere interrogato. + Elaborazione del file OPML + Importazione/esportazione OPML + Condividi l\'URL + Aggiorna + Webview + Apri gli elementi con + Ricarica i colori dei flussi + Generale + Colori dei flussi + Recupera i colori dei flussi + Errore per il flusso %1$s + Nessun flusso trovato + Nessun elemento + Elimina + %1$s flusso + %1$s flussi + Locale + Illimitato + Numero massimo di articoli per flusso + Applicazione pubblicata sotto licenza GPLv3 + Nuovo account + Accesso non riuscito. Per favore controlla le tue credenziali + Dal più vecchio al più recente + Dal più recente al più vecchio + Credenziali + La cartella non esiste sul server + Cattivo formato per la nuova cartella + La cartella esiste già + Si è verificato un errore + Il flusso %1$s non esiste sul server + Il flusso %1$s è stato eliminato + Eliminare l\'account\? + Elimina l\'account + Eliminare la cartella\? + Modifica la cartella + Flussi + Cartelle + Gestisci i flussi e le cartelle + Account + Flussi e cartelle + Scegli un account + Nessuno flusso + Aggiungi un account + Impostazioni dell\'account + Questa è la tua password FreshRSS API (Configurazione > Profilo) + Password + Nome utente + Nome dell\'account + URL dell\'account + Seleziona tutto + Segna come letto + Segna come non letto + Filtra + Mostra gli articoli letti + Da leggere più tardi + Articoli + Errore sconosciuto per il flusso %1$s + Formato errato per il flusso %1$s + Errore durante l\'analisi del flusso %1$s + Errore di rete durante l\'accesso al flusso %1$s + Il flusso %1$s è stato aggiunto con successo + Risultati + Aggiornamento del flusso: %1$s + Carica + Eliminare il flusso\? + Annulla + Nessuna cartella + Cartella + Modifica il flusso + Nome del flusso + Cartella del flusso + Aggiungi una cartella + Apri l\'URL + Condividi l\'articolo + 1 min + Meno di un minuto + %1$s min + da %1$s + Sito sconosciuto + Errore di connessione + Nessun URL di flusso trovato + L\'ingresso non è un URL valido + Il campo non può essere vuoto + Convalida + URL del flusso + Aggiungi un flusso + Informazioni + Impostazioni + Aggiungi una cartella + Aggiungi un flusso + Chiudi il menù + Apri il menù + Articoli non letti + Da leggere + \ No newline at end of file diff --git a/appcompose/src/main/res/values-nb-rNO/strings.xml b/appcompose/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 00000000..1142cae7 --- /dev/null +++ b/appcompose/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,69 @@ + + + Favoritter + Manuell + Ny informasjonskanal + Mørk + Lys + Drakt + Del bilde + Last ned bilde + Bildevalg + Eller + Tilganger + Prøv igjen + OPML-eksport + OPML-import + Slett + Ny konto + Mappen finnes allerede + Slett konto\? + Slett konto + Slett mappe\? + Rediger mappe + Informasjonskanaler + Mapper + Konto + Kontonettadresse + Kontoinnstillinger + Passord + Kontonavn + Velg alle + Marker som lest + Marker som ulest + Filter + Vis leste artikler + Artikler + Last inn + Slett informasjonskanal\? + Avbryt + Ingen mappe + Mappe + Rediger informasjonskanal + Informasjonskanalsnavn + Informasjonskanalsmappe + Legg til mappe + Åpne nettadresse + Informasjonskanalsnettadresse + Fant ingen informasjonskanalsnettadresse + Feltet kan ikke stå tomt + Bekreft + Legg til informasjonskanal + Om + Innstillinger + Legg til mappe + Legg til informasjonskanal + Lukk meny + Åpne meny + Del nettadresse + Identitetsdetaljer + Resultater + Del artikkel + Ett minutt + Mindre enn ett minutt + %1$s min + av %1$s + Ukjent vert + Tilkoblingsfeil + Ikke-leste artikler + \ No newline at end of file diff --git a/appcompose/src/main/res/values/strings.xml b/appcompose/src/main/res/values/strings.xml index 731b4987..07c22295 100644 --- a/appcompose/src/main/res/values/strings.xml +++ b/appcompose/src/main/res/values/strings.xml @@ -1,3 +1,149 @@ + - app compose + Readrops + To read + Non read articles + Open menu + Close menu + Add feed + Add folder + Settings + About + Add feed + Feed url + Validate + Field can\'t be empty + Input is not a valid URL + No feed url found + Connection error + Unknown host + by %1$s + %1$s mins + Less than a minute + 1 min + · + Share Article + Open url + Add folder + Feed folder + Feed name + Edit feed + Folder + No folder + Cancel + Delete feed ? + Load + Updating feed : %1$s + Results + Feed %1$s added successfully + Network failure when accessing feed %1$s + Failure when parsing feed %1$s + Wrong format for feed %1$s + Unknown error for feed %1$s + Articles + To read later + Show read articles + Filter + Mark as non read + Mark as read + Select all + Account url + Account name + Login + Password + This is your FreshRSS API password (Configuration > Profile) + Account settings + Add account + No feed + Choose an account + Feeds and folders + Account + Manage feeds and folders + Folders + Feeds + Edit folder + Delete folder ? + Delete account + Delete account ? + The feed %1$s has been deleted + The feed %1$s doesn\'t exist on the server + An error occured + The folder already exists + Bad format for the new folder + The folder doesn\'t exist on the server + Credentials + Newest to oldest + Oldest to newest + Login failed. Please check your credentials + New account + https://github.com/readrops/Readrops + Maximum number of items per feed + Unlimited + Local + %1$s feeds + %1$s feed + Delete + App released under the GPLv3 licence + No item + No feed found + Error for feed %1$s + Get feeds colors + Feeds Colors + Global + Reload feeds colors + Open items in + Webview + External web browser + Actualize + Share URL + OPML Import/Export + Processing OPML file + This operation can take a significant time as each feed needs to be queried. + An error occurred during the file processing + OPML import + OPML export + Subscriptions export needs external storage permission + Try again + Permissions + Or + Image Options + Download image + Share image + Theme + Light + Dark + Export feeds and folders + New feed + To download the image, storage permission is needed + Automatic synchronisation + Manual + 30 mins + 1 hour + 2 hours + 3 hours + 6 hours + 12 hours + Every day + Accounts synchronisation + %1$s new articles + Enable notifications + Notifications + Enable all feeds notifications + Automatic synchronization is disabled + To be displayed, notifications need auto synchronization to be activated.\nDo you want to open settings ? + Open + Back + Show caption + Synchronize + Navigator view + Favorites + Source code + Changelog + https://github.com/readrops/Readrops/blob/develop/CHANGELOG.md + System theme + light + dark + system + Hide feeds without new items + Mark items read on scroll \ No newline at end of file From 8718781af42f993bd8e7d28ab457129cf7fab2d0 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 14 Feb 2024 22:10:42 +0100 Subject: [PATCH 79/95] Use string resources in FeedTab --- .../com/readrops/app/compose/feeds/FeedTab.kt | 19 ++++++++++--------- .../app/compose/feeds/FolderExpandableItem.kt | 3 ++- .../compose/feeds/dialogs/AddFeedDialog.kt | 5 +++-- .../compose/feeds/dialogs/FeedBottomSheet.kt | 5 +++-- .../app/compose/feeds/dialogs/FolderDialog.kt | 3 ++- .../compose/feeds/dialogs/UpdateFeedDialog.kt | 9 +++++---- 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt index 940e58d8..61e8b7b2 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedTab.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.tab.Tab @@ -50,7 +51,7 @@ object FeedTab : Tab { @Composable get() = TabOptions( index = 2u, - title = "Feeds" + title = stringResource(R.string.feeds) ) @OptIn(ExperimentalMaterial3Api::class) @@ -75,11 +76,11 @@ object FeedTab : Tab { is DialogState.DeleteFeed -> { TwoChoicesDialog( - title = "Delete feed", + title = stringResource(R.string.delete_feed), text = "Do you want to delete feed ${dialog.feed.name}?", icon = rememberVectorPainter(image = Icons.Default.Delete), - confirmText = "Delete", - dismissText = "Cancel", + confirmText = stringResource(R.string.delete), + dismissText = stringResource(R.string.cancel), onDismiss = { viewModel.closeDialog() }, onConfirm = { viewModel.deleteFeed(dialog.feed) @@ -132,11 +133,11 @@ object FeedTab : Tab { is DialogState.DeleteFolder -> { TwoChoicesDialog( - title = "Delete folder", + title = stringResource(R.string.delete_folder), text = "Do you want to delete folder ${dialog.folder.name}?", icon = rememberVectorPainter(image = Icons.Default.Delete), - confirmText = "Delete", - dismissText = "Cancel", + confirmText = stringResource(R.string.delete), + dismissText = stringResource(R.string.cancel), onDismiss = { viewModel.closeDialog() }, onConfirm = { viewModel.deleteFolder(dialog.folder) @@ -165,7 +166,7 @@ object FeedTab : Tab { Scaffold( topBar = { TopAppBar( - title = { Text(text = "Feeds") }, + title = { Text(text = stringResource(R.string.feeds)) }, actions = { IconButton( onClick = { viewModel.setFolderExpandState(state.areFoldersExpanded.not()) } @@ -272,7 +273,7 @@ object FeedTab : Tab { } } else { Placeholder( - text = "No feed", + text = stringResource(R.string.no_feed), painter = painterResource(R.drawable.ic_rss_feed_grey) ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt index 3810c16c..b9667bb8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import com.readrops.app.compose.R import com.readrops.app.compose.util.theme.MediumSpacer @@ -115,7 +116,7 @@ fun FolderExpandableItem( ) DropdownMenuItem( - text = { Text(text = "Delete") }, + text = { Text(text = stringResource(R.string.delete)) }, onClick = { isDropDownMenuExpanded = false onDeleteFolder() diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index aa6d6ec1..e555db29 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -71,7 +72,7 @@ fun AddFeedDialog( MediumSpacer() Text( - text = "Add new feed", + text = stringResource(R.string.add_feed_item), style = MaterialTheme.typography.headlineSmall ) @@ -159,7 +160,7 @@ fun AddFeedDialog( TextButton( onClick = { viewModel.addFeedDialogValidate() }, ) { - Text(text = "Validate") + Text(text = stringResource(R.string.validate)) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt index e99b5bbe..0baabd24 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FeedBottomSheet.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage @@ -92,7 +93,7 @@ fun FeedModalBottomSheet( MediumSpacer() BottomSheetOption( - text = "Open", + text = stringResource(R.string.open), icon = ImageVector.vectorResource(id = R.drawable.ic_open_in_browser), onClick = onOpen ) @@ -110,7 +111,7 @@ fun FeedModalBottomSheet( ) BottomSheetOption( - text = "Delete", + text = stringResource(R.string.delete), icon = Icons.Default.Delete, onClick = onDelete ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt index 7c48f21e..c8d1bdcf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.readrops.app.compose.R import com.readrops.app.compose.feeds.FeedViewModel @@ -24,7 +25,7 @@ fun FolderDialog( val state by viewModel.folderState.collectAsStateWithLifecycle() BaseDialog( - title = if (updateFolder) "Update Folder" else "Add Folder", + title = stringResource(id = if (updateFolder) R.string.edit_folder else R.string.add_folder), icon = painterResource(id = if (updateFolder) R.drawable.ic_folder_grey else R.drawable.ic_new_folder), onDismiss = onDismiss, onValidate = onValidate diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt index 63d389ca..0333795b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -63,7 +64,7 @@ fun UpdateFeedDialog( MediumSpacer() Text( - text = "Update feed", + text = stringResource(R.string.edit_feed), style = MaterialTheme.typography.headlineSmall ) @@ -72,7 +73,7 @@ fun UpdateFeedDialog( OutlinedTextField( value = state.feedName, onValueChange = { viewModel.setUpdateFeedDialogStateFeedName(it) }, - label = { Text(text = "Feed name") }, + label = { Text(text = stringResource(R.string.feed_name)) }, singleLine = true, isError = state.isFeedNameError, supportingText = { @@ -89,7 +90,7 @@ fun UpdateFeedDialog( OutlinedTextField( value = state.feedUrl, onValueChange = { viewModel.setUpdateFeedDialogFeedUrl(it) }, - label = { Text(text = "Feed URL") }, + label = { Text(text = stringResource(R.string.feed_url)) }, singleLine = true, readOnly = state.isFeedUrlReadOnly, isError = state.isFeedUrlError, @@ -153,7 +154,7 @@ fun UpdateFeedDialog( TextButton( onClick = { viewModel.updateFeedDialogValidate() }, ) { - Text(text = "Validate") + Text(text = stringResource(R.string.validate)) } } } From 9aa2004627821db594ce6de4a36d92f058eb4720 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 14 Feb 2024 23:49:45 +0100 Subject: [PATCH 80/95] Improve global FeedTab layout --- .../com/readrops/app/compose/feeds/FeedItem.kt | 5 ++++- .../app/compose/feeds/FolderExpandableItem.kt | 7 ++++--- .../app/compose/feeds/dialogs/AddFeedDialog.kt | 14 +++++++------- .../app/compose/feeds/dialogs/UpdateFeedDialog.kt | 2 +- .../app/compose/util/components/BaseDialog.kt | 2 +- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt index 9c18d7f9..a133256d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedItem.kt @@ -36,7 +36,10 @@ fun FeedItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(MaterialTheme.spacing.shortSpacing) + .padding( + horizontal = MaterialTheme.spacing.mediumSpacing, + vertical = MaterialTheme.spacing.shortSpacing + ) ) { AsyncImage( model = feed.iconUrl, diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt index b9667bb8..6e5bd6b3 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FolderExpandableItem.kt @@ -65,8 +65,9 @@ fun FolderExpandableItem( modifier = Modifier .clickable { isFolderExpanded = isFolderExpanded.not() } .padding( - horizontal = MaterialTheme.spacing.shortSpacing, - vertical = MaterialTheme.spacing.veryShortSpacing, + start = MaterialTheme.spacing.mediumSpacing, + top = MaterialTheme.spacing.veryShortSpacing, + bottom = MaterialTheme.spacing.veryShortSpacing ) ) { Row( @@ -105,7 +106,7 @@ fun FolderExpandableItem( DropdownMenu( expanded = isDropDownMenuExpanded, - onDismissRequest = { isDropDownMenuExpanded = false } + onDismissRequest = { isDropDownMenuExpanded = false }, ) { DropdownMenuItem( text = { Text(text = "Update") }, diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index e555db29..08fb00cf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -53,7 +53,7 @@ fun AddFeedDialog( onDismissRequest = onDismiss ) { Card( - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(24.dp), ) { Column( verticalArrangement = Arrangement.Center, @@ -121,9 +121,9 @@ fun AddFeedDialog( leadingIcon = { Icon( painter = painterResource( - id = if (state.selectedAccount.isLocal){ - R.drawable.ic_rss_feed_grey} - else + id = if (state.selectedAccount.isLocal) { + R.drawable.ic_rss_feed_grey + } else state.selectedAccount.accountType!!.iconRes ), contentDescription = null @@ -143,9 +143,9 @@ fun AddFeedDialog( leadingIcon = { Icon( painter = painterResource( - id = if (state.selectedAccount.isLocal){ - R.drawable.ic_rss_feed_grey} - else + id = if (state.selectedAccount.isLocal) { + R.drawable.ic_rss_feed_grey + } else state.selectedAccount.accountType!!.iconRes ), contentDescription = null diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt index 0333795b..d24b5015 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt @@ -45,7 +45,7 @@ fun UpdateFeedDialog( onDismissRequest = onDismissRequest ) { Card( - shape = RoundedCornerShape(16.dp) + shape = RoundedCornerShape(24.dp) ) { Column( verticalArrangement = Arrangement.Center, diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt index 3fefece1..29018ef4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt @@ -34,7 +34,7 @@ fun BaseDialog( onDismissRequest = onDismiss ) { Card( - shape = RoundedCornerShape(16.dp) + shape = RoundedCornerShape(24.dp) ) { Column( verticalArrangement = Arrangement.Center, From 90b8c54e18718fe5fda43995376ed18d11fcbcc9 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 15 Feb 2024 12:03:39 +0100 Subject: [PATCH 81/95] Use string resources in TimelineTab when possible --- .../java/com/readrops/app/compose/timelime/TimelineTab.kt | 3 ++- .../readrops/app/compose/timelime/drawer/TimelineDrawer.kt | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 5b643202..f1307067 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems @@ -114,7 +115,7 @@ object TimelineTab : Tab { Scaffold( topBar = { TopAppBar( - title = { Text(text = "Articles") }, + title = { Text(text = stringResource(R.string.articles)) }, navigationIcon = { IconButton( onClick = { viewModel.openDrawer() } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt index 5b3627c2..a534dcb6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt @@ -19,6 +19,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage @@ -117,7 +118,7 @@ fun DrawerDefaultItems( onClick: (FilterType) -> Unit, ) { NavigationDrawerItem( - label = { Text("Articles") }, + label = { Text(text = stringResource(R.string.articles)) }, icon = { Icon( painter = painterResource(id = R.drawable.ic_timeline), @@ -143,7 +144,7 @@ fun DrawerDefaultItems( ) NavigationDrawerItem( - label = { Text("Favorites") }, + label = { Text(text = stringResource(R.string.favorites)) }, icon = { Icon( imageVector = Icons.Filled.Star, @@ -156,7 +157,7 @@ fun DrawerDefaultItems( ) NavigationDrawerItem( - label = { Text("To read later") }, + label = { Text(text = stringResource(R.string.read_later)) }, icon = { Icon( painter = painterResource(id = R.drawable.ic_read_later), From 6c875d9d93bd37e2d850478fb79ed3a32053cd5e Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 15 Feb 2024 12:15:57 +0100 Subject: [PATCH 82/95] Update build workflow to use JDK 1.17 --- .github/workflows/android.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index ed02aca4..7a5fb3d3 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -15,11 +15,11 @@ jobs: steps: - uses: actions/checkout@v2 - - name: set up JDK 1.11 + - name: set up JDK 1.17 uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '11' + java-version: '17' - name: Android Emulator Runner uses: ReactiveCircus/android-emulator-runner@v2.28.0 with: @@ -28,5 +28,5 @@ jobs: - uses: codecov/codecov-action@v2.1.0 with: files: ./build/reports/jacoco/jacocoFullReport.xml - fail_ci_if_error: true + fail_ci_if_error: false verbose: true \ No newline at end of file From a29b27801e85cf5f90955cf4312a6ca7395d7be6 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 15 Feb 2024 21:41:32 +0100 Subject: [PATCH 83/95] Fix gradle full build step --- api/build.gradle | 1 + app/proguard-rules.pro | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/api/build.gradle b/api/build.gradle index 05dfdb5b..3c855767 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -35,6 +35,7 @@ android { } kotlinOptions { jvmTarget = '17' + freeCompilerArgs = ["-Xstring-concat=inline"] } lint { abortOnError false diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 77345f4c..d755688e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -31,4 +31,21 @@ -keep class com.readrops.api.localfeed.** { *; } --keep class com.readrops.api.opml.model.** { *; } \ No newline at end of file +-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 \ No newline at end of file From ad13ba99e026a933f9bbb44ec67037226489ed1d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Feb 2024 19:04:10 +0100 Subject: [PATCH 84/95] Fix GetFoldersWithFeeds test --- .../app/compose/GetFoldersWithFeedsTest.kt | 36 +++++++++++++------ .../com/readrops/db/dao/newdao/NewFeedDao.kt | 4 +-- .../readrops/db/dao/newdao/NewFolderDao.kt | 2 +- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt b/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt index b0b5ac2d..0cf78a58 100644 --- a/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt +++ b/appcompose/src/androidTest/java/com/readrops/app/compose/GetFoldersWithFeedsTest.kt @@ -10,7 +10,7 @@ import com.readrops.db.entities.Folder import com.readrops.db.entities.Item import com.readrops.db.entities.account.Account import com.readrops.db.entities.account.AccountType -import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.joda.time.LocalDateTime import org.junit.Before @@ -31,34 +31,48 @@ class GetFoldersWithFeedsTest { runTest { account.id = database.newAccountDao().insert(account).toInt() + // inserting 3 folders repeat(3) { time -> - database.newFolderDao().insert(Folder(name = "Folder $time", accountId = account.id)) + database.newFolderDao() + .insert(Folder(name = "Folder $time", accountId = account.id)) } + // inserting 2 feeds, not linked to any folder repeat(2) { time -> database.newFeedDao().insert(Feed(name = "Feed $time", accountId = account.id)) } + // inserting 2 feeds linked to first folder (Folder 0) repeat(2) { time -> - database.newFeedDao().insert(Feed(name = "Feed ${time+2}", folderId = 1, accountId = account.id)) + database.newFeedDao() + .insert(Feed(name = "Feed ${time + 2}", folderId = 1, accountId = account.id)) } + // inserting 3 items linked to first feed (Feed 0) repeat(3) { time -> - database.newItemDao().insert(Item(title = "Item $time", feedId = 1, pubDate = LocalDateTime.now())) + database.newItemDao() + .insert(Item(title = "Item $time", feedId = 1, pubDate = LocalDateTime.now())) } } } @Test fun getFoldersWithFeedsTest() = runTest { - getFoldersWithFeeds = GetFoldersWithFeeds(database, StandardTestDispatcher(testScheduler)) - val foldersAndFeeds = getFoldersWithFeeds.get(account.id) + getFoldersWithFeeds = GetFoldersWithFeeds(database) + val job = launch { + getFoldersWithFeeds.get(account.id) + .collect { foldersAndFeeds -> - assertTrue { foldersAndFeeds.size == 4 } - assertTrue { foldersAndFeeds.entries.first().value.size == 2 } - assertTrue { foldersAndFeeds.entries.last().key == null } - assertTrue { foldersAndFeeds[null]!!.size == 2 } - assertTrue { foldersAndFeeds[null]!!.first().unreadCount == 3 } + assertTrue { foldersAndFeeds.size == 4 } + assertTrue { foldersAndFeeds.entries.first().value.size == 2 } + assertTrue { foldersAndFeeds.entries.last().key == null } + assertTrue { foldersAndFeeds[null]!!.size == 2 } + assertTrue { foldersAndFeeds[null]!!.first().unreadCount == 3 } + } + } + + // for an unknown reason, the coroutine must be canceled to stop the test, and I don't really know why + job.cancel() } } \ No newline at end of file diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index bb2fb36f..f5aa95cb 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -21,8 +21,8 @@ abstract class NewFeedDao : NewBaseDao { @Query("Select case When :feedUrl In (Select url from Feed Where account_id = :accountId) Then 1 else 0 end") abstract suspend fun feedExists(feedUrl: String, accountId: Int): Boolean - @Query("Select Feed.*, count(*) as unreadCount From Feed Inner Join Item On Feed.id = Item.feed_id " + - "Where Feed.folder_id is Null And Item.read = 0 And Feed.account_id = :accountId Group by Feed.id") + @Query("Select Feed.*, count(*) as unreadCount From Feed Left Join Item On Feed.id = Item.feed_id " + + "Where Feed.folder_id is Null And (Item.read = 0 OR Item.read is NULL) And Feed.account_id = :accountId Group by Feed.id") abstract fun selectFeedsWithoutFolder(accountId: Int): Flow> @Query("Update Feed set name = :feedName, url = :feedUrl, folder_id = :folderId Where id = :feedId") diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt index 44a0211d..4968b6a5 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFolderDao.kt @@ -12,7 +12,7 @@ abstract class NewFolderDao : NewBaseDao { @Query("Select Folder.id As folderId, Folder.name as folderName, Feed.id As feedId, Feed.name AS feedName, " + "Feed.icon_url As feedIcon, Feed.url as feedUrl, Feed.siteUrl as feedSiteUrl, count(*) As unreadCount, Folder.account_id as accountId " + "From Folder Left Join Feed On Folder.id = Feed.folder_id Left Join Item On Item.feed_id = Feed.id " + - "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And Item.read = 0 " + + "Where Feed.folder_id is NULL OR Feed.folder_id is NOT NULL And (Item.read = 0 OR Item.read is NULL) " + "And Feed.account_id = :accountId Group By Feed.id, Folder.id Order By Folder.id") abstract fun selectFoldersAndFeeds(accountId: Int): Flow> From fc297596410d259d756735644a93eb2a1081a412 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Feb 2024 21:40:45 +0100 Subject: [PATCH 85/95] Add logic to update feed in FeedTab --- .../com/readrops/app/compose/feeds/FeedState.kt | 1 + .../readrops/app/compose/feeds/FeedViewModel.kt | 14 ++++++++++++-- .../app/compose/repositories/ARepository.kt | 2 +- .../java/com/readrops/db/dao/newdao/NewFeedDao.kt | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index 67ea09da..f969d32c 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -38,6 +38,7 @@ data class AddFeedDialogState( } data class UpdateFeedDialogState( + val feedId: Int = 0, val feedName: String = "", val feedNameError: TextFieldError? = null, val feedUrl: String = "", diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt index 26e8152d..92f96557 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedViewModel.kt @@ -99,6 +99,7 @@ class FeedViewModel( if (state is DialogState.UpdateFeed) { _updateFeedDialogState.update { it.copy( + feedId = state.feed.id, feedName = state.feed.name!!, feedUrl = state.feed.url!!, selectedFolder = state.folder @@ -266,8 +267,17 @@ class FeedViewModel( else -> { viewModelScope.launch(Dispatchers.IO) { - // TODO add logig to update feed - //repository?.updateFeed() + with(_updateFeedDialogState.value) { + repository?.updateFeed( + Feed( + id = feedId, + name = feedName, + url = feedUrl, + folderId = selectedFolder?.id + ) + ) + } + closeDialog() } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index 24217225..72e8c299 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -47,7 +47,7 @@ abstract class BaseRepository( account: Account, ) : ARepository(database, account) { - open suspend fun updateFeed(feed: Feed) = database.newFeedDao().updateFeedFields(feed.id, feed.name!!, feed.url!!, feed.folderId!!) + open suspend fun updateFeed(feed: Feed) = database.newFeedDao().updateFeedFields(feed.id, feed.name!!, feed.url!!, feed.folderId) open suspend fun deleteFeed(feed: Feed) = database.newFeedDao().delete(feed) diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt index f5aa95cb..edf2219d 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewFeedDao.kt @@ -26,6 +26,6 @@ abstract class NewFeedDao : NewBaseDao { abstract fun selectFeedsWithoutFolder(accountId: Int): Flow> @Query("Update Feed set name = :feedName, url = :feedUrl, folder_id = :folderId Where id = :feedId") - abstract fun updateFeedFields(feedId: Int, feedName: String, feedUrl: String, folderId: Int) + abstract fun updateFeedFields(feedId: Int, feedName: String, feedUrl: String, folderId: Int?) } \ No newline at end of file From 939af36b2d17a83ba7af3697687054f97da9e42f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Feb 2024 22:50:02 +0100 Subject: [PATCH 86/95] Add initial layout of AccountTab --- .../app/compose/account/AccountTab.kt | 110 ++++++++++++++---- .../app/compose/account/AccountViewModel.kt | 27 ++++- .../app/compose/util/components/IconText.kt | 40 +++++++ .../src/main/res/drawable/ic_add_account.xml | 5 + .../main/res/drawable/ic_notifications.xml | 5 + 5 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 appcompose/src/main/res/drawable/ic_add_account.xml create mode 100644 appcompose/src/main/res/drawable/ic_notifications.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index a47d147f..1d9404b8 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -1,25 +1,42 @@ package com.readrops.app.compose.account +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.readrops.app.compose.R import com.readrops.app.compose.account.selection.AccountSelectionScreen +import com.readrops.app.compose.util.components.SelectableIconText +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing import org.koin.androidx.compose.getViewModel object AccountTab : Tab { @@ -28,7 +45,7 @@ object AccountTab : Tab { @Composable get() = TabOptions( index = 3u, - title = "Account" + title = stringResource(R.string.account) ) @OptIn(ExperimentalMaterial3Api::class) @@ -36,7 +53,9 @@ object AccountTab : Tab { override fun Content() { val navigator = LocalNavigator.currentOrThrow val viewModel = getViewModel() + val closeHome by viewModel.closeHome.collectAsStateWithLifecycle() + val state by viewModel.accountState.collectAsStateWithLifecycle() if (closeHome) { navigator.replaceAll(AccountSelectionScreen()) @@ -44,31 +63,82 @@ object AccountTab : Tab { Scaffold( topBar = { - TopAppBar(title = { Text(text = "Account") }) + TopAppBar( + title = { Text(text = stringResource(R.string.account)) }, + actions = { + IconButton( + onClick = { } + ) { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null + ) + } + } + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = {} + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_account), + contentDescription = null + ) + } } ) { paddingValues -> Column( - modifier = Modifier.padding(paddingValues) + modifier = Modifier + .padding(paddingValues) ) { - Row { - Button(onClick = { viewModel.deleteAccount() }) { - Text( - text = "Delete" - ) - } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.ic_freshrss), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) - Spacer(modifier = Modifier.size(16.dp)) + MediumSpacer() - Button(onClick = { navigator.push(AccountSelectionScreen()) }) { - Text( - text = "New" - ) - } + Text( + text = state.account.accountName!!, + style = MaterialTheme.typography.titleLarge + ) } + + LargeSpacer() + + SelectableIconText( + icon = painterResource(id = R.drawable.ic_add_account), + text = stringResource(R.string.credentials), + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + onClick = { } + ) + + SelectableIconText( + icon = painterResource(id = R.drawable.ic_notifications), + text = stringResource(R.string.notifications), + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + onClick = { } + ) + + SelectableIconText( + icon = rememberVectorPainter(image = Icons.Default.AccountCircle), + text = stringResource(R.string.delete_account), + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + color = MaterialTheme.colorScheme.error, + tint = MaterialTheme.colorScheme.error, + onClick = { /*viewModel.deleteAccount()*/ } + ) } } - - } - } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt index 2f4d6efb..084afe1b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt @@ -1,8 +1,10 @@ package com.readrops.app.compose.account -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.readrops.app.compose.base.TabViewModel import com.readrops.db.Database +import com.readrops.db.entities.account.Account +import com.readrops.db.entities.account.AccountType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -11,11 +13,26 @@ import kotlinx.coroutines.launch class AccountViewModel( private val database: Database -) : ViewModel() { +) : TabViewModel(database) { private val _closeHome = MutableStateFlow(false) val closeHome = _closeHome.asStateFlow() + private val _accountState = MutableStateFlow(AccountState()) + val accountState = _accountState.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + accountEvent.collect { account -> + _accountState.update { + it.copy( + account = account + ) + } + } + } + } + fun deleteAccount() { viewModelScope.launch(Dispatchers.IO) { @@ -25,5 +42,9 @@ class AccountViewModel( _closeHome.update { true } } } +} -} \ No newline at end of file +data class AccountState( + val account: Account = Account(accountName = "account", accountType = AccountType.LOCAL), + val dialog: Unit = Unit, +) \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt index d5fb784b..720d43b4 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt @@ -1,8 +1,11 @@ package com.readrops.app.compose.util.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon @@ -25,6 +28,7 @@ fun IconText( text: String, style: TextStyle, modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, tint: Color = LocalContentColor.current, padding: Dp = MaterialTheme.spacing.veryShortSpacing, onClick: (() -> Unit)? = null, @@ -45,6 +49,42 @@ fun IconText( Text( text = text, style = style, + color = color, ) } +} + +@Composable +fun SelectableIconText( + icon: Painter, + text: String, + style: TextStyle, + onClick: () -> Unit, + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, + tint: Color = LocalContentColor.current, + padding: Dp = MaterialTheme.spacing.veryShortSpacing, +) { + Box( + modifier = modifier.clickable { onClick() } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = MaterialTheme.spacing.mediumSpacing, + vertical = MaterialTheme.spacing.mediumSpacing + ) + ) { + IconText( + icon = icon, + text = text, + style = style, + padding = padding, + tint = tint, + color = color + ) + } + } } \ No newline at end of file diff --git a/appcompose/src/main/res/drawable/ic_add_account.xml b/appcompose/src/main/res/drawable/ic_add_account.xml new file mode 100644 index 00000000..e0e8a950 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_add_account.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_notifications.xml b/appcompose/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 00000000..1d038a44 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,5 @@ + + + From 5b74480398e89932490942734e836929231141e7 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Sun, 18 Feb 2024 23:38:47 +0100 Subject: [PATCH 87/95] Add initial layout of MoreTab --- .../com/readrops/app/compose/more/MoreTab.kt | 71 +++++++++++++++++-- .../src/main/res/drawable/ic_backup.xml | 5 ++ .../src/main/res/drawable/ic_settings.xml | 5 ++ 3 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 appcompose/src/main/res/drawable/ic_backup.xml create mode 100644 appcompose/src/main/res/drawable/ic_settings.xml diff --git a/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt b/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt index e7d915f1..6990af89 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt @@ -1,26 +1,89 @@ package com.readrops.app.compose.more +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions +import com.readrops.app.compose.BuildConfig +import com.readrops.app.compose.R +import com.readrops.app.compose.util.components.SelectableIconText +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.theme.spacing object MoreTab : Tab { override val options: TabOptions @Composable get() = TabOptions( - index = 4u, - title = "More" + index = 4u, + title = "More" ) @Composable override fun Content() { - Column { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize() + ) { + LargeSpacer() + + Image( + painter = painterResource(id = R.drawable.ic_freshrss), + contentDescription = null, + modifier = Modifier.size(64.dp) + ) + + MediumSpacer() + Text( - text = "More" + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge + ) + + ShortSpacer() + + Text( + text = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", + style = MaterialTheme.typography.labelLarge + ) + + LargeSpacer() + + SelectableIconText( + icon = painterResource(id = R.drawable.ic_settings), + text = stringResource(R.string.settings), + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + onClick = { } + ) + + SelectableIconText( + icon = painterResource(id = R.drawable.ic_settings), + text = "Backup", + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + onClick = { } + ) + + SelectableIconText( + icon = painterResource(id = R.drawable.ic_settings), + text = "Open-source libraries", + style = MaterialTheme.typography.titleMedium, + padding = MaterialTheme.spacing.mediumSpacing, + onClick = { } ) } } diff --git a/appcompose/src/main/res/drawable/ic_backup.xml b/appcompose/src/main/res/drawable/ic_backup.xml new file mode 100644 index 00000000..24f2a3d6 --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_backup.xml @@ -0,0 +1,5 @@ + + + diff --git a/appcompose/src/main/res/drawable/ic_settings.xml b/appcompose/src/main/res/drawable/ic_settings.xml new file mode 100644 index 00000000..298a5a1f --- /dev/null +++ b/appcompose/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,5 @@ + + + From 13557c00e64cd89c03e5176bd3948040a17c1e21 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 19 Feb 2024 13:27:43 +0100 Subject: [PATCH 88/95] Add dialog to delete account in AccountTab --- .../app/compose/account/AccountTab.kt | 27 ++++++++++++++++++- .../app/compose/account/AccountViewModel.kt | 16 ++++++++--- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index 1d9404b8..dd58c8c3 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton @@ -34,6 +35,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R import com.readrops.app.compose.account.selection.AccountSelectionScreen import com.readrops.app.compose.util.components.SelectableIconText +import com.readrops.app.compose.util.components.TwoChoicesDialog import com.readrops.app.compose.util.theme.LargeSpacer import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.spacing @@ -61,6 +63,29 @@ object AccountTab : Tab { navigator.replaceAll(AccountSelectionScreen()) } + when (state.dialog) { + DialogState.DeleteAccount -> { + TwoChoicesDialog( + title = stringResource(R.string.delete_account), + text = stringResource(R.string.delete_account_question), + icon = rememberVectorPainter(image = Icons.Default.Delete), + confirmText = stringResource(R.string.delete), + dismissText = stringResource(R.string.cancel), + onDismiss = { viewModel.closeDialog() }, + onConfirm = { + viewModel.closeDialog() + viewModel.deleteAccount() + } + ) + } + + DialogState.NewAccount -> { + + } + + else -> {} + } + Scaffold( topBar = { TopAppBar( @@ -136,7 +161,7 @@ object AccountTab : Tab { padding = MaterialTheme.spacing.mediumSpacing, color = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error, - onClick = { /*viewModel.deleteAccount()*/ } + onClick = { viewModel.openDialog(DialogState.DeleteAccount) } ) } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt index 084afe1b..612c9cc6 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountViewModel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.launch class AccountViewModel( private val database: Database -) : TabViewModel(database) { +) : TabViewModel(database) { private val _closeHome = MutableStateFlow(false) val closeHome = _closeHome.asStateFlow() @@ -33,11 +33,14 @@ class AccountViewModel( } } + fun openDialog(dialog: DialogState) = _accountState.update { it.copy(dialog = dialog) } + + fun closeDialog() = _accountState.update { it.copy(dialog = null) } fun deleteAccount() { viewModelScope.launch(Dispatchers.IO) { database.newAccountDao() - .deleteAllAccounts() + .delete(currentAccount!!) _closeHome.update { true } } @@ -46,5 +49,10 @@ class AccountViewModel( data class AccountState( val account: Account = Account(accountName = "account", accountType = AccountType.LOCAL), - val dialog: Unit = Unit, -) \ No newline at end of file + val dialog: DialogState? = null, +) + +sealed interface DialogState { + object DeleteAccount : DialogState + object NewAccount : DialogState +} \ No newline at end of file From 7b537123c84fa04e17a2744fe7c1da28c08e4b31 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 19 Feb 2024 21:53:33 +0100 Subject: [PATCH 89/95] Add new account dialog in AccountTab --- .../app/compose/account/AccountTab.kt | 15 +- .../selection/AccountSelectionDialog.kt | 80 ++++++++++ .../readrops/app/compose/home/HomeScreen.kt | 108 +++++++------- .../com/readrops/app/compose/more/MoreTab.kt | 6 +- .../app/compose/util/components/IconText.kt | 138 ++++++++++++++---- .../db/entities/account/AccountType.kt | 2 +- 6 files changed, 262 insertions(+), 87 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt index dd58c8c3..e0a4a64d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/AccountTab.kt @@ -33,6 +33,8 @@ import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import com.readrops.app.compose.R +import com.readrops.app.compose.account.credentials.AccountCredentialsScreen +import com.readrops.app.compose.account.selection.AccountSelectionDialog import com.readrops.app.compose.account.selection.AccountSelectionScreen import com.readrops.app.compose.util.components.SelectableIconText import com.readrops.app.compose.util.components.TwoChoicesDialog @@ -80,7 +82,13 @@ object AccountTab : Tab { } DialogState.NewAccount -> { - + AccountSelectionDialog( + onDismiss = { viewModel.closeDialog() }, + onValidate = { accountType -> + viewModel.closeDialog() + navigator.push(AccountCredentialsScreen(accountType, state.account)) + } + ) } else -> {} @@ -104,7 +112,7 @@ object AccountTab : Tab { }, floatingActionButton = { FloatingActionButton( - onClick = {} + onClick = { viewModel.openDialog(DialogState.NewAccount) } ) { Icon( painter = painterResource(id = R.drawable.ic_add_account), @@ -142,6 +150,7 @@ object AccountTab : Tab { icon = painterResource(id = R.drawable.ic_add_account), text = stringResource(R.string.credentials), style = MaterialTheme.typography.titleMedium, + spacing = MaterialTheme.spacing.mediumSpacing, padding = MaterialTheme.spacing.mediumSpacing, onClick = { } ) @@ -150,6 +159,7 @@ object AccountTab : Tab { icon = painterResource(id = R.drawable.ic_notifications), text = stringResource(R.string.notifications), style = MaterialTheme.typography.titleMedium, + spacing = MaterialTheme.spacing.mediumSpacing, padding = MaterialTheme.spacing.mediumSpacing, onClick = { } ) @@ -158,6 +168,7 @@ object AccountTab : Tab { icon = rememberVectorPainter(image = Icons.Default.AccountCircle), text = stringResource(R.string.delete_account), style = MaterialTheme.typography.titleMedium, + spacing = MaterialTheme.spacing.mediumSpacing, padding = MaterialTheme.spacing.mediumSpacing, color = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error, diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt new file mode 100644 index 00000000..044851cf --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt @@ -0,0 +1,80 @@ +package com.readrops.app.compose.account.selection + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.readrops.app.compose.R +import com.readrops.app.compose.util.components.SelectableImageText +import com.readrops.app.compose.util.theme.MediumSpacer +import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.entities.account.AccountType + +@Composable +fun AccountSelectionDialog( + onDismiss: () -> Unit, + onValidate: (AccountType) -> Unit, +) { + Dialog( + onDismissRequest = onDismiss + ) { + Card( + shape = RoundedCornerShape(24.dp) + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(MaterialTheme.spacing.largeSpacing) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add_account), + contentDescription = null, + modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) + ) + + MediumSpacer() + + Text( + text = stringResource(R.string.new_account), + style = MaterialTheme.typography.headlineSmall + ) + + MediumSpacer() + + AccountType.values().forEach { type -> + SelectableImageText( + image = painterResource( + id = if (type != AccountType.LOCAL) + type.iconRes + else + R.drawable.ic_rss_feed_grey + ), + text = stringResource(id = type.typeName), + style = MaterialTheme.typography.titleMedium, + spacing = MaterialTheme.spacing.mediumSpacing, + padding = MaterialTheme.spacing.shortSpacing, + imageSize = 36.dp, + onClick = { onValidate(type) } + ) + } + } + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt index 08637a3c..d0e3bdbf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/home/HomeScreen.kt @@ -4,7 +4,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.BottomAppBar import androidx.compose.material3.Icon @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.androidx.AndroidScreen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -33,75 +34,74 @@ class HomeScreen : AndroidScreen() { val navigator = LocalNavigator.currentOrThrow TabNavigator( - tab = TimelineTab + tab = TimelineTab ) { tabNavigator -> CompositionLocalProvider(LocalNavigator provides navigator) { Scaffold( - bottomBar = { - BottomAppBar { - NavigationBarItem( - selected = tabNavigator.current.key == TimelineTab.key, - onClick = { tabNavigator.current = TimelineTab }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_timeline), - contentDescription = null - ) - }, - label = { Text("Timeline") } - ) + bottomBar = { + BottomAppBar { + NavigationBarItem( + selected = tabNavigator.current.key == TimelineTab.key, + onClick = { tabNavigator.current = TimelineTab }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_timeline), + contentDescription = null + ) + }, + label = { Text("Timeline") } + ) - NavigationBarItem( - selected = tabNavigator.current.key == FeedTab.key, - onClick = { tabNavigator.current = FeedTab }, - icon = { - Icon( - painter = painterResource(R.drawable.ic_rss_feed_grey), - contentDescription = null - ) - }, - label = { Text("Feeds") } - ) + NavigationBarItem( + selected = tabNavigator.current.key == FeedTab.key, + onClick = { tabNavigator.current = FeedTab }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_rss_feed_grey), + contentDescription = null + ) + }, + label = { Text(text = stringResource(R.string.feeds)) } + ) - NavigationBarItem( - selected = tabNavigator.current.key == AccountTab.key, - onClick = { tabNavigator.current = AccountTab }, - icon = { - Icon( - imageVector = Icons.Default.AccountBox, - contentDescription = null, - ) - }, - label = { Text("Account") } - ) + NavigationBarItem( + selected = tabNavigator.current.key == AccountTab.key, + onClick = { tabNavigator.current = AccountTab }, + icon = { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = null, + ) + }, + label = { Text(text = stringResource(R.string.account)) } + ) - NavigationBarItem( - selected = tabNavigator.current.key == MoreTab.key, - onClick = { tabNavigator.current = MoreTab }, - icon = { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = null, - ) - }, - label = { Text("More") } - ) - } - }, + NavigationBarItem( + selected = tabNavigator.current.key == MoreTab.key, + onClick = { tabNavigator.current = MoreTab }, + icon = { + Icon( + imageVector = Icons.Default.MoreVert, + contentDescription = null, + ) + }, + label = { Text("More") } + ) + } + }, ) { paddingValues -> Box( - modifier = Modifier.padding(paddingValues) + modifier = Modifier.padding(paddingValues) ) { CurrentTab() } BackHandler( - enabled = tabNavigator.current != TimelineTab, - onBack = { tabNavigator.current = TimelineTab } + enabled = tabNavigator.current != TimelineTab, + onBack = { tabNavigator.current = TimelineTab } ) } } - } } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt b/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt index 6990af89..8450dd28 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/more/MoreTab.kt @@ -66,7 +66,7 @@ object MoreTab : Tab { icon = painterResource(id = R.drawable.ic_settings), text = stringResource(R.string.settings), style = MaterialTheme.typography.titleMedium, - padding = MaterialTheme.spacing.mediumSpacing, + spacing = MaterialTheme.spacing.mediumSpacing, onClick = { } ) @@ -74,7 +74,7 @@ object MoreTab : Tab { icon = painterResource(id = R.drawable.ic_settings), text = "Backup", style = MaterialTheme.typography.titleMedium, - padding = MaterialTheme.spacing.mediumSpacing, + spacing = MaterialTheme.spacing.mediumSpacing, onClick = { } ) @@ -82,7 +82,7 @@ object MoreTab : Tab { icon = painterResource(id = R.drawable.ic_settings), text = "Open-source libraries", style = MaterialTheme.typography.titleMedium, - padding = MaterialTheme.spacing.mediumSpacing, + spacing = MaterialTheme.spacing.mediumSpacing, onClick = { } ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt index 720d43b4..ebf5d99b 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/IconText.kt @@ -1,5 +1,6 @@ package com.readrops.app.compose.util.components +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -23,28 +24,22 @@ import com.readrops.app.compose.util.theme.spacing import com.readrops.app.compose.util.toDp @Composable -fun IconText( - icon: Painter, +fun BaseText( text: String, style: TextStyle, modifier: Modifier = Modifier, color: Color = LocalContentColor.current, - tint: Color = LocalContentColor.current, - padding: Dp = MaterialTheme.spacing.veryShortSpacing, + spacing: Dp = MaterialTheme.spacing.veryShortSpacing, onClick: (() -> Unit)? = null, + leftContent: @Composable () -> Unit ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = if (onClick != null) modifier.clickable { onClick() } else modifier, ) { - Icon( - painter = icon, - tint = tint, - contentDescription = null, - modifier = Modifier.size(style.toDp()), - ) + leftContent() - Spacer(Modifier.width(padding)) + Spacer(Modifier.width(spacing)) Text( text = text, @@ -54,6 +49,63 @@ fun IconText( } } +@Composable +fun IconText( + icon: Painter, + text: String, + style: TextStyle, + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, + tint: Color = LocalContentColor.current, + spacing: Dp = MaterialTheme.spacing.veryShortSpacing, + onClick: (() -> Unit)? = null, +) { + BaseText( + text = text, + style = style, + color = color, + spacing = spacing, + modifier = modifier, + onClick = onClick + ) { + Icon( + painter = icon, + tint = tint, + contentDescription = null, + modifier = Modifier.size(style.toDp()), + ) + } +} + +@Composable +fun ImageText( + image: Painter, + text: String, + style: TextStyle, + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, + spacing: Dp = MaterialTheme.spacing.veryShortSpacing, + imageSize: Dp = style.toDp(), + onClick: (() -> Unit)? = null +) { + BaseText( + text = text, + style = style, + color = color, + spacing = spacing, + modifier = modifier, + onClick = onClick + ) { + Image( + painter = image, + contentDescription = null, + modifier = Modifier.size(imageSize), + ) + } +} + + + @Composable fun SelectableIconText( icon: Painter, @@ -63,27 +115,59 @@ fun SelectableIconText( modifier: Modifier = Modifier, color: Color = LocalContentColor.current, tint: Color = LocalContentColor.current, - padding: Dp = MaterialTheme.spacing.veryShortSpacing, + spacing: Dp = MaterialTheme.spacing.veryShortSpacing, + padding: Dp = MaterialTheme.spacing.shortSpacing ) { Box( - modifier = modifier.clickable { onClick() } + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(padding) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = MaterialTheme.spacing.mediumSpacing, - vertical = MaterialTheme.spacing.mediumSpacing - ) + BaseText( + text = text, + style = style, + color = color, + spacing = spacing ) { - IconText( - icon = icon, - text = text, - style = style, - padding = padding, + Icon( + painter = icon, tint = tint, - color = color + contentDescription = null, + modifier = Modifier.size(style.toDp()), + ) + } + } +} + +@Composable +fun SelectableImageText( + image: Painter, + text: String, + style: TextStyle, + onClick: () -> Unit, + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current, + spacing: Dp = MaterialTheme.spacing.veryShortSpacing, + padding: Dp = MaterialTheme.spacing.shortSpacing, + imageSize: Dp = style.toDp() +) { + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(padding) + ) { + BaseText( + text = text, + style = style, + color = color, + spacing = spacing + ) { + Image( + painter = image, + contentDescription = null, + modifier = Modifier.size(imageSize), ) } } diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountType.kt b/db/src/main/java/com/readrops/db/entities/account/AccountType.kt index edb235d1..37f130e0 100644 --- a/db/src/main/java/com/readrops/db/entities/account/AccountType.kt +++ b/db/src/main/java/com/readrops/db/entities/account/AccountType.kt @@ -12,6 +12,6 @@ enum class AccountType(@DrawableRes val iconRes: Int, val accountConfig: AccountConfig?) : Parcelable { LOCAL(R.mipmap.ic_launcher, R.string.local_account, AccountConfig.LOCAL), NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTCLOUD_NEWS), - FEEDLY(R.drawable.ic_feedly, R.string.feedly, null), + /* FEEDLY(R.drawable.ic_feedly, R.string.feedly, null),*/ FRESHRSS(R.drawable.ic_freshrss, R.string.freshrss, AccountConfig.FRESHRSS); } \ No newline at end of file From 5a0affdf3b188736ee372b57cd9820cae794d87d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 19 Feb 2024 22:43:53 +0100 Subject: [PATCH 90/95] Improve the use of BaseDialog --- .../selection/AccountSelectionDialog.kt | 79 ++----- .../readrops/app/compose/feeds/FeedState.kt | 2 + .../compose/feeds/dialogs/AddFeedDialog.kt | 181 ++++++--------- .../app/compose/feeds/dialogs/FolderDialog.kt | 13 +- .../compose/feeds/dialogs/UpdateFeedDialog.kt | 207 +++++++----------- .../app/compose/util/components/BaseDialog.kt | 11 - 6 files changed, 189 insertions(+), 304 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt index 044851cf..e75c66a5 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/account/selection/AccountSelectionDialog.kt @@ -1,26 +1,13 @@ package com.readrops.app.compose.account.selection -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import com.readrops.app.compose.R +import com.readrops.app.compose.util.components.BaseDialog import com.readrops.app.compose.util.components.SelectableImageText -import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.spacing import com.readrops.db.entities.account.AccountType @@ -29,52 +16,26 @@ fun AccountSelectionDialog( onDismiss: () -> Unit, onValidate: (AccountType) -> Unit, ) { - Dialog( - onDismissRequest = onDismiss + BaseDialog( + title = stringResource(R.string.new_account), + icon = painterResource(id = R.drawable.ic_add_account), + onDismiss = onDismiss ) { - Card( - shape = RoundedCornerShape(24.dp) - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(MaterialTheme.spacing.largeSpacing) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_add_account), - contentDescription = null, - modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) - ) - - MediumSpacer() - - Text( - text = stringResource(R.string.new_account), - style = MaterialTheme.typography.headlineSmall - ) - - MediumSpacer() - - AccountType.values().forEach { type -> - SelectableImageText( - image = painterResource( - id = if (type != AccountType.LOCAL) - type.iconRes - else - R.drawable.ic_rss_feed_grey - ), - text = stringResource(id = type.typeName), - style = MaterialTheme.typography.titleMedium, - spacing = MaterialTheme.spacing.mediumSpacing, - padding = MaterialTheme.spacing.shortSpacing, - imageSize = 36.dp, - onClick = { onValidate(type) } - ) - } - } + AccountType.values().forEach { type -> + SelectableImageText( + image = painterResource( + id = if (type != AccountType.LOCAL) + type.iconRes + else + R.drawable.ic_rss_feed_grey + ), + text = stringResource(id = type.typeName), + style = MaterialTheme.typography.titleMedium, + spacing = MaterialTheme.spacing.mediumSpacing, + padding = MaterialTheme.spacing.shortSpacing, + imageSize = 36.dp, + onClick = { onValidate(type) } + ) } } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt index f969d32c..5488dfa9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/FeedState.kt @@ -56,6 +56,8 @@ data class UpdateFeedDialogState( val isFeedUrlReadOnly: Boolean get() = accountType != null && !accountType.accountConfig!!.isFeedUrlEditable + + val hasFolders = folders.isNotEmpty() } data class FolderState( diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt index 08fb00cf..c8e3ee79 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/AddFeedDialog.kt @@ -1,22 +1,13 @@ package com.readrops.app.compose.feeds.dialogs -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Card import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -25,19 +16,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.readrops.app.compose.R import com.readrops.app.compose.feeds.FeedViewModel +import com.readrops.app.compose.util.components.BaseDialog import com.readrops.app.compose.util.theme.LargeSpacer -import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.ShortSpacer -import com.readrops.app.compose.util.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -49,96 +36,50 @@ fun AddFeedDialog( var isExpanded by remember { mutableStateOf(false) } - Dialog( - onDismissRequest = onDismiss + BaseDialog( + title = stringResource(R.string.add_feed_item), + icon = painterResource(id = R.drawable.ic_rss_feed_grey), + onDismiss = onDismiss ) { - Card( - shape = RoundedCornerShape(24.dp), - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(MaterialTheme.spacing.largeSpacing) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_rss_feed_grey), - contentDescription = null, - modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) - ) - - MediumSpacer() - - Text( - text = stringResource(R.string.add_feed_item), - style = MaterialTheme.typography.headlineSmall - ) - - MediumSpacer() - - OutlinedTextField( - value = state.url, - label = { - Text(text = "URL") - }, - onValueChange = { viewModel.setAddFeedDialogURL(it) }, - singleLine = true, - trailingIcon = { - if (state.url.isNotEmpty()) { - IconButton( - onClick = { viewModel.setAddFeedDialogURL("") } - ) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = null - ) - } - } - }, - isError = state.isError, - supportingText = { Text(state.error?.errorText().orEmpty()) } - ) - - ShortSpacer() - - ExposedDropdownMenuBox( - expanded = isExpanded, - onExpandedChange = { isExpanded = isExpanded.not() } - ) { - ExposedDropdownMenu( - expanded = isExpanded, - onDismissRequest = { isExpanded = false } + OutlinedTextField( + value = state.url, + label = { + Text(text = "URL") + }, + onValueChange = { viewModel.setAddFeedDialogURL(it) }, + singleLine = true, + trailingIcon = { + if (state.url.isNotEmpty()) { + IconButton( + onClick = { viewModel.setAddFeedDialogURL("") } ) { - for (account in state.accounts) { - DropdownMenuItem( - text = { Text(text = account.accountName!!) }, - onClick = { - isExpanded = false - viewModel.setAddFeedDialogSelectedAccount(account) - }, - leadingIcon = { - Icon( - painter = painterResource( - id = if (state.selectedAccount.isLocal) { - R.drawable.ic_rss_feed_grey - } else - state.selectedAccount.accountType!!.iconRes - ), - contentDescription = null - ) - } - ) - } + Icon( + imageVector = Icons.Default.Clear, + contentDescription = null + ) } + } + }, + isError = state.isError, + supportingText = { Text(state.error?.errorText().orEmpty()) } + ) - OutlinedTextField( - value = state.selectedAccount.accountName!!, - readOnly = true, - onValueChange = {}, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) + ShortSpacer() + + ExposedDropdownMenuBox( + expanded = isExpanded, + onExpandedChange = { isExpanded = isExpanded.not() } + ) { + ExposedDropdownMenu( + expanded = isExpanded, + onDismissRequest = { isExpanded = false } + ) { + for (account in state.accounts) { + DropdownMenuItem( + text = { Text(text = account.accountName!!) }, + onClick = { + isExpanded = false + viewModel.setAddFeedDialogSelectedAccount(account) }, leadingIcon = { Icon( @@ -150,19 +91,39 @@ fun AddFeedDialog( ), contentDescription = null ) - }, - modifier = Modifier.menuAnchor() + } ) } - - LargeSpacer() - - TextButton( - onClick = { viewModel.addFeedDialogValidate() }, - ) { - Text(text = stringResource(R.string.validate)) - } } + + OutlinedTextField( + value = state.selectedAccount.accountName!!, + readOnly = true, + onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) + }, + leadingIcon = { + Icon( + painter = painterResource( + id = if (state.selectedAccount.isLocal) { + R.drawable.ic_rss_feed_grey + } else + state.selectedAccount.accountType!!.iconRes + ), + contentDescription = null + ) + }, + modifier = Modifier.menuAnchor() + ) + } + + LargeSpacer() + + TextButton( + onClick = { viewModel.addFeedDialogValidate() }, + ) { + Text(text = stringResource(R.string.validate)) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt index c8d1bdcf..c8c23382 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/FolderDialog.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.res.painterResource @@ -14,6 +15,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.readrops.app.compose.R import com.readrops.app.compose.feeds.FeedViewModel import com.readrops.app.compose.util.components.BaseDialog +import com.readrops.app.compose.util.theme.LargeSpacer @Composable fun FolderDialog( @@ -27,8 +29,7 @@ fun FolderDialog( BaseDialog( title = stringResource(id = if (updateFolder) R.string.edit_folder else R.string.add_folder), icon = painterResource(id = if (updateFolder) R.drawable.ic_folder_grey else R.drawable.ic_new_folder), - onDismiss = onDismiss, - onValidate = onValidate + onDismiss = onDismiss ) { OutlinedTextField( value = state.name.orEmpty(), @@ -52,5 +53,13 @@ fun FolderDialog( isError = state.isError, supportingText = { Text(text = state.nameError?.errorText().orEmpty()) } ) + + LargeSpacer() + + TextButton( + onClick = { onValidate() }, + ) { + Text(text = stringResource(R.string.validate)) + } } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt index d24b5015..e1465442 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/feeds/dialogs/UpdateFeedDialog.kt @@ -1,36 +1,24 @@ package com.readrops.app.compose.feeds.dialogs -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.readrops.app.compose.R import com.readrops.app.compose.feeds.FeedViewModel +import com.readrops.app.compose.util.components.BaseDialog import com.readrops.app.compose.util.theme.LargeSpacer import com.readrops.app.compose.util.theme.MediumSpacer -import com.readrops.app.compose.util.theme.spacing @OptIn(ExperimentalMaterial3Api::class) @@ -41,122 +29,97 @@ fun UpdateFeedDialog( ) { val state by viewModel.updateFeedDialogState.collectAsStateWithLifecycle() - Dialog( - onDismissRequest = onDismissRequest + BaseDialog( + title = stringResource(R.string.edit_feed), + icon = painterResource(id = R.drawable.ic_rss_feed_grey), + onDismiss = onDismissRequest ) { - Card( - shape = RoundedCornerShape(24.dp) - ) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(MaterialTheme.spacing.largeSpacing) - ) { - Icon( - painter = painterResource(id = R.drawable.ic_rss_feed_grey), - contentDescription = null, - modifier = Modifier.size(MaterialTheme.spacing.largeSpacing) - ) - - MediumSpacer() - - Text( - text = stringResource(R.string.edit_feed), - style = MaterialTheme.typography.headlineSmall - ) - - MediumSpacer() - - OutlinedTextField( - value = state.feedName, - onValueChange = { viewModel.setUpdateFeedDialogStateFeedName(it) }, - label = { Text(text = stringResource(R.string.feed_name)) }, - singleLine = true, - isError = state.isFeedNameError, - supportingText = { - if (state.isFeedNameError) { - Text( - text = state.feedNameError?.errorText().orEmpty() - ) - } - } - ) - - MediumSpacer() - - OutlinedTextField( - value = state.feedUrl, - onValueChange = { viewModel.setUpdateFeedDialogFeedUrl(it) }, - label = { Text(text = stringResource(R.string.feed_url)) }, - singleLine = true, - readOnly = state.isFeedUrlReadOnly, - isError = state.isFeedUrlError, - supportingText = { - if (state.isFeedUrlError) { - Text( - text = state.feedUrlError?.errorText().orEmpty() - ) - } - } - ) - - MediumSpacer() - - ExposedDropdownMenuBox( - expanded = state.isAccountDropDownExpanded, - onExpandedChange = { viewModel.setAccountDropDownState(state.isAccountDropDownExpanded.not()) } - ) { - ExposedDropdownMenu( - expanded = state.isAccountDropDownExpanded, - onDismissRequest = { viewModel.setAccountDropDownState(false) } - ) { - for (folder in state.folders) { - DropdownMenuItem( - text = { Text(text = folder.name!!) }, - onClick = { - viewModel.setSelectedFolder(folder) - viewModel.setAccountDropDownState(false) - }, - leadingIcon = { - Icon( - painterResource(id = R.drawable.ic_folder_grey), - contentDescription = null, - ) - } - ) - } - } - - OutlinedTextField( - value = state.selectedFolder?.name.orEmpty(), - readOnly = true, - onValueChange = {}, - trailingIcon = { - ExposedDropdownMenuDefaults.TrailingIcon(expanded = state.isAccountDropDownExpanded) - }, - leadingIcon = { - if (state.selectedFolder != null) { - Icon( - painterResource(id = R.drawable.ic_folder_grey), - contentDescription = null, - ) - } - }, - modifier = Modifier.menuAnchor() + OutlinedTextField( + value = state.feedName, + onValueChange = { viewModel.setUpdateFeedDialogStateFeedName(it) }, + label = { Text(text = stringResource(R.string.feed_name)) }, + singleLine = true, + isError = state.isFeedNameError, + supportingText = { + if (state.isFeedNameError) { + Text( + text = state.feedNameError?.errorText().orEmpty() ) } + } + ) - LargeSpacer() + MediumSpacer() - TextButton( - onClick = { viewModel.updateFeedDialogValidate() }, - ) { - Text(text = stringResource(R.string.validate)) + OutlinedTextField( + value = state.feedUrl, + onValueChange = { viewModel.setUpdateFeedDialogFeedUrl(it) }, + label = { Text(text = stringResource(R.string.feed_url)) }, + singleLine = true, + readOnly = state.isFeedUrlReadOnly, + isError = state.isFeedUrlError, + supportingText = { + if (state.isFeedUrlError) { + Text( + text = state.feedUrlError?.errorText().orEmpty() + ) } } + ) + + MediumSpacer() + + ExposedDropdownMenuBox( + expanded = state.isAccountDropDownExpanded && state.hasFolders, + onExpandedChange = { viewModel.setAccountDropDownState(state.isAccountDropDownExpanded.not()) } + ) { + ExposedDropdownMenu( + expanded = state.isAccountDropDownExpanded && state.hasFolders, + onDismissRequest = { viewModel.setAccountDropDownState(false) } + ) { + for (folder in state.folders) { + DropdownMenuItem( + text = { Text(text = folder.name!!) }, + onClick = { + viewModel.setSelectedFolder(folder) + viewModel.setAccountDropDownState(false) + }, + leadingIcon = { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null, + ) + } + ) + } + } + + OutlinedTextField( + value = state.selectedFolder?.name.orEmpty(), + readOnly = true, + enabled = state.hasFolders, + onValueChange = {}, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = state.isAccountDropDownExpanded) + }, + leadingIcon = { + if (state.selectedFolder != null) { + Icon( + painterResource(id = R.drawable.ic_folder_grey), + contentDescription = null, + ) + } + }, + modifier = Modifier.menuAnchor() + ) + } + + LargeSpacer() + + TextButton( + onClick = { viewModel.updateFeedDialogValidate() }, + ) { + Text(text = stringResource(R.string.validate)) } } } diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt index 29018ef4..29132676 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/BaseDialog.kt @@ -11,14 +11,12 @@ import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import com.readrops.app.compose.util.theme.LargeSpacer import com.readrops.app.compose.util.theme.MediumSpacer import com.readrops.app.compose.util.theme.spacing @@ -27,7 +25,6 @@ fun BaseDialog( title: String, icon: Painter, onDismiss: () -> Unit, - onValidate: () -> Unit, content: @Composable () -> Unit ) { Dialog( @@ -60,14 +57,6 @@ fun BaseDialog( MediumSpacer() content() - - LargeSpacer() - - TextButton( - onClick = { onValidate() }, - ) { - Text(text = "Validate") - } } } } From 40accbc8b44c1b6fb11043a8d8afa61e14c1cb8f Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 19 Feb 2024 22:59:19 +0100 Subject: [PATCH 91/95] Fix no feed placeholder not appearing in FeedTab --- .../repositories/GetFoldersWithFeeds.kt | 22 ++++++++++++++----- .../compose/util/components/Placeholder.kt | 5 ++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index e140879d..73f76e8a 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -18,7 +18,13 @@ class GetFoldersWithFeeds( .selectFeedsWithoutFolder(accountId) ) { folders, feedsWithoutFolder -> val foldersWithFeeds = folders.groupBy( - keySelector = { Folder(id = it.folderId, name = it.folderName, accountId = it.accountId) }, + keySelector = { + Folder( + id = it.folderId, + name = it.folderName, + accountId = it.accountId + ) as Folder? + }, valueTransform = { Feed( id = it.feedId, @@ -37,11 +43,15 @@ class GetFoldersWithFeeds( } } - foldersWithFeeds + mapOf( - Pair( - null, - feedsWithoutFolder.map { it.feed.apply { unreadCount = it.unreadCount } }) - ) + if (feedsWithoutFolder.isNotEmpty()) { + foldersWithFeeds + mapOf( + Pair( + null, + feedsWithoutFolder.map { it.feed.apply { unreadCount = it.unreadCount } }) + ) + } + + foldersWithFeeds } } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt b/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt index 38b6d933..216bd217 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/util/components/Placeholder.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter +import com.readrops.app.compose.util.theme.ShortSpacer import com.readrops.app.compose.util.toDp @@ -39,9 +40,11 @@ fun Placeholder( modifier = Modifier.size(MaterialTheme.typography.displayMedium.toDp() * 1.5f) ) + ShortSpacer() + Text( text = text, - style = MaterialTheme.typography.displayMedium + style = MaterialTheme.typography.displaySmall ) } } \ No newline at end of file From edc2094a6e43f66a221d0e54bb45117bd2e873ca Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Mon, 19 Feb 2024 23:16:06 +0100 Subject: [PATCH 92/95] Fix GetFoldersWithFeeds not sending feeds without folder --- .../readrops/app/compose/repositories/GetFoldersWithFeeds.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt index 73f76e8a..5b39ccfa 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/GetFoldersWithFeeds.kt @@ -49,9 +49,9 @@ class GetFoldersWithFeeds( null, feedsWithoutFolder.map { it.feed.apply { unreadCount = it.unreadCount } }) ) + } else { + foldersWithFeeds } - - foldersWithFeeds } } } \ No newline at end of file From 09bcba7c5ad32c6a0694d8c330c1bec5e070e5ab Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 21 Feb 2024 21:53:11 +0100 Subject: [PATCH 93/95] Change top bar title depending on current filter in TimelineTab --- .../readrops/app/compose/timelime/TimelineTab.kt | 14 +++++++++++++- .../app/compose/timelime/TimelineViewModel.kt | 12 ++++++++---- .../compose/timelime/drawer/DrawerFolderItem.kt | 4 ++-- .../app/compose/timelime/drawer/TimelineDrawer.kt | 10 ++++++---- appcompose/src/main/res/values/strings.xml | 1 + 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index f1307067..0fcb7339 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -43,6 +43,7 @@ import com.readrops.app.compose.item.ItemScreen import com.readrops.app.compose.timelime.drawer.TimelineDrawer import com.readrops.app.compose.util.components.CenteredColumn import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.filters.FilterType import org.koin.androidx.compose.getViewModel @@ -115,7 +116,18 @@ object TimelineTab : Tab { Scaffold( topBar = { TopAppBar( - title = { Text(text = stringResource(R.string.articles)) }, + title = { + Text( + text = when (state.filters.filterType) { + FilterType.FEED_FILTER -> state.filterFeedName + FilterType.FOLDER_FILER -> state.filterFolderName + FilterType.READ_IT_LATER_FILTER -> stringResource(R.string.read_later) + FilterType.STARS_FILTER -> stringResource(R.string.favorites) + FilterType.NO_FILTER -> stringResource(R.string.articles) + FilterType.NEW -> stringResource(R.string.new_articles) + } + ) + }, navigationIcon = { IconButton( onClick = { viewModel.openDrawer() } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index 4d4b06c6..b6a220f2 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -115,31 +115,33 @@ class TimelineViewModel( } } - fun updateDrawerFolderSelection(folderId: Int) { + fun updateDrawerFolderSelection(folder: Folder) { _timelineState.update { it.copy( filters = updateFilters { it.filters.copy( filterType = FilterType.FOLDER_FILER, - filterFolderId = folderId, + filterFolderId = folder.id, filterFeedId = 0 ) }, + filterFolderName = folder.name!!, isDrawerOpen = false ) } } - fun updateDrawerFeedSelection(feedId: Int) { + fun updateDrawerFeedSelection(feed: Feed) { _timelineState.update { it.copy( filters = updateFilters { it.filters.copy( filterType = FilterType.FEED_FILTER, - filterFeedId = feedId, + filterFeedId = feed.id, filterFolderId = 0 ) }, + filterFeedName = feed.name!!, isDrawerOpen = false ) } @@ -189,6 +191,8 @@ data class TimelineState( val isDrawerOpen: Boolean = false, val endSynchronizing: Boolean = false, val filters: QueryFilters = QueryFilters(), + val filterFeedName: String = "", + val filterFolderName: String = "", val foldersAndFeeds: Map> = emptyMap(), val itemState: Flow> = emptyFlow() ) diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt index ec4f101e..cb0f2926 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/DrawerFolderItem.kt @@ -46,7 +46,7 @@ fun DrawerFolderItem( onClick: () -> Unit, feeds: List, selectedFeed: Int, - onFeedClick: (Int) -> Unit, + onFeedClick: (Feed) -> Unit, modifier: Modifier = Modifier, ) { val colors = NavigationDrawerItemDefaults.colors() @@ -131,7 +131,7 @@ fun DrawerFolderItem( }, badge = { Text(feed.unreadCount.toString()) }, selected = feed.id == selectedFeed, - onClick = { onFeedClick(feed.id) }, + onClick = { onFeedClick(feed) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt index a534dcb6..bb2c14e9 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/drawer/TimelineDrawer.kt @@ -26,14 +26,16 @@ import coil.compose.AsyncImage import com.readrops.app.compose.R import com.readrops.app.compose.timelime.TimelineState import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.entities.Feed +import com.readrops.db.entities.Folder import com.readrops.db.filters.FilterType @Composable fun TimelineDrawer( state: TimelineState, onClickDefaultItem: (FilterType) -> Unit, - onFolderClick: (Int) -> Unit, - onFeedClick: (Int) -> Unit, + onFolderClick: (Folder) -> Unit, + onFeedClick: (Feed) -> Unit, ) { val scrollState = rememberScrollState() @@ -74,7 +76,7 @@ fun TimelineDrawer( Text(folderEntry.value.sumOf { it.unreadCount }.toString()) }, selected = state.filters.filterFolderId == folder.id, - onClick = { onFolderClick(folder.id) }, + onClick = { onFolderClick(folder) }, feeds = folderEntry.value, selectedFeed = state.filters.filterFeedId, onFeedClick = { onFeedClick(it) }, @@ -102,7 +104,7 @@ fun TimelineDrawer( }, badge = { Text(feed.unreadCount.toString()) }, selected = feed.id == state.filters.filterFeedId, - onClick = { onFeedClick(feed.id) }, + onClick = { onFeedClick(feed) }, modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) ) } diff --git a/appcompose/src/main/res/values/strings.xml b/appcompose/src/main/res/values/strings.xml index 07c22295..272ac80c 100644 --- a/appcompose/src/main/res/values/strings.xml +++ b/appcompose/src/main/res/values/strings.xml @@ -146,4 +146,5 @@ system Hide feeds without new items Mark items read on scroll + New articles \ No newline at end of file From adb56cdacf4de79d976bc28ddbcc9020a0c3f053 Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 21 Feb 2024 23:59:01 +0100 Subject: [PATCH 94/95] Add mark all read items action for some filters in TimelineTab --- .../app/compose/repositories/ARepository.kt | 16 +++++++++ .../app/compose/timelime/TimelineTab.kt | 26 +++++++++++++- .../app/compose/timelime/TimelineViewModel.kt | 36 ++++++++++++++++++- .../com/readrops/db/dao/newdao/NewItemDao.kt | 14 ++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) diff --git a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt index 72e8c299..8b133d8d 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/repositories/ARepository.kt @@ -64,4 +64,20 @@ abstract class BaseRepository( open suspend fun setItemStarState(item: Item) { database.newItemDao().updateStarState(item.id, item.isStarred) } + + open suspend fun setAllItemsRead(accountId: Int) { + database.newItemDao().setAllItemsRead(accountId) + } + + open suspend fun setAllStarredItemsRead(accountId: Int) { + database.newItemDao().setAllStarredItemsRead(accountId) + } + + open suspend fun setAllItemsReadByFeed(feedId: Int, accountId: Int) { + database.newItemDao().setAllItemsReadByFeed(feedId, accountId) + } + + open suspend fun setAllItemsReadByFolder(folderId: Int, accountId: Int) { + database.newItemDao().setAllItemsReadByFolder(folderId, accountId) + } } \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index 0fcb7339..ea421bf0 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -42,6 +42,7 @@ import com.readrops.app.compose.R import com.readrops.app.compose.item.ItemScreen import com.readrops.app.compose.timelime.drawer.TimelineDrawer import com.readrops.app.compose.util.components.CenteredColumn +import com.readrops.app.compose.util.components.TwoChoicesDialog import com.readrops.app.compose.util.theme.spacing import com.readrops.db.filters.FilterType import org.koin.androidx.compose.getViewModel @@ -96,6 +97,21 @@ object TimelineTab : Tab { } } + if (state.confirmDialog) { + TwoChoicesDialog( + title = "Mark all items as read", + text = "Do you really want to mark all items as read?", + icon = painterResource(id = R.drawable.ic_rss_feed_grey), + confirmText = "Validate", + dismissText = "Cancel", + onDismiss = { viewModel.closeConfirmDialog() }, + onConfirm = { + viewModel.closeConfirmDialog() + viewModel.setAllItemsRead() + } + ) + } + ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -158,7 +174,15 @@ object TimelineTab : Tab { ) }, floatingActionButton = { - FloatingActionButton(onClick = { }) { + FloatingActionButton( + onClick = { + if (state.filters.filterType == FilterType.NO_FILTER) { + viewModel.openConfirmDialog() + } else { + viewModel.setAllItemsRead() + } + } + ) { Icon( painter = painterResource(id = R.drawable.ic_done_all), contentDescription = null diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index b6a220f2..e0433b6a 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -183,6 +183,39 @@ class TimelineViewModel( context.startActivity(Intent.createChooser(it, null)) } } + + fun setAllItemsRead() { + viewModelScope.launch(dispatcher) { + when (_timelineState.value.filters.filterType) { + FilterType.FEED_FILTER -> + repository?.setAllItemsReadByFeed( + _timelineState.value.filters.filterFeedId, + currentAccount!!.id + ) + + FilterType.FOLDER_FILER -> repository?.setAllItemsReadByFolder( + _timelineState.value.filters.filterFolderId, + currentAccount!!.id + ) + FilterType.READ_IT_LATER_FILTER -> TODO() + FilterType.STARS_FILTER -> repository?.setAllStarredItemsRead(currentAccount!!.id) + FilterType.NO_FILTER -> repository?.setAllItemsRead(currentAccount!!.id) + FilterType.NEW -> TODO() + } + } + } + + fun openConfirmDialog() { + _timelineState.value = _timelineState.value.copy( + confirmDialog = true + ) + } + + fun closeConfirmDialog() { + _timelineState.value = _timelineState.value.copy( + confirmDialog = false + ) + } } @Immutable @@ -194,5 +227,6 @@ data class TimelineState( val filterFeedName: String = "", val filterFolderName: String = "", val foldersAndFeeds: Map> = emptyMap(), - val itemState: Flow> = emptyFlow() + val itemState: Flow> = emptyFlow(), + val confirmDialog: Boolean = false ) diff --git a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt index ed9aaa05..ad83af9e 100644 --- a/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt +++ b/db/src/main/java/com/readrops/db/dao/newdao/NewItemDao.kt @@ -22,4 +22,18 @@ abstract class NewItemDao : NewBaseDao { @Query("Update Item Set starred = :starred Where id = :itemId") abstract suspend fun updateStarState(itemId: Int, starred: Boolean) + + @Query("Update Item set read = 1 Where feed_id IN (Select id From Feed Where account_id = :accountId)") + abstract suspend fun setAllItemsRead(accountId: Int) + + @Query("Update Item set read = 1 Where starred = 1 And feed_id IN (Select id From Feed Where account_id = :accountId)") + abstract suspend fun setAllStarredItemsRead(accountId: Int) + + @Query("Update Item set read = 1 Where feed_id IN " + + "(Select id From Feed Where id = :feedId And account_id = :accountId)") + abstract suspend fun setAllItemsReadByFeed(feedId: Int, accountId: Int) + + @Query("Update Item set read = 1 Where feed_id IN (Select Feed.id From Feed Inner Join Folder " + + "On Feed.folder_id = Folder.id Where Folder.id = :folderId And Folder.account_id = :accountId)") + abstract suspend fun setAllItemsReadByFolder(folderId: Int, accountId: Int) } \ No newline at end of file From 1cdb98eedb38326d61fb154bd9a71a4493bc9e4d Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Thu, 22 Feb 2024 21:44:03 +0100 Subject: [PATCH 95/95] Add some filters in TimelineTab --- .../app/compose/timelime/FilterBottomSheet.kt | 93 +++++++++++++++++++ .../app/compose/timelime/TimelineTab.kt | 46 ++++++--- .../app/compose/timelime/TimelineViewModel.kt | 41 ++++++-- appcompose/src/main/res/values-fr/strings.xml | 2 + appcompose/src/main/res/values/strings.xml | 1 + 5 files changed, 159 insertions(+), 24 deletions(-) create mode 100644 appcompose/src/main/java/com/readrops/app/compose/timelime/FilterBottomSheet.kt diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/FilterBottomSheet.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/FilterBottomSheet.kt new file mode 100644 index 00000000..357bdcf9 --- /dev/null +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/FilterBottomSheet.kt @@ -0,0 +1,93 @@ +package com.readrops.app.compose.timelime + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.readrops.app.compose.R +import com.readrops.app.compose.util.theme.LargeSpacer +import com.readrops.app.compose.util.theme.ShortSpacer +import com.readrops.app.compose.util.theme.spacing +import com.readrops.db.filters.ListSortType +import com.readrops.db.queries.QueryFilters + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun FilterBottomSheet( + viewModel: TimelineViewModel, + filters: QueryFilters, + onDismiss: () -> Unit, +) { + ModalBottomSheet( + onDismissRequest = onDismiss + ) { + Column( + modifier = Modifier.padding(MaterialTheme.spacing.mediumSpacing) + ) { + Text( + text = stringResource(R.string.filters) + ) + + ShortSpacer() + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { viewModel.setShowReadItemsState(!filters.showReadItems) } + ) { + Checkbox( + checked = filters.showReadItems, + onCheckedChange = { viewModel.setShowReadItemsState(!filters.showReadItems) } + ) + + ShortSpacer() + + Text( + text = stringResource(R.string.show_read_articles) + ) + } + + ShortSpacer() + + fun setSortTypeState() { + viewModel.setSortTypeState( + if (filters.sortType == ListSortType.NEWEST_TO_OLDEST) + ListSortType.OLDEST_TO_NEWEST + else + ListSortType.NEWEST_TO_OLDEST + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { setSortTypeState() } + ) { + Checkbox( + checked = filters.sortType == ListSortType.OLDEST_TO_NEWEST, + onCheckedChange = { setSortTypeState() } + ) + + ShortSpacer() + + Text( + text = "Show oldest items first" + ) + } + + LargeSpacer() + } + } +} \ No newline at end of file diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt index ea421bf0..faf7febf 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineTab.kt @@ -97,19 +97,33 @@ object TimelineTab : Tab { } } - if (state.confirmDialog) { - TwoChoicesDialog( - title = "Mark all items as read", - text = "Do you really want to mark all items as read?", - icon = painterResource(id = R.drawable.ic_rss_feed_grey), - confirmText = "Validate", - dismissText = "Cancel", - onDismiss = { viewModel.closeConfirmDialog() }, - onConfirm = { - viewModel.closeConfirmDialog() - viewModel.setAllItemsRead() - } - ) + when (state.dialog) { + DialogState.ConfirmDialog -> { + TwoChoicesDialog( + title = "Mark all items as read", + text = "Do you really want to mark all items as read?", + icon = painterResource(id = R.drawable.ic_rss_feed_grey), + confirmText = "Validate", + dismissText = "Cancel", + onDismiss = { viewModel.closeDialog() }, + onConfirm = { + viewModel.closeDialog() + viewModel.setAllItemsRead() + } + ) + } + + DialogState.FilterSheet -> { + FilterBottomSheet( + viewModel = viewModel, + filters = state.filters, + onDismiss = { + viewModel.closeDialog() + } + ) + } + + null -> {} } ModalNavigationDrawer( @@ -155,7 +169,9 @@ object TimelineTab : Tab { } }, actions = { - IconButton(onClick = { }) { + IconButton( + onClick = { viewModel.openDialog(DialogState.FilterSheet) } + ) { Icon( painter = painterResource(id = R.drawable.ic_filter_list), contentDescription = null @@ -177,7 +193,7 @@ object TimelineTab : Tab { FloatingActionButton( onClick = { if (state.filters.filterType == FilterType.NO_FILTER) { - viewModel.openConfirmDialog() + viewModel.openDialog(DialogState.ConfirmDialog) } else { viewModel.setAllItemsRead() } diff --git a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt index e0433b6a..b9945429 100644 --- a/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt +++ b/appcompose/src/main/java/com/readrops/app/compose/timelime/TimelineViewModel.kt @@ -15,6 +15,7 @@ import com.readrops.db.entities.Feed import com.readrops.db.entities.Folder import com.readrops.db.entities.Item import com.readrops.db.filters.FilterType +import com.readrops.db.filters.ListSortType import com.readrops.db.pojo.ItemWithFeed import com.readrops.db.queries.ItemsQueryBuilder import com.readrops.db.queries.QueryFilters @@ -197,6 +198,7 @@ class TimelineViewModel( _timelineState.value.filters.filterFolderId, currentAccount!!.id ) + FilterType.READ_IT_LATER_FILTER -> TODO() FilterType.STARS_FILTER -> repository?.setAllStarredItemsRead(currentAccount!!.id) FilterType.NO_FILTER -> repository?.setAllItemsRead(currentAccount!!.id) @@ -205,16 +207,32 @@ class TimelineViewModel( } } - fun openConfirmDialog() { - _timelineState.value = _timelineState.value.copy( - confirmDialog = true - ) + fun openDialog(dialog: DialogState) = _timelineState.update { it.copy(dialog = dialog) } + + fun closeDialog() = _timelineState.update { it.copy(dialog = null) } + + fun setShowReadItemsState(showReadItems: Boolean) { + _timelineState.update { + it.copy( + filters = updateFilters { + it.filters.copy( + showReadItems = showReadItems + ) + } + ) + } } - fun closeConfirmDialog() { - _timelineState.value = _timelineState.value.copy( - confirmDialog = false - ) + fun setSortTypeState(sortType: ListSortType) { + _timelineState.update { + it.copy( + filters = updateFilters { + it.filters.copy( + sortType = sortType + ) + } + ) + } } } @@ -228,5 +246,10 @@ data class TimelineState( val filterFolderName: String = "", val foldersAndFeeds: Map> = emptyMap(), val itemState: Flow> = emptyFlow(), - val confirmDialog: Boolean = false + val dialog: DialogState? = null ) + +sealed interface DialogState { + object ConfirmDialog : DialogState + object FilterSheet : DialogState +} diff --git a/appcompose/src/main/res/values-fr/strings.xml b/appcompose/src/main/res/values-fr/strings.xml index 3384ba26..e0d77928 100644 --- a/appcompose/src/main/res/values-fr/strings.xml +++ b/appcompose/src/main/res/values-fr/strings.xml @@ -73,6 +73,7 @@ Du plus ancien au plus récent La connexion a échoué. Veuillez vérifier vos identifiants Nouveau compte + Nouveaux articles Appli distribuée sous la licence GPLv3 Nombre maximum d\'articles par flux Illimité @@ -140,5 +141,6 @@ Thème du système Cacher les flux sans nouveaux items Marquer les items comme lus pendant le défilement + Filtres \ No newline at end of file diff --git a/appcompose/src/main/res/values/strings.xml b/appcompose/src/main/res/values/strings.xml index 272ac80c..1f651913 100644 --- a/appcompose/src/main/res/values/strings.xml +++ b/appcompose/src/main/res/values/strings.xml @@ -147,4 +147,5 @@ Hide feeds without new items Mark items read on scroll New articles + Filters \ No newline at end of file