├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── dictionaries │ ├── Cezar.xml │ └── constantin.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinScripting.xml ├── misc.xml ├── runConfigurations.xml ├── sqldelight │ └── database │ │ └── .sqldelight └── vcs.xml ├── LICENSE ├── README.md ├── androidapp ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── constantin │ │ └── microflux │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── constantin │ │ │ └── microflux │ │ │ ├── app │ │ │ ├── AppComponent.kt │ │ │ ├── ApplicationModule.kt │ │ │ └── ConstaFluxApplication.kt │ │ │ ├── broadcast │ │ │ ├── BroadcastReceiversModule.kt │ │ │ └── ViewConstafluxBroadcastReceiver.kt │ │ │ ├── module │ │ │ ├── DatabaseModule.kt │ │ │ ├── NetworkModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ └── ViewModelModule.kt │ │ │ ├── notification │ │ │ ├── Notification.kt │ │ │ ├── NotificationAccountIvalid.kt │ │ │ └── NotificationEntryUpdate.kt │ │ │ ├── ui │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityModule.kt │ │ │ ├── adapters │ │ │ │ ├── AccountListRecyclerViewAdapter.kt │ │ │ │ ├── CategoryListRecyclerViewAdapter.kt │ │ │ │ ├── EntryDescriptionPageAdapter.kt │ │ │ │ ├── EntryListRecyclerViewAdapter.kt │ │ │ │ ├── EntryResAdapter.kt │ │ │ │ └── FeedListRecyclerViewAdapter.kt │ │ │ └── fragment │ │ │ │ ├── AccountDialog.kt │ │ │ │ ├── AccountFragment.kt │ │ │ │ ├── BottomNavigationDrawerFragment.kt │ │ │ │ ├── CategoryDialog.kt │ │ │ │ ├── CategoryFragment.kt │ │ │ │ ├── EntryDescriptionFragment.kt │ │ │ │ ├── EntryDescriptionPagerFragment.kt │ │ │ │ ├── EntryFragment.kt │ │ │ │ ├── FeedDialog.kt │ │ │ │ ├── FeedFragment.kt │ │ │ │ ├── FragmentListContentBinding.kt │ │ │ │ └── SettingsFragment.kt │ │ │ ├── util │ │ │ ├── BindingBottomSheetDialogFragment.kt │ │ │ ├── BindingDialogFragment.kt │ │ │ ├── BindingFragment.kt │ │ │ ├── BroadcastReceiver.kt │ │ │ ├── ByteArrayFetcher.kt │ │ │ ├── ContextExtension.kt │ │ │ ├── DaggerBottomSheetDialogFragment.kt │ │ │ ├── DaggerDialogFragment.kt │ │ │ ├── Flow.kt │ │ │ ├── IOnBackPressed.kt │ │ │ ├── Intent.kt │ │ │ ├── MenuItem.kt │ │ │ ├── RecyclerViewAdapter.kt │ │ │ ├── SelectableRecyclerViewAdapter.kt │ │ │ ├── Settings.kt │ │ │ ├── SimpleCallBack.kt │ │ │ ├── Snackbar.kt │ │ │ ├── String.kt │ │ │ └── Toolbar.kt │ │ │ ├── view │ │ │ ├── NestedScrollWebView.kt │ │ │ └── RefreshLayout.kt │ │ │ └── worker │ │ │ ├── ConstafluxWorkerFactory.kt │ │ │ ├── MinifluxNotificationWorker.kt │ │ │ ├── WorkerAssistedInjectFactory.kt │ │ │ └── WorkerModule.kt │ └── res │ │ ├── anim │ │ ├── enter_from_bottom.xml │ │ ├── enter_from_left.xml │ │ ├── enter_from_right.xml │ │ ├── enter_from_top.xml │ │ ├── exit_to_bottom.xml │ │ ├── exit_to_left.xml │ │ ├── exit_to_right.xml │ │ └── exit_to_top.xml │ │ ├── drawable │ │ ├── empty_state_placeholder.xml │ │ ├── ic_add.xml │ │ ├── ic_all_articles.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_arrow_forward.xml │ │ ├── ic_category.xml │ │ ├── ic_check_mark.xml │ │ ├── ic_close.xml │ │ ├── ic_error.xml │ │ ├── ic_fetch_original_article.xml │ │ ├── ic_hamburger.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_mark_as_read.xml │ │ ├── ic_mark_as_unread.xml │ │ ├── ic_miniflux.xml │ │ ├── ic_no_star.xml │ │ ├── ic_open_web.xml │ │ ├── ic_rss_feed.xml │ │ ├── ic_select_all.xml │ │ ├── ic_settings.xml │ │ ├── ic_share.xml │ │ ├── ic_star.xml │ │ ├── ic_undo_fetch_original.xml │ │ ├── selection_state.xml │ │ ├── selector_color.xml │ │ └── webview_scroll.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dialog_account.xml │ │ ├── dialog_category.xml │ │ ├── dialog_feed.xml │ │ ├── fragment_entry_description.xml │ │ ├── fragment_entry_description_pager.xml │ │ ├── fragment_list_content.xml │ │ ├── fragment_login.xml │ │ ├── fragment_navigation_bottomsheet.xml │ │ ├── fragment_settings.xml │ │ ├── list_account.xml │ │ ├── list_add.xml │ │ ├── list_item_category.xml │ │ ├── list_item_entry.xml │ │ └── list_item_feed.xml │ │ ├── menu │ │ ├── menu_bottom_nav_drawer.xml │ │ ├── menu_entry_description.xml │ │ ├── menu_feed_category_dialog.xml │ │ ├── menu_list_category_feed.xml │ │ ├── menu_list_entry.xml │ │ └── menu_list_selection.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── navigation_main.xml │ │ ├── values-night │ │ ├── colors.xml │ │ └── theme.xml │ │ └── values │ │ ├── array.xml │ │ ├── attr.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── theme.xml │ └── test │ └── java │ └── com │ └── constantin │ └── microflux │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── data ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── constantin │ └── microflux │ ├── data │ ├── Category.kt │ ├── Entry.kt │ ├── Extensions.kt │ ├── Feed.kt │ ├── Me.kt │ ├── MergeBundle.kt │ ├── Result.kt │ ├── Server.kt │ ├── Settings.kt │ ├── User.kt │ └── Work.kt │ └── util │ └── Async.kt ├── database ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── constantin │ │ └── microflux │ │ └── database │ │ ├── Category.kt │ │ ├── ConstafluxDatabase.kt │ │ ├── Entry.kt │ │ ├── Feed.kt │ │ ├── Me.kt │ │ ├── Settings.kt │ │ ├── User.kt │ │ ├── Work.kt │ │ └── util │ │ ├── Error.kt │ │ └── SqlDelight.kt │ └── sqldelight │ └── com │ └── constantin │ └── microflux │ └── database │ ├── Category.sq │ ├── Entry.sq │ ├── Feed.sq │ ├── Me.sq │ ├── Server.sq │ ├── Settings.sq │ ├── User.sq │ └── Work.sq ├── encryption ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── constantin │ └── microflux │ └── encryption │ └── AesEncryption.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── network ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── constantin │ └── microflux │ └── network │ ├── CategoryNetwork.kt │ ├── EntryNetwork.kt │ ├── FeedNetwork.kt │ ├── MeNetwork.kt │ ├── MinifluxService.kt │ ├── data │ ├── Account.kt │ ├── Category.kt │ ├── Entry.kt │ ├── Feed.kt │ └── Me.kt │ └── util │ ├── Credentials.kt │ ├── Error.kt │ └── Ktor.kt ├── repository ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── constantin │ └── microflux │ └── repository │ ├── AccountRepository.kt │ ├── CategoryRepository.kt │ ├── ConstafluxRepository.kt │ ├── EntryRepository.kt │ ├── FeedRepository.kt │ ├── MeRepository.kt │ ├── NotificationRepository.kt │ ├── SettingsRepository.kt │ ├── WorkRepository.kt │ ├── transformation │ ├── Category.kt │ ├── Entry.kt │ ├── Feed.kt │ ├── Me.kt │ └── NotificationInformationBundle.kt │ └── util │ ├── DisplayTime.kt │ └── String.kt ├── settings.gradle.kts └── viewmodel ├── .gitignore ├── build.gradle.kts └── src └── main ├── AndroidManifest.xml └── java └── com └── constantin └── microflux └── module ├── AccountViewModel.kt ├── CategoryViewModel.kt ├── EntryDescriptionViewModel.kt ├── EntryViewModel.kt ├── FeedViewModel.kt ├── NavigationViewModel.kt ├── SettingsViewModel.kt ├── State.kt ├── ViewmodelFactory.kt └── util ├── BaseViewModel.kt └── load.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/dictionaries/Cezar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | chrisbanes 5 | consta 6 | constaflux 7 | constraintlayout 8 | coordinatorlayout 9 | dankito 10 | insetter 11 | jetbrains 12 | jsoup 13 | kapt 14 | kotlinx 15 | ktor 16 | miniflux 17 | snackbar 18 | sqldelight 19 | squareup 20 | swiperefreshlayout 21 | upsert 22 | viewmodel 23 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/dictionaries/constantin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | aead 5 | leakcanary 6 | microflux 7 | tink 8 | touchstart 9 | webview 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinScripting.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/sqldelight/database/.sqldelight: -------------------------------------------------------------------------------- 1 | {"databases":[{"packageName":"com.constantin.microflux.database","compilationUnits":[{"name":"debug","sourceFolders":[{"path":"src/debug/sqldelight","dependency":false},{"path":"src/main/sqldelight","dependency":false}]},{"name":"release","sourceFolders":[{"path":"src/main/sqldelight","dependency":false},{"path":"src/release/sqldelight","dependency":false}]}],"outputDirectory":"build/generated/sqldelight/code/Database","className":"Database","dependencies":[],"dialectPreset":"SQLITE_3_18","deriveSchemaFromMigrations":false}]} -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Constantin Cezar Begu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microflux 2 | Miniflux client. 3 | -------------------------------------------------------------------------------- /androidapp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /androidapp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /androidapp/src/androidTest/java/com/constantin/microflux/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.JUnit4 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(JUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("com.example.constaflux2", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /androidapp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /androidapp/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/app/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.app 2 | 3 | import com.constantin.microflux.module.DatabaseModule 4 | import com.constantin.microflux.module.NetworkModule 5 | import com.constantin.microflux.module.RepositoryModule 6 | import com.constantin.microflux.module.ViewModelModule 7 | import com.squareup.inject.assisted.dagger2.AssistedModule 8 | import dagger.BindsInstance 9 | import dagger.Component 10 | import dagger.Module 11 | import dagger.android.AndroidInjectionModule 12 | import dagger.android.AndroidInjector 13 | import javax.inject.Singleton 14 | 15 | @Singleton 16 | @Component( 17 | modules = [ 18 | AndroidInjectionModule::class, 19 | AssistedInjectModule::class, 20 | ApplicationModule::class, 21 | NetworkModule::class, 22 | DatabaseModule::class, 23 | RepositoryModule::class, 24 | ViewModelModule::class 25 | ] 26 | ) 27 | interface AppComponent : AndroidInjector { 28 | @Component.Factory 29 | interface Factory { 30 | fun create(@BindsInstance application: ConstaFluxApplication): AppComponent 31 | } 32 | } 33 | 34 | @AssistedModule 35 | @Module(includes = [AssistedInject_AssistedInjectModule::class]) 36 | interface AssistedInjectModule 37 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/app/ConstaFluxApplication.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.app 2 | 3 | import android.os.StrictMode 4 | import androidx.work.Configuration 5 | import androidx.work.WorkManager 6 | import com.constantin.microflux.BuildConfig 7 | import com.constantin.microflux.notification.registerNotificationChannels 8 | import com.constantin.microflux.worker.MinifluxNotificationWorker 9 | import dagger.android.support.DaggerApplication 10 | import javax.inject.Inject 11 | 12 | class ConstaFluxApplication : DaggerApplication(), Configuration.Provider { 13 | 14 | @Inject 15 | lateinit var workManagerConfig: Configuration 16 | 17 | @Inject 18 | lateinit var workManager: WorkManager 19 | 20 | override fun applicationInjector() = DaggerAppComponent.factory().create(this) 21 | 22 | override fun getWorkManagerConfiguration() = workManagerConfig 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | if (BuildConfig.DEBUG) { 27 | setupStrictMode() 28 | } 29 | registerNotificationChannels() 30 | enqueueWork() 31 | } 32 | 33 | private fun enqueueWork() { 34 | MinifluxNotificationWorker.enqueue(workManager) 35 | } 36 | 37 | private fun setupStrictMode() { 38 | StrictMode.setThreadPolicy( 39 | StrictMode.ThreadPolicy.Builder() 40 | .detectAll() 41 | .penaltyLog() 42 | .build() 43 | ) 44 | StrictMode.setVmPolicy( 45 | StrictMode.VmPolicy.Builder() 46 | .detectAll() 47 | .penaltyLog() 48 | .build() 49 | ) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/broadcast/BroadcastReceiversModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.broadcast 2 | 3 | import dagger.Module 4 | import dagger.android.ContributesAndroidInjector 5 | 6 | @Module 7 | abstract class BroadcastReceiversModule { 8 | 9 | @ContributesAndroidInjector 10 | abstract fun contributeViewConstafluxBroadcastReceiver(): ViewConstafluxBroadcastReceiver 11 | } 12 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/broadcast/ViewConstafluxBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.broadcast 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import androidx.lifecycle.LifecycleCoroutineScope 6 | import com.constantin.microflux.data.FeedId 7 | import com.constantin.microflux.data.ServerId 8 | import com.constantin.microflux.data.UserId 9 | import com.constantin.microflux.repository.ConstafluxRepository 10 | import com.constantin.microflux.util.goAsync 11 | import dagger.android.DaggerBroadcastReceiver 12 | import javax.inject.Inject 13 | 14 | class ViewConstafluxBroadcastReceiver : DaggerBroadcastReceiver() { 15 | 16 | companion object { 17 | private const val ACTION_UNIQUE = "actionUnique" 18 | private const val ACTION_SUMMARY = "actionSummary" 19 | private const val FEED_ID = "feedId" 20 | 21 | fun createIntent( 22 | context: Context, 23 | serverId: ServerId, 24 | feedId: FeedId 25 | ) = Intent(context, ViewConstafluxBroadcastReceiver::class.java) 26 | .setAction("$ACTION_UNIQUE:${serverId.id}:${feedId.id}") 27 | 28 | fun createIntentSummary( 29 | context: Context, 30 | serverId: ServerId, 31 | userId: UserId, 32 | feedIds: List 33 | ) = Intent(context, ViewConstafluxBroadcastReceiver::class.java) 34 | .setAction("$ACTION_SUMMARY:${serverId.id}:${userId.id}") 35 | .putExtra(FEED_ID, feedIds.map { it.id }.toLongArray()) 36 | } 37 | 38 | @Inject 39 | lateinit var repository: ConstafluxRepository 40 | 41 | @Inject 42 | lateinit var processLifecycleScope: LifecycleCoroutineScope 43 | 44 | override fun onReceive(context: Context, intent: Intent) { 45 | super.onReceive(context, intent) 46 | goAsync(processLifecycleScope) { 47 | val actions = intent.action?.split(":") ?: return@goAsync 48 | 49 | val action = actions[0] 50 | 51 | if (action == ACTION_UNIQUE) { 52 | val serverId = actions[1].toLong().let(::ServerId) 53 | 54 | val feedId = actions[2].toLong().let(::FeedId) 55 | 56 | repository.feedRepository.clearFeedNotificationCount( 57 | serverId = serverId, 58 | feedId = feedId 59 | ) 60 | 61 | } else if (action == ACTION_SUMMARY) { 62 | 63 | val serverId = actions[1].toLong().let(::ServerId) 64 | 65 | val feedIds = 66 | intent.getLongArrayExtra(FEED_ID) 67 | ?.map { FeedId(it) } ?: arrayListOf() 68 | 69 | feedIds.forEach{ feedId -> 70 | repository.feedRepository.clearFeedNotificationCount( 71 | serverId = serverId, 72 | feedId = feedId 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/module/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.database.ConstafluxDatabase 4 | import com.constantin.microflux.encryption.AesEncryption 5 | import com.squareup.sqldelight.db.SqlDriver 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | 10 | @Module 11 | object DatabaseModule { 12 | @Provides 13 | @Singleton 14 | fun provideDatabase( 15 | sqlDriver: SqlDriver, 16 | aesEncryption: AesEncryption 17 | ) = ConstafluxDatabase( 18 | sqlDriver = sqlDriver, 19 | aesEncryption = aesEncryption 20 | ) 21 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/module/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.network.MinifluxService 4 | import dagger.Module 5 | import dagger.Provides 6 | import io.ktor.client.engine.HttpClientEngineConfig 7 | import io.ktor.client.engine.HttpClientEngineFactory 8 | import io.ktor.client.engine.android.Android 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | object NetworkModule { 13 | @Provides 14 | @Singleton 15 | fun providesNetwork() = MinifluxService( 16 | engine = Android 17 | ) 18 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/module/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.database.ConstafluxDatabase 4 | import com.constantin.microflux.network.MinifluxService 5 | import com.constantin.microflux.repository.ConstafluxRepository 6 | import dagger.Module 7 | import dagger.Provides 8 | import javax.inject.Singleton 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | @Module 12 | object RepositoryModule { 13 | @Provides 14 | @Singleton 15 | fun provideRepository( 16 | context: CoroutineContext, 17 | constafluxDatabase: ConstafluxDatabase, 18 | minifluxService: MinifluxService 19 | ) = ConstafluxRepository( 20 | context = context, 21 | constafluxDatabase = constafluxDatabase, 22 | minifluxService = minifluxService 23 | ) 24 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/module/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.repository.ConstafluxRepository 4 | import dagger.Module 5 | import dagger.Provides 6 | import javax.inject.Singleton 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | @Module 10 | object ViewModelModule { 11 | @Provides 12 | @Singleton 13 | fun provideViewmodel( 14 | context: CoroutineContext, 15 | constafluxRepository: ConstafluxRepository 16 | ) = ViewmodelFactory( 17 | context = context, 18 | constafluxRepository = constafluxRepository 19 | ) 20 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/notification/Notification.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.notification 2 | 3 | import android.app.NotificationChannel 4 | import android.content.Context 5 | import android.os.Build 6 | import androidx.annotation.StringRes 7 | import androidx.core.app.NotificationManagerCompat 8 | import com.constantin.microflux.R 9 | 10 | enum class NotificationId { 11 | NEW_ENTRY, 12 | INVALID_USER 13 | } 14 | 15 | enum class NotificationChannelId { 16 | NEW_ENTRY, 17 | INVALID_USER 18 | } 19 | 20 | data class NotificationChannelData( 21 | val id: NotificationChannelId, 22 | @StringRes val title: Int, 23 | @StringRes val description: Int? = null, 24 | val importance: Int = NotificationManagerCompat.IMPORTANCE_DEFAULT 25 | ) 26 | 27 | val channelsData = listOf( 28 | NotificationChannelData( 29 | NotificationChannelId.NEW_ENTRY, 30 | R.string.notify_new_entry_channel_title, 31 | R.string.notify_new_entry_channel_desc 32 | ), 33 | NotificationChannelData( 34 | NotificationChannelId.INVALID_USER, 35 | R.string.notify_invalid_account_chanel_title, 36 | R.string.notify_invalid_account_channel_desc 37 | ) 38 | ) 39 | 40 | fun Context.registerNotificationChannels() { 41 | if (Build.VERSION.SDK_INT < 26) { 42 | return 43 | } 44 | 45 | val notificationManager = NotificationManagerCompat.from(this) 46 | channelsData.map { (id, title, description, importance) -> 47 | NotificationChannel(id.toString(), getString(title), importance).apply { 48 | this.description = description?.let(::getString) 49 | } 50 | }.forEach(notificationManager::createNotificationChannel) 51 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/notification/NotificationAccountIvalid.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.notification 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import androidx.core.app.NotificationCompat 6 | import androidx.core.app.NotificationManagerCompat 7 | import com.constantin.microflux.R 8 | import com.constantin.microflux.database.Account 9 | import com.constantin.microflux.ui.MainActivity 10 | 11 | fun List.notifyInvalidUsers( 12 | context: Context 13 | ) { 14 | forEach { 15 | it.notifyInvalidUser(context) 16 | } 17 | } 18 | 19 | private fun Account.notifyInvalidUser( 20 | context: Context 21 | ) { 22 | val channelId = NotificationChannelId.INVALID_USER.name 23 | val notificationId = NotificationId.INVALID_USER.ordinal 24 | val notificationManager = NotificationManagerCompat.from(context) 25 | notificationManager.cancelAll() 26 | 27 | val contentTitle = 28 | context.getString(R.string.notify_invalid_account_title, userName.name) 29 | 30 | val contentText = context.getString(R.string.notify_invalid_account_text) 31 | 32 | val contentIntent = PendingIntent.getActivity( 33 | context, 34 | 0, 35 | MainActivity.createIntentOpenInvalidAccountNotification( 36 | context = context, 37 | serverId = serverId, 38 | userId = userId 39 | ), 40 | PendingIntent.FLAG_UPDATE_CURRENT 41 | ) 42 | 43 | val notification = NotificationCompat.Builder(context, channelId) 44 | .setContentTitle(contentTitle) 45 | .setContentText(contentText) 46 | .setContentIntent(contentIntent) 47 | .setSmallIcon(R.drawable.ic_miniflux) 48 | .setColor(context.getColor(R.color.miniflux_vomit_green_mat_variant)) 49 | .setAutoCancel(true) 50 | .build() 51 | 52 | notificationManager.notify( 53 | "${serverId.id}:${userId.id}", 54 | notificationId, 55 | notification 56 | ) 57 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui 2 | 3 | import com.constantin.microflux.ui.fragment.* 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | abstract class MainActivityModule { 9 | 10 | @ContributesAndroidInjector 11 | abstract fun contributeEntryDescriptionFragment(): EntryDescriptionFragment 12 | 13 | @ContributesAndroidInjector 14 | abstract fun contributeEntryFragment(): EntryFragment 15 | 16 | @ContributesAndroidInjector 17 | abstract fun contributeFeedFragment(): FeedFragment 18 | 19 | @ContributesAndroidInjector 20 | abstract fun contributeCategoryFragment(): CategoryFragment 21 | 22 | @ContributesAndroidInjector 23 | abstract fun contributeSettingsFragment(): SettingsFragment 24 | 25 | @ContributesAndroidInjector 26 | abstract fun contributeAccountFragment(): AccountFragment 27 | 28 | @ContributesAndroidInjector 29 | abstract fun contributeBottomNavigationDrawerFragment(): BottomNavigationDrawerFragment 30 | 31 | @ContributesAndroidInjector 32 | abstract fun contributeEntryDescriptionPagerFragment(): EntryDescriptionPagerFragment 33 | 34 | @ContributesAndroidInjector 35 | abstract fun contributeCategoryDialog(): CategoryDialog 36 | 37 | @ContributesAndroidInjector 38 | abstract fun contributeFeedDialog(): FeedDialog 39 | 40 | @ContributesAndroidInjector 41 | abstract fun contributeAccountDialog(): AccountDialog 42 | 43 | @ContributesAndroidInjector 44 | abstract fun contributeMainActivity(): MainActivity 45 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/adapters/CategoryListRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.adapters 2 | 3 | import android.content.Context 4 | import androidx.recyclerview.widget.DiffUtil 5 | import com.constantin.microflux.database.Category 6 | import com.constantin.microflux.databinding.ListItemCategoryBinding 7 | import com.constantin.microflux.util.RecyclerViewAdapter 8 | 9 | class CategoryListRecyclerViewAdapter( 10 | itemClickCallback: (Long, Context, Int) -> Unit = { _, _, _ -> }, 11 | itemLongClickCallback: (Long, Context, Int) -> Unit = { _, _, _ -> } 12 | ) : 13 | RecyclerViewAdapter( 14 | viewInflater = ListItemCategoryBinding::inflate, 15 | itemClickCallback = itemClickCallback, 16 | itemLongClickCallback = itemLongClickCallback, 17 | diffItemCallback = diffItemCallback 18 | ) { 19 | 20 | companion object { 21 | private val diffItemCallback = object : DiffUtil.ItemCallback() { 22 | override fun areItemsTheSame( 23 | oldItem: Category, 24 | newItem: Category 25 | ): Boolean = oldItem.categoryId == newItem.categoryId 26 | 27 | override fun areContentsTheSame( 28 | oldItem: Category, 29 | newItem: Category 30 | ): Boolean = oldItem.categoryTitle == newItem.categoryTitle 31 | } 32 | } 33 | 34 | override val Category.id: Long 35 | get() = this.categoryId.id 36 | 37 | override fun onBindingCreated(item: Category, binding: ListItemCategoryBinding) { 38 | binding.run { 39 | textViewCategoryTitle.text = item.categoryTitle.title 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/adapters/EntryDescriptionPageAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.adapters 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | import com.constantin.microflux.data.EntryId 6 | import com.constantin.microflux.ui.fragment.EntryDescriptionFragment 7 | 8 | class EntryDescriptionPageAdapter( 9 | fragment: Fragment, 10 | private val entryIds: List 11 | ) : FragmentStateAdapter(fragment) { 12 | override fun getItemCount(): Int = entryIds.size 13 | override fun createFragment(position: Int): Fragment = 14 | EntryDescriptionFragment.createFragment(entryIds[position]) 15 | } 16 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/adapters/EntryResAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.adapters 2 | 3 | import android.content.Context 4 | import android.graphics.drawable.Drawable 5 | import androidx.core.content.ContextCompat 6 | import com.constantin.microflux.R 7 | import com.constantin.microflux.data.EntryStarred 8 | import com.constantin.microflux.data.EntryStatus 9 | 10 | fun EntryStarred.starIcon(context: Context): Drawable = 11 | ContextCompat.getDrawable( 12 | context, if (this == EntryStarred.STARRED) R.drawable.ic_star 13 | else R.drawable.ic_no_star 14 | )!! 15 | 16 | 17 | fun EntryStatus.statusIcon(context: Context): Drawable = 18 | ContextCompat.getDrawable( 19 | context, 20 | if (this == EntryStatus.UN_READ) R.drawable.ic_mark_as_unread 21 | else R.drawable.ic_mark_as_read 22 | )!! 23 | 24 | fun EntryStarred.starTitle(): Int = 25 | if (this == EntryStarred.UN_STARRED) R.string.star_article 26 | else R.string.un_star_article 27 | 28 | fun EntryStatus.statusTitle(): Int = 29 | if (this == EntryStatus.UN_READ) R.string.mark_as_read 30 | else R.string.mark_as_unread 31 | 32 | 33 | fun Boolean.fetchIcon(context: Context): Drawable = 34 | ContextCompat.getDrawable( 35 | context, if (this) R.drawable.ic_undo_fetch_original 36 | else R.drawable.ic_fetch_original_article 37 | )!! 38 | 39 | fun Boolean.fetchOriginalTitle(): Int = 40 | if (this) R.string.miniflux_article 41 | else R.string.fetch_feed_original_content -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/adapters/FeedListRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.adapters 2 | 3 | import android.content.Context 4 | import androidx.core.content.ContextCompat 5 | import androidx.recyclerview.widget.DiffUtil 6 | import coil.ImageLoader 7 | import coil.request.LoadRequest 8 | import com.constantin.microflux.R 9 | import com.constantin.microflux.database.FeedListPreview 10 | import com.constantin.microflux.databinding.ListItemFeedBinding 11 | import com.constantin.microflux.util.RecyclerViewAdapter 12 | import java.util.stream.DoubleStream.builder 13 | 14 | class FeedListRecyclerViewAdapter( 15 | private val imageLoader: ImageLoader, 16 | itemClickCallback: (Long, Context, Int) -> Unit = { _, _, _ -> }, 17 | itemLongClickCallback: (Long, Context, Int) -> Unit = { _, _, _ -> } 18 | ) : 19 | RecyclerViewAdapter( 20 | viewInflater = ListItemFeedBinding::inflate, 21 | itemClickCallback = itemClickCallback, 22 | itemLongClickCallback = itemLongClickCallback, 23 | diffItemCallback = diffItemCallback 24 | ) { 25 | 26 | companion object { 27 | private val diffItemCallback = object : DiffUtil.ItemCallback() { 28 | override fun areItemsTheSame( 29 | oldItem: FeedListPreview, 30 | newItem: FeedListPreview 31 | ): Boolean = oldItem.feedId == newItem.feedId 32 | 33 | override fun areContentsTheSame( 34 | oldItem: FeedListPreview, 35 | newItem: FeedListPreview 36 | ): Boolean = oldItem.feedTitle == newItem.feedTitle 37 | && oldItem.feedCheckedAtDisplay == newItem.feedCheckedAtDisplay 38 | && oldItem.categoryTitle == newItem.categoryTitle 39 | } 40 | } 41 | 42 | override val FeedListPreview.id: Long 43 | get() = this.feedId.id 44 | 45 | override fun onBindingCreated(item: FeedListPreview, binding: ListItemFeedBinding) { 46 | binding.run { 47 | imageViewIconFeed.run { 48 | imageLoader.execute( 49 | LoadRequest.Builder(context) 50 | .data(item.feedIcon.icon) 51 | .listener( 52 | onError = { _, _ -> 53 | setImageDrawable( 54 | ContextCompat.getDrawable( 55 | context, 56 | R.drawable.ic_miniflux 57 | ) 58 | ) 59 | } 60 | ) 61 | .target(this) 62 | .apply { builder() } 63 | .build() 64 | ) 65 | } 66 | 67 | textViewTitleFeed.text = item.feedTitle.title 68 | textViewLastCheckedFeed.text = item.feedCheckedAtDisplay.checkedAt 69 | textViewCategoryFeed.text = item.categoryTitle.title 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/fragment/BottomNavigationDrawerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.fragment 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.navigation.fragment.findNavController 6 | import com.constantin.microflux.R 7 | import com.constantin.microflux.databinding.FragmentNavigationBottomsheetBinding 8 | import com.constantin.microflux.module.NavigationViewModel 9 | import com.constantin.microflux.module.State 10 | import com.constantin.microflux.module.ViewmodelFactory 11 | import com.constantin.microflux.util.BindingBottomSheetDialogFragment 12 | import javax.inject.Inject 13 | 14 | class BottomNavigationDrawerFragment() : 15 | BindingBottomSheetDialogFragment( 16 | FragmentNavigationBottomsheetBinding::inflate 17 | ) { 18 | 19 | @Inject 20 | lateinit var viewModelFactory: ViewmodelFactory 21 | private lateinit var viewmodel: NavigationViewModel 22 | 23 | override fun onAttach(context: Context) { 24 | super.onAttach(context) 25 | viewmodel = viewModelFactory.create(State.Navigation) as NavigationViewModel 26 | } 27 | 28 | override fun onBindingCreated( 29 | binding: FragmentNavigationBottomsheetBinding, 30 | savedInstanceState: Bundle? 31 | ) { 32 | binding.run { 33 | attachCurrentAccount() 34 | attachAccountSelectionButton() 35 | attachNavigation() 36 | } 37 | } 38 | 39 | private fun FragmentNavigationBottomsheetBinding.attachCurrentAccount() { 40 | viewmodel.currentAccount.run { 41 | username.text = userName.name 42 | userUrl.text = serverUrl.url 43 | } 44 | } 45 | 46 | private fun FragmentNavigationBottomsheetBinding.attachAccountSelectionButton() { 47 | root.setOnClickListener { 48 | findNavController().navigate( 49 | BottomNavigationDrawerFragmentDirections.actionBottomNavigationDrawerFragmentToAccountDialog() 50 | ) 51 | } 52 | } 53 | 54 | private fun FragmentNavigationBottomsheetBinding.attachNavigation() { 55 | navigationView.setNavigationItemSelectedListener { menuItem -> 56 | findNavController().navigate( 57 | when (menuItem.itemId) { 58 | R.id.nav_all -> BottomNavigationDrawerFragmentDirections.actionBottomNavigationDrawerFragmentToEntryFragment() 59 | R.id.nav_feeds -> BottomNavigationDrawerFragmentDirections.actionBottomNavigationDrawerFragmentToFeedFragment() 60 | else -> BottomNavigationDrawerFragmentDirections.actionBottomNavigationDrawerFragmentToCategoryFragment() 61 | } 62 | ) 63 | dismiss() 64 | true 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/fragment/EntryDescriptionPagerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.fragment 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.navigation.fragment.navArgs 6 | import androidx.viewpager2.widget.ViewPager2 7 | import com.constantin.microflux.R 8 | import com.constantin.microflux.data.EntryId 9 | import com.constantin.microflux.databinding.FragmentEntryDescriptionPagerBinding 10 | import com.constantin.microflux.ui.adapters.EntryDescriptionPageAdapter 11 | import com.constantin.microflux.util.BindingFragment 12 | 13 | class EntryDescriptionPagerFragment() : BindingFragment( 14 | FragmentEntryDescriptionPagerBinding::inflate 15 | ) { 16 | 17 | private val args: EntryDescriptionPagerFragmentArgs by navArgs() 18 | private lateinit var entryIds: List 19 | private var selectedEntryId = EntryId.NO_ENTRY 20 | 21 | 22 | override fun onAttach(context: Context) { 23 | super.onAttach(context) 24 | entryIds = args.entryIds.map { EntryId(it) } 25 | selectedEntryId = args.selectedEntryId.let(::EntryId) 26 | } 27 | 28 | override fun onBindingCreated( 29 | binding: FragmentEntryDescriptionPagerBinding, 30 | savedInstanceState: Bundle? 31 | ) { 32 | binding.run { 33 | setAppBar() 34 | entriesPager() 35 | } 36 | } 37 | 38 | private fun FragmentEntryDescriptionPagerBinding.setAppBar() { 39 | toolBar.run { 40 | inflateMenu(R.menu.menu_entry_description) 41 | setNavigationOnClickListener { 42 | requireActivity().onBackPressed() 43 | } 44 | } 45 | } 46 | 47 | private fun FragmentEntryDescriptionPagerBinding.entriesPager() { 48 | entryDescriptionViewPager.run { 49 | orientation = ViewPager2.ORIENTATION_HORIZONTAL 50 | adapter = EntryDescriptionPageAdapter( 51 | fragment = this@EntryDescriptionPagerFragment, 52 | entryIds = entryIds.toList() 53 | ) 54 | setCurrentItem(entryIds.indexOf(selectedEntryId), false) 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/ui/fragment/FragmentListContentBinding.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.ui.fragment 2 | 3 | import com.constantin.microflux.R 4 | import com.constantin.microflux.data.Result 5 | import com.constantin.microflux.databinding.FragmentListContentBinding 6 | import com.constantin.microflux.util.EventSnackbar 7 | import com.constantin.microflux.util.makeSnackbar 8 | import com.constantin.microflux.util.toAndroidString 9 | 10 | 11 | fun FragmentListContentBinding.onRefresh(result: Result) { 12 | when (result) { 13 | is Result.InProgress -> { 14 | contentRefresh.isRefreshing = true 15 | } 16 | is Result.Complete -> { 17 | contentRefresh.isRefreshing = false 18 | } 19 | else -> { 20 | } 21 | } 22 | } 23 | 24 | fun FragmentListContentBinding.onError(result: Result, eventSnackbar: EventSnackbar) { 25 | if (result is Result.Error) { 26 | val stringRes = if (result is Result.Error.NetworkError) R.string.no_connectivity_error 27 | else R.string.error 28 | val snackbar = root.makeSnackbar(stringRes.toAndroidString()).setAnchorView(bottomAppBar) 29 | eventSnackbar.set(snackbar) 30 | } 31 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/BindingBottomSheetDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.viewbinding.ViewBinding 9 | 10 | abstract class BindingBottomSheetDialogFragment( 11 | private val viewInflater: (LayoutInflater, ViewGroup?, Boolean) -> B 12 | ) : 13 | DaggerBottomSheetDialogFragment() { 14 | 15 | protected val supportActivity: AppCompatActivity? 16 | get() = activity as? AppCompatActivity 17 | 18 | private var binding: B? = null 19 | 20 | protected abstract fun onBindingCreated(binding: B, savedInstanceState: Bundle?) 21 | 22 | fun requireBinding(): B { 23 | return checkNotNull(binding) 24 | } 25 | 26 | override fun onCreateView( 27 | inflater: LayoutInflater, 28 | container: ViewGroup?, 29 | savedInstanceState: Bundle? 30 | ): View? { 31 | return viewInflater(inflater, container, false).also { binding = it }.root 32 | } 33 | 34 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 35 | super.onViewCreated(view, savedInstanceState) 36 | val binding = requireBinding() 37 | onBindingCreated(binding, savedInstanceState) 38 | } 39 | 40 | override fun onDestroyView() { 41 | super.onDestroyView() 42 | binding = null 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/BindingDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.viewbinding.ViewBinding 9 | 10 | abstract class BindingDialogFragment( 11 | private val viewInflater: (LayoutInflater, ViewGroup?, Boolean) -> B 12 | ) : DaggerDialogFragment() { 13 | 14 | protected val supportActivity: AppCompatActivity? 15 | get() = activity as? AppCompatActivity 16 | 17 | private var binding: B? = null 18 | 19 | protected abstract fun onBindingCreated(binding: B, savedInstanceState: Bundle?) 20 | 21 | fun requireBinding(): B { 22 | return checkNotNull(binding) 23 | } 24 | 25 | override fun onCreateView( 26 | inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? { 30 | return viewInflater(inflater, container, false).also { binding = it }.root 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | val binding = requireBinding() 36 | onBindingCreated(binding, savedInstanceState) 37 | } 38 | 39 | override fun onDestroyView() { 40 | super.onDestroyView() 41 | binding = null 42 | } 43 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/BindingFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.viewbinding.ViewBinding 9 | import dagger.android.support.DaggerFragment 10 | 11 | abstract class BindingFragment( 12 | private val viewInflater: (LayoutInflater, ViewGroup?, Boolean) -> B 13 | ) : DaggerFragment() { 14 | 15 | protected val supportActivity: AppCompatActivity? 16 | get() = activity as? AppCompatActivity 17 | 18 | private var binding: B? = null 19 | 20 | protected abstract fun onBindingCreated(binding: B, savedInstanceState: Bundle?) 21 | 22 | 23 | fun requireBinding(): B { 24 | return checkNotNull(binding) 25 | } 26 | 27 | override fun onCreateView( 28 | inflater: LayoutInflater, 29 | container: ViewGroup?, 30 | savedInstanceState: Bundle? 31 | ): View? { 32 | return viewInflater(inflater, container, false).also { binding = it }.root 33 | } 34 | 35 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 36 | super.onViewCreated(view, savedInstanceState) 37 | val binding = requireBinding() 38 | // binding.root.setEdgeToEdgeSystemUiFlags(true) 39 | onBindingCreated(binding, savedInstanceState) 40 | } 41 | 42 | override fun onDestroyView() { 43 | super.onDestroyView() 44 | binding = null 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/BroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.BroadcastReceiver 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.launch 6 | 7 | inline fun BroadcastReceiver.goAsync( 8 | coroutineScope: CoroutineScope, 9 | crossinline action: suspend () -> Unit 10 | ) { 11 | val pendingResult = goAsync() 12 | coroutineScope.launch { 13 | try { 14 | action() 15 | } finally { 16 | pendingResult.finish() 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/ByteArrayFetcher.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import coil.bitmappool.BitmapPool 4 | import coil.decode.DataSource 5 | import coil.decode.Options 6 | import coil.fetch.FetchResult 7 | import coil.fetch.Fetcher 8 | import coil.fetch.SourceResult 9 | import coil.size.Size 10 | import okio.buffer 11 | import okio.source 12 | import java.io.ByteArrayInputStream 13 | 14 | 15 | class ByteArrayFetcher : Fetcher { 16 | 17 | override fun key(data: ByteArray): String? = null 18 | 19 | override suspend fun fetch( 20 | pool: BitmapPool, 21 | data: ByteArray, 22 | size: Size, 23 | options: Options 24 | ): FetchResult { 25 | return SourceResult( 26 | source = ByteArrayInputStream(data).source().buffer(), 27 | mimeType = null, 28 | dataSource = DataSource.MEMORY 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/ContextExtension.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.util.TypedValue 6 | import android.view.LayoutInflater 7 | import androidx.annotation.AttrRes 8 | import androidx.annotation.ColorInt 9 | import androidx.core.content.res.use 10 | 11 | inline val Context.layoutInflater: LayoutInflater 12 | get() = LayoutInflater.from(this) 13 | 14 | @ColorInt 15 | fun Context.getThemeColor( 16 | @AttrRes attrResId: Int, 17 | @ColorInt defaultValue: Int = Color.BLACK 18 | ) = obtainStyledAttributes(null, intArrayOf(attrResId)).use { it.getColor(0, defaultValue) } 19 | 20 | @ColorInt 21 | fun Context.getAttributeColor( 22 | @AttrRes attrResId: Int 23 | ) = resources.getColor( 24 | TypedValue().also { 25 | theme.resolveAttribute(attrResId, it, true) 26 | }.resourceId, 27 | theme 28 | ) 29 | 30 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/DaggerBottomSheetDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.Context 4 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 5 | import dagger.android.AndroidInjector 6 | import dagger.android.DispatchingAndroidInjector 7 | import dagger.android.HasAndroidInjector 8 | import dagger.android.support.AndroidSupportInjection 9 | import dagger.internal.Beta 10 | import javax.inject.Inject 11 | 12 | @Beta 13 | abstract class DaggerBottomSheetDialogFragment() : BottomSheetDialogFragment(), HasAndroidInjector { 14 | @Inject 15 | lateinit var androidInjector: DispatchingAndroidInjector 16 | 17 | override fun onAttach(context: Context) { 18 | AndroidSupportInjection.inject(this) 19 | super.onAttach(context) 20 | } 21 | 22 | override fun androidInjector(): AndroidInjector { 23 | return androidInjector 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/DaggerDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.Context 4 | import androidx.fragment.app.DialogFragment 5 | import dagger.android.AndroidInjector 6 | import dagger.android.DispatchingAndroidInjector 7 | import dagger.android.HasAndroidInjector 8 | import dagger.android.support.AndroidSupportInjection 9 | import dagger.internal.Beta 10 | import javax.inject.Inject 11 | 12 | @Beta 13 | abstract class DaggerDialogFragment() : DialogFragment(), HasAndroidInjector { 14 | @Inject 15 | lateinit var androidInjector: DispatchingAndroidInjector 16 | 17 | override fun onAttach(context: Context) { 18 | AndroidSupportInjection.inject(this) 19 | super.onAttach(context) 20 | } 21 | 22 | override fun androidInjector(): AndroidInjector { 23 | return androidInjector 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/Flow.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import androidx.lifecycle.LifecycleOwner 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.lifecycleScope 6 | import kotlinx.coroutines.Job 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | 11 | inline fun Flow?>.observeFilter( 12 | owner: LifecycleOwner, 13 | crossinline fistRunAction: () -> Unit = {}, 14 | crossinline stateChangeAction: () -> Unit = {}, 15 | crossinline action: (T) -> Unit 16 | ) { 17 | var job: Job? = null 18 | var stateChange: Boolean 19 | var isFirstRun = true 20 | onEach { childFlow -> 21 | stateChange = true 22 | job?.cancel() 23 | job = childFlow?.onEach { 24 | action(it) 25 | if (isFirstRun) { 26 | fistRunAction() 27 | isFirstRun = false 28 | stateChange = false 29 | } else if (stateChange) { 30 | stateChangeAction() 31 | stateChange = false 32 | } 33 | }?.launchIn(owner.lifecycleScope) 34 | }.launchIn(owner.lifecycleScope) 35 | } 36 | 37 | inline fun Flow?>.observeFilterLiveData( 38 | owner: LifecycleOwner, 39 | crossinline fistRunAction: () -> Unit = {}, 40 | crossinline stateChangeAction: () -> Unit = {}, 41 | crossinline action: (T) -> Unit 42 | ) { 43 | var job: LiveData? = null 44 | var stateChange: Boolean 45 | var isFirstRun = true 46 | onEach { childFlow -> 47 | stateChange = true 48 | job?.removeObservers(owner) 49 | job = childFlow.also { liveData -> 50 | liveData?.observe(owner) { 51 | action(it) 52 | if (isFirstRun) { 53 | fistRunAction() 54 | isFirstRun = false 55 | stateChange = false 56 | } else if (stateChange) { 57 | stateChangeAction() 58 | stateChange = false 59 | } 60 | } 61 | } 62 | }.launchIn(owner.lifecycleScope) 63 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/IOnBackPressed.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | interface IOnBackPressed { 4 | fun onBackPressed(): Boolean 5 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/Intent.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.Intent 4 | 5 | fun shareArticleIntent(title: String, url: String): Intent { 6 | val sendIntent = Intent(Intent.ACTION_SEND) 7 | sendIntent.putExtra(Intent.EXTRA_TITLE, title) 8 | sendIntent.putExtra(Intent.EXTRA_TEXT, url) 9 | sendIntent.type = "text/plain" 10 | return Intent.createChooser(sendIntent, null) 11 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/MenuItem.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.graphics.drawable.Drawable 4 | import android.view.MenuItem 5 | import androidx.annotation.StringRes 6 | 7 | fun MenuItem.changeMenu( 8 | drawableRes: Drawable, 9 | @StringRes resId: Int 10 | ) { 11 | icon = drawableRes 12 | setTitle(resId) 13 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/SelectableRecyclerViewAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.viewbinding.ViewBinding 8 | 9 | abstract class SelectableRecyclerViewAdapter( 10 | viewInflater: (LayoutInflater, ViewGroup?, Boolean) -> B, 11 | itemClickCallback: (Long, Context, Int) -> Unit = { _, _, _ -> }, 12 | private val itemCountCallback: (Int) -> Unit = { _ -> }, 13 | private val selectionCallback: (Boolean) -> Unit, 14 | diffItemCallback: DiffUtil.ItemCallback 15 | ) : RecyclerViewAdapter( 16 | viewInflater = viewInflater, 17 | diffItemCallback = diffItemCallback 18 | ) { 19 | private val _selectionList = mutableListOf() 20 | val selectionList: List 21 | get() = _selectionList.toList() 22 | 23 | var selection = false 24 | private set(value) { 25 | field = value 26 | selectionCallback(field) 27 | } 28 | 29 | init { 30 | setItemClickCallback { itemId, context, position -> 31 | if (!selection) itemClickCallback(itemId, context, position) 32 | else selectItem(itemId, position) 33 | } 34 | setItemLongClickCallback { itemId, _, position -> 35 | selectItem(itemId, position) 36 | } 37 | } 38 | 39 | fun isInList(itemId: Long) = itemId in _selectionList 40 | 41 | fun bulkSelection(toSelect: List = currentList.map { it.id }) { 42 | if (toSelect.isNotEmpty()) { 43 | selection = true 44 | _selectionList.clear() 45 | _selectionList.addAll(toSelect) 46 | notifyDataSetChanged() 47 | itemCountCallback(_selectionList.size) 48 | } 49 | } 50 | 51 | fun clearSelection() { 52 | selection = false 53 | _selectionList.clear() 54 | notifyDataSetChanged() 55 | itemCountCallback(_selectionList.size) 56 | } 57 | 58 | private fun selectItem(itemId: Long, itemPosition: Int) { 59 | if (itemId in _selectionList) removeSelectedItem(itemId) 60 | else addSelectedItem(itemId) 61 | notifyItemChanged(itemPosition) 62 | itemCountCallback(_selectionList.size) 63 | } 64 | 65 | private fun addSelectedItem(itemId: Long) { 66 | if (_selectionList.isEmpty()) selection = true 67 | _selectionList.add(itemId) 68 | } 69 | 70 | private fun removeSelectedItem(itemId: Long) { 71 | _selectionList.remove(itemId) 72 | if (_selectionList.isEmpty()) selection = false 73 | } 74 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import androidx.appcompat.app.AppCompatDelegate 4 | import com.constantin.microflux.data.SettingsTheme 5 | 6 | fun SettingsTheme?.toAndroidDelegate() = when (this) { 7 | SettingsTheme.AUTO -> defaultTheme 8 | SettingsTheme.DARK -> AppCompatDelegate.MODE_NIGHT_YES 9 | SettingsTheme.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO 10 | else -> defaultTheme 11 | } 12 | 13 | val defaultTheme = 14 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { 15 | AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 16 | } else { 17 | AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY 18 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/SimpleCallBack.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import androidx.recyclerview.widget.ItemTouchHelper 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.recyclerview.widget.SimpleItemAnimator 6 | 7 | 8 | enum class Direction(val flag: Int) { 9 | LEFT(ItemTouchHelper.LEFT), 10 | RIGHT(ItemTouchHelper.RIGHT), 11 | START(ItemTouchHelper.START), 12 | END(ItemTouchHelper.END), 13 | UP(ItemTouchHelper.UP), 14 | DOWN(ItemTouchHelper.DOWN) 15 | } 16 | 17 | fun RecyclerView.disableAnimations() { 18 | (this.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = 19 | false 20 | } 21 | 22 | fun Array.fold() = this.fold(0) { acc, direction -> acc or direction.flag } 23 | 24 | val DEFAULT_SWIPE = arrayOf(Direction.START, Direction.END) 25 | 26 | fun ItemTouchHelper.SimpleCallback.stopSwipes() { 27 | this.setDefaultSwipeDirs(0) 28 | } 29 | 30 | fun ItemTouchHelper.SimpleCallback.setSwipes(vararg directions: Direction = DEFAULT_SWIPE) { 31 | this.setDefaultSwipeDirs(directions.fold()) 32 | } 33 | 34 | inline fun RecyclerView.onSwipe( 35 | vararg directions: Direction = DEFAULT_SWIPE, 36 | crossinline action: (RecyclerView.ViewHolder, Direction) -> Unit 37 | ): ItemTouchHelper.SimpleCallback { 38 | val swipeDirFlags = directions.fold() 39 | 40 | val simpleCallback = object : ItemTouchHelper.SimpleCallback(0, swipeDirFlags) { 41 | override fun onMove( 42 | recyclerView: RecyclerView, 43 | viewHolder: RecyclerView.ViewHolder, 44 | target: RecyclerView.ViewHolder 45 | ): Boolean { 46 | return false 47 | } 48 | 49 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 50 | val swipedDirection = Direction.values().single { it.flag == direction } 51 | action(viewHolder, swipedDirection) 52 | } 53 | } 54 | 55 | ItemTouchHelper(simpleCallback).attachToRecyclerView(this) 56 | 57 | return simpleCallback 58 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/Snackbar.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.view.View 4 | import com.constantin.microflux.R 5 | import com.google.android.material.snackbar.BaseTransientBottomBar 6 | import com.google.android.material.snackbar.Snackbar 7 | 8 | enum class SnackbarLength(val flag: Int) { 9 | INDEFINITE(Snackbar.LENGTH_INDEFINITE), 10 | SHORT(Snackbar.LENGTH_SHORT), 11 | LONG(Snackbar.LENGTH_LONG) 12 | } 13 | 14 | inline fun View.makeSnackbar( 15 | text: AndroidString, 16 | actionText: AndroidString? = null, 17 | length: SnackbarLength = SnackbarLength.SHORT, 18 | show: Boolean = false, 19 | crossinline action: () -> Unit = { } 20 | ): Snackbar { 21 | val textString = context.getString(text) 22 | val actionTextString = actionText?.let(context::getString) 23 | return Snackbar.make(this, textString, length.flag).apply { 24 | if (actionTextString != null) { 25 | setAction(actionTextString) { action() } 26 | val oppositePrimaryColor = context.getColor(R.color.color_primary_opposite) 27 | setActionTextColor(oppositePrimaryColor) 28 | } 29 | if (show) { 30 | show() 31 | } 32 | } 33 | } 34 | 35 | class EventSnackbar { 36 | 37 | private var eventSnackbar: Snackbar? = null 38 | private var eventSnackbarCallback: Snackbar.Callback? = null 39 | 40 | fun set(snackbar: Snackbar?, onFinish: () -> Unit = { }) { 41 | val validDismissEvent = BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_SWIPE 42 | eventSnackbarCallback?.onDismissed(eventSnackbar, validDismissEvent) 43 | 44 | snackbar ?: return 45 | 46 | eventSnackbarCallback = object : Snackbar.Callback() { 47 | override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { 48 | transientBottomBar ?: return 49 | if (event != DISMISS_EVENT_MANUAL && event != DISMISS_EVENT_CONSECUTIVE) { 50 | onFinish() 51 | } 52 | eventSnackbar = null 53 | eventSnackbarCallback = null 54 | } 55 | } 56 | eventSnackbar = snackbar.apply { 57 | addCallback(eventSnackbarCallback) 58 | show() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/String.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | 6 | sealed class AndroidString { 7 | data class Res(@StringRes val resId: Int) : AndroidString() 8 | data class Raw(val string: String) : AndroidString() 9 | } 10 | 11 | fun Context.getString(androidString: AndroidString): String { 12 | return when (androidString) { 13 | is AndroidString.Res -> getString(androidString.resId) 14 | is AndroidString.Raw -> androidString.string 15 | } 16 | } 17 | 18 | fun @receiver:StringRes Int.toAndroidString() = AndroidString.Res(this) 19 | 20 | fun String.toAndroidString() = AndroidString.Raw(this) -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/util/Toolbar.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import androidx.annotation.MenuRes 4 | import androidx.appcompat.widget.Toolbar 5 | 6 | fun Toolbar.replaceMenu(@MenuRes newMenu: Int) { 7 | menu.clear() 8 | inflateMenu(newMenu) 9 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/view/RefreshLayout.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import androidx.core.view.NestedScrollingChild 6 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 7 | import com.constantin.microflux.R 8 | import com.constantin.microflux.util.getThemeColor 9 | 10 | class RefreshLayout @JvmOverloads constructor( 11 | context: Context, 12 | attrs: AttributeSet? = null 13 | ) : SwipeRefreshLayout(context, attrs), NestedScrollingChild { 14 | 15 | init { 16 | setThemeColorScheme() 17 | } 18 | 19 | private fun SwipeRefreshLayout.setThemeColorScheme() { 20 | val foregroundColor = context.getThemeColor(R.attr.colorPrimary) 21 | val backgroundColor = context.getThemeColor(R.attr.colorBackgroundFloating) 22 | setColorSchemeColors(foregroundColor) 23 | setProgressBackgroundColorSchemeColor(backgroundColor) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/worker/ConstafluxWorkerFactory.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.worker 2 | 3 | import android.content.Context 4 | import androidx.work.ListenableWorker 5 | import androidx.work.WorkerFactory 6 | import androidx.work.WorkerParameters 7 | import javax.inject.Inject 8 | import javax.inject.Provider 9 | 10 | class ConstafluxWorkerFactory @Inject constructor( 11 | private val providers: Map, @JvmSuppressWildcards Provider> 12 | ) : WorkerFactory() { 13 | 14 | override fun createWorker( 15 | appContext: Context, 16 | workerClassName: String, 17 | workerParameters: WorkerParameters 18 | ): ListenableWorker? { 19 | val workerClass = try { 20 | Class.forName(workerClassName) 21 | } catch (e: ClassNotFoundException) { 22 | return null 23 | } 24 | val workerProvider = requireNotNull(providers[workerClass]) { 25 | "No provider found for worker: $workerClassName" 26 | } 27 | return workerProvider.get().create(appContext, workerParameters) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/worker/MinifluxNotificationWorker.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.worker 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import androidx.work.* 6 | import coil.ImageLoader 7 | import com.constantin.microflux.notification.notifyInvalidUsers 8 | import com.constantin.microflux.notification.notifyNewMinifluxEntries 9 | import com.constantin.microflux.repository.ConstafluxRepository 10 | import com.squareup.inject.assisted.Assisted 11 | import com.squareup.inject.assisted.AssistedInject 12 | import java.time.Duration 13 | 14 | typealias NotificationResult = com.constantin.microflux.data.Result.NotificationInformation 15 | 16 | class MinifluxNotificationWorker @AssistedInject constructor( 17 | @Assisted appContext: Context, 18 | @Assisted workerParams: WorkerParameters, 19 | private val repository: ConstafluxRepository, 20 | private val imageLoader: ImageLoader 21 | ) : CoroutineWorker(appContext, workerParams) { 22 | 23 | companion object { 24 | private const val WORK_NAME = "newEntries" 25 | 26 | @SuppressLint("NewApi") // Core library desugaring handles java.time backport 27 | fun enqueue(workManager: WorkManager) { 28 | val repeatInterval = Duration.ofMinutes(15) 29 | val constraints = Constraints.Builder() 30 | .setRequiredNetworkType(NetworkType.CONNECTED) 31 | .build() 32 | val workRequest = PeriodicWorkRequestBuilder(repeatInterval) 33 | .setConstraints(constraints) 34 | .build() 35 | workManager.enqueueUniquePeriodicWork( 36 | WORK_NAME, 37 | ExistingPeriodicWorkPolicy.REPLACE, 38 | workRequest 39 | ) 40 | } 41 | } 42 | 43 | @AssistedInject.Factory 44 | interface Factory : WorkerAssistedInjectFactory 45 | 46 | override suspend fun doWork(): Result { 47 | repository.backGroundProcessRepository.refreshAllContent().run { 48 | if (this is NotificationResult) { 49 | notifications.run { 50 | accountsFeedsInformation.forEach { 51 | it.notifyNewMinifluxEntries( 52 | applicationContext, 53 | imageLoader 54 | ) 55 | } 56 | invalidAccounts.notifyInvalidUsers(applicationContext) 57 | } 58 | } 59 | } 60 | return Result.success() 61 | } 62 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/worker/WorkerAssistedInjectFactory.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.worker 2 | 3 | import android.content.Context 4 | import androidx.work.ListenableWorker 5 | import androidx.work.WorkerParameters 6 | 7 | interface WorkerAssistedInjectFactory { 8 | fun create(appContext: Context, workerParams: WorkerParameters): ListenableWorker 9 | } -------------------------------------------------------------------------------- /androidapp/src/main/java/com/constantin/microflux/worker/WorkerModule.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.worker 2 | 3 | import androidx.work.ListenableWorker 4 | import dagger.Binds 5 | import dagger.MapKey 6 | import dagger.Module 7 | import dagger.multibindings.IntoMap 8 | import kotlin.reflect.KClass 9 | 10 | @Module 11 | abstract class WorkersModule { 12 | @Binds 13 | @IntoMap 14 | @WorkerKey(MinifluxNotificationWorker::class) 15 | abstract fun bindNewEntryWorker(factory: MinifluxNotificationWorker.Factory): WorkerAssistedInjectFactory 16 | } 17 | 18 | @MapKey 19 | @Retention(AnnotationRetention.RUNTIME) 20 | @Target(AnnotationTarget.FUNCTION) 21 | annotation class WorkerKey(val value: KClass) 22 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/enter_from_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/enter_from_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/enter_from_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/enter_from_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/exit_to_bottom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/exit_to_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/exit_to_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/anim/exit_to_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_all_articles.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_arrow_forward.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_category.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_check_mark.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_fetch_original_article.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_hamburger.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_mark_as_read.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_mark_as_unread.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_miniflux.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_no_star.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_open_web.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_rss_feed.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_select_all.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/ic_undo_fetch_original.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/selection_state.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/selector_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /androidapp/src/main/res/drawable/webview_scroll.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/dialog_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 25 | 26 | 35 | 36 | 50 | 51 | 60 | 61 | 62 | 63 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/fragment_entry_description.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/fragment_entry_description_pager.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 21 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/fragment_navigation_bottomsheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 23 | 24 | 36 | 37 | 45 | 46 | 47 | 57 | 58 | 59 | 60 | 72 | 73 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/list_account.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 19 | 20 | 32 | 33 | 44 | 45 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/list_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /androidapp/src/main/res/layout/list_item_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 22 | 23 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /androidapp/src/main/res/menu/menu_bottom_nav_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | -------------------------------------------------------------------------------- /androidapp/src/main/res/menu/menu_entry_description.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 19 | 24 | 29 | -------------------------------------------------------------------------------- /androidapp/src/main/res/menu/menu_feed_category_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /androidapp/src/main/res/menu/menu_list_category_feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /androidapp/src/main/res/menu/menu_list_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | -------------------------------------------------------------------------------- /androidapp/src/main/res/menu/menu_list_selection.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 19 | -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/androidapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /androidapp/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @color/color_primary_light 5 | 6 | @color/miniflux_vomit_green_mat_variant 7 | #33FFFFFF 8 | #B3000000 9 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values-night/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/array.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Auto 5 | Light 6 | Dark 7 | 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #7abf9c 4 | #1a6043 5 | #4b8e6e 6 | 7 | #7395be 8 | #103d60 9 | #44678e 10 | 11 | 12 | @color/miniflux_vomit_green_mat_dark 13 | @color/miniflux_vomit_green_mat_light 14 | 15 | @color/color_primary_dark 16 | 17 | @color/miniflux_vomit_green_mat_variant 18 | #B3FFFFFF 19 | #33000000 20 | 21 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 8dp 6 | 176dp 7 | 16dp 8 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4B8E6E 4 | -------------------------------------------------------------------------------- /androidapp/src/main/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 23 | 24 | 35 | 36 | 40 | 41 | 46 | 47 | 50 | 51 | 54 | -------------------------------------------------------------------------------- /androidapp/src/test/java/com/constantin/microflux/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | apply(plugin = "com.github.ben-manes.versions") 2 | 3 | buildscript { 4 | val kotlinVersion by extra("1.4.10") 5 | repositories { 6 | google() 7 | jcenter() 8 | maven("https://dl.bintray.com/kotlin/kotlin-eap") 9 | maven("https://plugins.gradle.org/m2/") 10 | gradlePluginPortal() 11 | } 12 | 13 | dependencies { 14 | // Reading gradle versions 15 | classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0") 16 | // Kotlin 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10") 18 | classpath("org.jetbrains.kotlin:kotlin-serialization:1.4.10") 19 | // Android tools 20 | classpath("com.android.tools.build:gradle:4.2.0-alpha15") 21 | // Navigation safe args 22 | classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.1") 23 | // Sqldelight 24 | classpath("com.squareup.sqldelight:gradle-plugin:1.4.3") 25 | 26 | } 27 | } 28 | 29 | allprojects { 30 | repositories { 31 | google() 32 | jcenter() 33 | maven("https://dl.bintray.com/kotlin/kotlin-eap") 34 | maven("https://oss.sonatype.org/content/repositories/snapshots") 35 | } 36 | } 37 | 38 | tasks.register("clean") { 39 | delete(rootProject.buildDir) 40 | } -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | id("kotlin") 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | } 10 | 11 | dependencies { 12 | // Kotlin std lib 13 | implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.10") 14 | // Coroutines 15 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") 16 | } 17 | 18 | val compileKotlin: KotlinCompile by tasks 19 | compileKotlin.kotlinOptions { 20 | jvmTarget = "1.8" 21 | @Suppress("SuspiciousCollectionReassignment") 22 | freeCompilerArgs += listOf( 23 | "-progressive", 24 | "-XXLanguage:+NewInference", 25 | "-XXLanguage:+InlineClasses", 26 | "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", 27 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 28 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 29 | ) 30 | } -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Category.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class CategoryId(val id: Long){ 4 | companion object{ 5 | val NO_CATEGORY = CategoryId(-1L) 6 | } 7 | } 8 | 9 | inline class CategoryTitle(val title: String) 10 | 11 | -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Entry.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class EntryId(val id: Long){ 4 | companion object{ 5 | val NO_ENTRY = EntryId(-1L) 6 | } 7 | } 8 | 9 | inline class EntryTitle(val title: String) 10 | 11 | inline class EntryUrl(val url: String) 12 | 13 | inline class EntryPreviewImage(val previewImage: String) 14 | 15 | inline class EntryAuthor(val author: String) 16 | 17 | inline class EntryContent(val content: String) 18 | 19 | inline class EntryPublishedAtDisplay(val publishedAt: String) { 20 | operator fun compareTo(entryPublishedAt: EntryPublishedAtDisplay): Int { 21 | return this.publishedAt.compareTo(entryPublishedAt.publishedAt) 22 | } 23 | } 24 | 25 | inline class EntryPublishedAtRaw(val publishedAt: String) { 26 | operator fun compareTo(entryPublishedAt: EntryPublishedAtRaw): Int { 27 | return this.publishedAt.compareTo(entryPublishedAt.publishedAt) 28 | } 29 | } 30 | 31 | inline class EntryStatus(val status: String) { 32 | companion object { 33 | val READ = EntryStatus("read") 34 | val UN_READ = EntryStatus("unread") 35 | val ALL = EntryStatus("all") 36 | } 37 | 38 | fun not() = if (this == READ) UN_READ else if (this == UN_READ) READ else ALL 39 | } 40 | 41 | inline class EntryStarred(val starred: Boolean) { 42 | companion object { 43 | val STARRED = EntryStarred(true) 44 | val UN_STARRED = EntryStarred(false) 45 | } 46 | fun not() = if (this == STARRED) UN_STARRED else STARRED 47 | } 48 | 49 | inline class EntryPublishedAtUnix(val publishedAt: Long) { 50 | companion object { 51 | val EMPTY = EntryPublishedAtUnix(0) 52 | } 53 | } -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | fun Long.toBoolean(): Boolean { 4 | return (this > 0) 5 | } 6 | 7 | fun Boolean.toLong(): Long { 8 | return if (this) 1 else 0 9 | } 10 | -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Feed.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class FeedId(val id: Long){ 4 | companion object{ 5 | val NO_FEED = FeedId(-1L) 6 | } 7 | } 8 | 9 | inline class FeedTitle(val title: String) 10 | 11 | inline class FeedSiteUrl(val siteUrl: String) 12 | 13 | inline class FeedUrl(val url: String) 14 | 15 | inline class FeedCheckedAtDisplay(val checkedAt: String) 16 | 17 | inline class FeedLastUpdateAtUnix(val lastUpdateAtUnix: Long) { 18 | companion object { 19 | val EMPTY = FeedLastUpdateAtUnix(0) 20 | } 21 | } 22 | 23 | inline class FeedIcon(val icon: ByteArray) 24 | 25 | inline class FeedScraperRules(val scraperRules: String) 26 | 27 | inline class FeedRewriteRules(val rewriteRules: String) 28 | 29 | inline class FeedCrawler(val crawler: Boolean) { 30 | companion object { 31 | val ON = FeedCrawler(true) 32 | val OFF = FeedCrawler(false) 33 | } 34 | } 35 | 36 | inline class FeedUsername(val username: String) 37 | 38 | inline class FeedPassword(val password: String) 39 | 40 | inline class FeedUserAgent(val userAgent: String) 41 | 42 | inline class FeedAllowNotification(val notification: Boolean) { 43 | companion object { 44 | val ON = FeedAllowNotification(true) 45 | val OFF = FeedAllowNotification(false) 46 | } 47 | } 48 | 49 | inline class FeedAllowImagePreview(val allowImagePreview: Boolean) { 50 | companion object { 51 | val ON = FeedAllowImagePreview(true) 52 | val OFF = FeedAllowImagePreview(false) 53 | } 54 | } 55 | 56 | inline class FeedNotificationCount(val count: Long) { 57 | companion object { 58 | val INVALID = FeedNotificationCount(0) 59 | } 60 | } 61 | 62 | inline class FeedNotified(val notified: Boolean) { 63 | companion object { 64 | val ON = FeedNotified(true) 65 | val OFF = FeedNotified(false) 66 | } 67 | } -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Me.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class MeIsAdmin(val isAdmin: Boolean) 4 | 5 | inline class MeLanguage(val language: String) 6 | 7 | inline class MeLastLoginAt(val lastLoginAt: String) 8 | 9 | inline class MeTheme(val theme: String) 10 | 11 | inline class MeTimeZone(val timeZone: String) 12 | 13 | inline class MeEntrySortingDirection(val sortingDirection: String) 14 | -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/MergeBundle.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | class MergeBundle( 4 | val rightData: T, 5 | val leftData: R 6 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Server.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class ServerUrl(val url: String) 4 | 5 | inline class ServerId(val id: Long){ 6 | companion object{ 7 | val NO_SERVER = ServerId(-1L) 8 | } 9 | } -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class SettingsTheme(val theme: Int) { 4 | companion object { 5 | val AUTO = SettingsTheme(0) 6 | val LIGHT = SettingsTheme(1) 7 | val DARK = SettingsTheme(2) 8 | } 9 | } 10 | 11 | inline class SettingsAllowImagePreview(val allowImagePreview: Boolean){ 12 | companion object{ 13 | val ON = SettingsAllowImagePreview(true) 14 | val OFF = SettingsAllowImagePreview(false) 15 | } 16 | } -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/User.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class UserId(val id: Long){ 4 | companion object{ 5 | val NO_USER = UserId(-1L) 6 | } 7 | } 8 | 9 | inline class UserName(val name: String) 10 | 11 | inline class UserPassword(val password: String) 12 | 13 | inline class UserSelected(val selected: Boolean) { 14 | companion object { 15 | val SELECTED = UserSelected(true) 16 | val UNSELECTED = UserSelected(false) 17 | } 18 | } 19 | 20 | inline class UserFirstTimeRun (val firstTimeRun: Boolean){ 21 | companion object { 22 | val TRUE = UserFirstTimeRun(true) 23 | val FALSE = UserFirstTimeRun(false) 24 | } 25 | } -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/data/Work.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.data 2 | 3 | inline class WorkType(val type: Long){ 4 | companion object{ 5 | val STATUS_MARK_AS_READ = WorkType(0) 6 | val STATUS_MARK_AS_UNREAD = WorkType(1) 7 | val STAR = WorkType(2) 8 | } 9 | } 10 | 11 | fun EntryStatus.toWorkType() = if (this == EntryStatus.READ) WorkType.STATUS_MARK_AS_READ else WorkType.STATUS_MARK_AS_UNREAD -------------------------------------------------------------------------------- /data/src/main/java/com/constantin/microflux/util/Async.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.util 2 | 3 | import kotlinx.coroutines.async 4 | import kotlinx.coroutines.awaitAll 5 | import kotlinx.coroutines.coroutineScope 6 | 7 | suspend inline fun Iterable.mapAsync(crossinline action: suspend (T) -> R) = 8 | coroutineScope { 9 | map { element -> 10 | async { action(element) } 11 | }.awaitAll() 12 | } 13 | 14 | suspend inline fun Iterable.forEachAsync(crossinline action: suspend (T) -> Unit): Unit = 15 | coroutineScope { 16 | map { element -> 17 | async { action(element) } 18 | }.awaitAll() 19 | Unit 20 | } -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /database/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("kotlin-android-extensions") 5 | id("kotlin-kapt") 6 | id("com.squareup.sqldelight") 7 | } 8 | 9 | android { 10 | compileSdkVersion(30) 11 | defaultConfig { 12 | minSdkVersion(24) 13 | targetSdkVersion(30) 14 | versionCode = 1 15 | versionName = "1.0" 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | getByName("release") { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 23 | ) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | @Suppress("SuspiciousCollectionReassignment") 33 | freeCompilerArgs += listOf( 34 | "-progressive", 35 | "-XXLanguage:+NewInference", 36 | "-XXLanguage:+InlineClasses", 37 | "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", 38 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 39 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 40 | ) 41 | } 42 | packagingOptions { 43 | pickFirst("META-INF/*.kotlin_module") 44 | } 45 | } 46 | 47 | repositories { 48 | jcenter() 49 | } 50 | 51 | dependencies { 52 | // Modules 53 | implementation(project(":data")) 54 | implementation(project(":encryption")) 55 | // Sqldelight 56 | implementation("com.squareup.sqldelight:android-driver:1.4.3") 57 | implementation("com.squareup.sqldelight:coroutines-extensions-jvm:1.4.3") 58 | // Tink for encryption 59 | implementation ("com.google.crypto.tink:tink-android:1.4.0-rc2") 60 | } 61 | 62 | sqldelight { 63 | database("Database") { 64 | packageName = "com.constantin.microflux.database" 65 | } 66 | } -------------------------------------------------------------------------------- /database/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/Category.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database 2 | 3 | import com.constantin.microflux.data.ServerId 4 | import com.constantin.microflux.data.UserId 5 | 6 | fun CategoryQueries.upsert( 7 | category: Category 8 | ) = transaction { 9 | insert( 10 | serverId = category.serverId, 11 | categoryId = category.categoryId, 12 | categoryTitle = category.categoryTitle, 13 | userId = category.userId 14 | ) 15 | update( 16 | serverId = category.serverId, 17 | categoryId = category.categoryId, 18 | categoryTitle = category.categoryTitle 19 | ) 20 | } 21 | 22 | fun CategoryQueries.refreshAll(serverId: ServerId, userId: UserId, categoryList: List) = 23 | transaction { 24 | clearAll( 25 | serverId = serverId, 26 | userId = userId, 27 | categoryId = categoryList.toCategoryId() 28 | ) 29 | categoryList.forEach { upsert(it) } 30 | } 31 | 32 | 33 | fun List.toCategoryId() = map { it.categoryId } -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/Feed.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database 2 | 3 | import com.constantin.microflux.data.FeedId 4 | import com.constantin.microflux.data.Result 5 | import com.constantin.microflux.data.ServerId 6 | import com.constantin.microflux.data.UserId 7 | import com.constantin.microflux.database.util.error 8 | 9 | fun FeedQueries.upsert( 10 | feed: Feed 11 | ) = transaction { 12 | insert( 13 | serverId = feed.serverId, 14 | feedId = feed.feedId, 15 | feedTitle = feed.feedTitle, 16 | feedSiteUrl = feed.feedSiteUrl, 17 | feedUrl = feed.feedUrl, 18 | feedCheckedAtDisplay = feed.feedCheckedAtDisplay, 19 | feedIcon = feed.feedIcon, 20 | feedScraperRules = feed.feedScraperRules, 21 | feedRewriteRules = feed.feedRewriteRules, 22 | feedCrawler = feed.feedCrawler, 23 | feedUsername = feed.feedUsername, 24 | feedPassword = feed.feedPassword, 25 | feedUserAgent = feed.feedUserAgent, 26 | categoryId = feed.categoryId 27 | ) 28 | updateImpl( 29 | serverId = feed.serverId, 30 | feedId = feed.feedId, 31 | feedTitle = feed.feedTitle, 32 | feedSiteUrl = feed.feedSiteUrl, 33 | feedUrl = feed.feedUrl, 34 | feedCheckedAtDisplay = feed.feedCheckedAtDisplay, 35 | feedIcon = feed.feedIcon, 36 | feedScraperRules = feed.feedScraperRules, 37 | feedRewriteRules = feed.feedRewriteRules, 38 | feedCrawler = feed.feedCrawler, 39 | feedUsername = feed.feedUsername, 40 | feedPassword = feed.feedPassword, 41 | feedUserAgent = feed.feedUserAgent, 42 | categoryId = feed.categoryId 43 | ) 44 | } 45 | 46 | fun FeedQueries.refreshAll( 47 | serverId: ServerId, 48 | userId: UserId, 49 | feedList: List 50 | ): Result = error { 51 | transaction { 52 | clearAll( 53 | serverId = serverId, 54 | userId = userId, 55 | feedId = feedList.toFeedIdList() 56 | ) 57 | feedList.forEach { upsert(it) } 58 | } 59 | } 60 | 61 | fun FeedQueries.updateLastUpdateAtUnix( 62 | serverId: ServerId, 63 | userId: UserId, 64 | feedId: FeedId 65 | ) = transaction { 66 | if (feedId == FeedId.NO_FEED){ 67 | selectAllId( 68 | serverId = serverId, 69 | userId = userId 70 | ).executeAsList().forEach { 71 | updateLastUpdateAtUnixImpl( 72 | serverId = serverId, 73 | feedId = it 74 | ) 75 | } 76 | } 77 | else { 78 | updateLastUpdateAtUnixImpl( 79 | serverId = serverId, 80 | feedId = feedId 81 | ) 82 | } 83 | } 84 | 85 | fun List.toFeedIdList() = map { it.feedId } -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/Me.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database 2 | 3 | fun MeQueries.upsert( 4 | me: Me 5 | ) = transaction { 6 | insert( 7 | serverId = me.serverId, 8 | userId = me.userId, 9 | meIsAdmin = me.meIsAdmin, 10 | meLanguage = me.meLanguage, 11 | meLastLoginAt = me.meLastLoginAt, 12 | meTheme = me.meTheme, 13 | meTimeZone = me.meTimeZone, 14 | meEntrySortingDirection = me.meEntrySortingDirection 15 | ) 16 | update( 17 | serverId = me.serverId, 18 | userId = me.userId, 19 | meIsAdmin = me.meIsAdmin, 20 | meLanguage = me.meLanguage, 21 | meLastLoginAt = me.meLastLoginAt, 22 | meTheme = me.meTheme, 23 | meTimeZone = me.meTimeZone, 24 | meEntrySortingDirection = me.meEntrySortingDirection 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database 2 | 3 | import com.constantin.microflux.data.ServerUrl 4 | 5 | fun ServerQueries.insert(serverUrl: ServerUrl) = insertImpl(serverUrl = serverUrl).run { 6 | selectForId(serverUrl).executeAsOne() 7 | } -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/User.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database 2 | 3 | import com.constantin.microflux.data.ServerId 4 | import com.constantin.microflux.data.UserId 5 | 6 | fun UserQueries.selectUser(serverId: ServerId, userId: UserId): Account { 7 | transaction { 8 | unSelectAllImpl() 9 | makeSelectedImpl(serverId, userId) 10 | } 11 | return selectCurent().executeAsOne() 12 | } 13 | 14 | 15 | fun UserQueries.upsert( 16 | user: User 17 | ) = transaction { 18 | insert( 19 | userName = user.userName, 20 | userPassword = user.userPassword, 21 | userSelected = user.userSelected, 22 | serverId = user.serverId, 23 | userId = user.userId 24 | ) 25 | update( 26 | userName = user.userName, 27 | userPassword = user.userPassword, 28 | userSelected = user.userSelected, 29 | serverId = user.serverId, 30 | userId = user.userId 31 | ) 32 | } 33 | 34 | fun UserQueries.deleteCurrentAndSwitch(): Account? { 35 | var account: Account? = null 36 | transaction { 37 | deleteSelected() 38 | selectAll().executeAsList().let { 39 | account = if (it.isNotEmpty()) selectUser( 40 | serverId = it[0].serverId, 41 | userId = it[0].userId 42 | ) else null 43 | } 44 | } 45 | return account 46 | } 47 | -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/Work.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database 2 | 3 | import com.constantin.microflux.data.EntryId 4 | import com.constantin.microflux.data.ServerId 5 | import com.constantin.microflux.data.UserId 6 | import com.constantin.microflux.data.WorkType 7 | 8 | fun WorkQueries.insertAll( 9 | serverId: ServerId, 10 | userId: UserId, 11 | entryIds: List, 12 | workType: WorkType 13 | ) = transaction { 14 | entryIds.forEach { 15 | insert ( 16 | serverId = serverId, 17 | userId = userId, 18 | entryId = it, 19 | workType = workType 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/util/Error.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database.util 2 | 3 | import android.database.sqlite.SQLiteConstraintException 4 | import android.database.sqlite.SQLiteOutOfMemoryException 5 | import com.constantin.microflux.data.Result 6 | import com.constantin.microflux.database.NoUserException 7 | 8 | inline fun error( 9 | block: () -> Unit 10 | ) = try { 11 | block() 12 | Result.success() 13 | } catch (e: NoUserException) { 14 | Result.Error.DatabaseError.NoUserError 15 | } catch (e: SQLiteConstraintException) { 16 | Result.Error.DatabaseError.InsertionError 17 | } catch (e: SQLiteOutOfMemoryException) { 18 | Result.Error.DatabaseError.NoMemoryError 19 | } -------------------------------------------------------------------------------- /database/src/main/java/com/constantin/microflux/database/util/SqlDelight.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.database.util 2 | 3 | import com.squareup.sqldelight.Query 4 | import com.squareup.sqldelight.runtime.coroutines.asFlow 5 | import com.squareup.sqldelight.runtime.coroutines.mapToList 6 | import com.squareup.sqldelight.runtime.coroutines.mapToOne 7 | import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull 8 | import kotlinx.coroutines.flow.distinctUntilChanged 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | fun Query.flowMapToList(context: CoroutineContext) = 12 | asFlow().mapToList(context).distinctUntilChanged() 13 | 14 | fun Query.flowMapToOne(context: CoroutineContext) = 15 | asFlow().mapToOne(context).distinctUntilChanged() 16 | 17 | fun Query.flowMapToOneOrNull(context: CoroutineContext) = 18 | asFlow().mapToOneOrNull(context).distinctUntilChanged() -------------------------------------------------------------------------------- /database/src/main/sqldelight/com/constantin/microflux/database/Category.sq: -------------------------------------------------------------------------------- 1 | import com.constantin.microflux.data.CategoryId; 2 | import com.constantin.microflux.data.CategoryTitle; 3 | import com.constantin.microflux.data.ServerId; 4 | import com.constantin.microflux.data.UserId; 5 | 6 | CREATE TABLE category( 7 | serverId INTEGER AS ServerId NOT NULL, 8 | categoryId INTEGER AS CategoryId NOT NULL, 9 | userId INTEGER AS UserId NOT NULL, 10 | categoryTitle TEXT AS CategoryTitle NOT NULL COLLATE NOCASE, 11 | PRIMARY KEY (serverId, categoryId), 12 | FOREIGN KEY (serverId, userId) REFERENCES user(serverId, userId) 13 | ON DELETE CASCADE 14 | ); 15 | 16 | selectAll: 17 | SELECT category.* 18 | FROM category 19 | INNER JOIN user ON category.serverId = user.serverId AND category.userId = user.userId 20 | WHERE user.serverId = ? 21 | AND user.userId = ? 22 | ORDER BY category.categoryTitle ASC; 23 | 24 | select: 25 | SELECT * 26 | FROM category 27 | WHERE category.serverId = ? 28 | AND category.categoryId = ? 29 | LIMIT 1; 30 | 31 | clearAll: 32 | WITH TO_CLEAR AS 33 | ( 34 | SELECT category.serverId, category.categoryId 35 | FROM category 36 | INNER JOIN user ON category.serverId = user.serverId AND category.userId = user.userId 37 | WHERE user.serverId = ? 38 | AND user.userId = ? 39 | AND category.categoryId NOT IN ? 40 | ) 41 | DELETE FROM category 42 | WHERE category.serverId = (SELECT serverId FROM TO_CLEAR LIMIT 1) 43 | AND category.categoryId IN (SELECT categoryId FROM TO_CLEAR); 44 | 45 | delete: 46 | DELETE FROM category 47 | WHERE category.serverId = ? 48 | AND category.categoryId = ?; 49 | 50 | insert: 51 | INSERT OR IGNORE INTO category( 52 | serverId, 53 | categoryId, 54 | userId, 55 | categoryTitle 56 | ) 57 | VALUES (?, ?, ?, ?); 58 | 59 | update: 60 | UPDATE category 61 | SET categoryTitle = ? 62 | WHERE serverId = ? 63 | AND categoryId = ?; -------------------------------------------------------------------------------- /database/src/main/sqldelight/com/constantin/microflux/database/Me.sq: -------------------------------------------------------------------------------- 1 | import com.constantin.microflux.data.MeEntrySortingDirection; 2 | import com.constantin.microflux.data.MeIsAdmin; 3 | import com.constantin.microflux.data.MeLanguage; 4 | import com.constantin.microflux.data.MeLastLoginAt; 5 | import com.constantin.microflux.data.MeTheme; 6 | import com.constantin.microflux.data.MeTimeZone; 7 | import com.constantin.microflux.data.ServerId; 8 | import com.constantin.microflux.data.UserId; 9 | 10 | CREATE TABLE me( 11 | serverId INTEGER AS ServerId NOT NULL, 12 | userId INTEGER AS UserId NOT NULL, 13 | meIsAdmin INTEGER AS MeIsAdmin NOT NULL, 14 | meLanguage TEXT AS MeLanguage NOT NULL, 15 | meLastLoginAt TEXT AS MeLastLoginAt NOT NULL, 16 | meTheme TEXT AS MeTheme NOT NULL, 17 | meTimeZone TEXT AS MeTimeZone NOT NULL, 18 | meEntrySortingDirection TEXT AS MeEntrySortingDirection NOT NULL, 19 | PRIMARY KEY (serverId, userId), 20 | FOREIGN KEY (serverId, userId) REFERENCES user(serverId, userId) 21 | ON DELETE CASCADE 22 | ); 23 | 24 | 25 | select: 26 | SELECT me.* 27 | FROM me 28 | INNER JOIN user ON me.serverId = user.serverId AND me.userId = user.userId 29 | WHERE user.serverId = ? 30 | AND user.userId = ? 31 | LIMIT 1; 32 | 33 | delete: 34 | DELETE FROM me 35 | WHERE me.serverId = ? 36 | AND me.userId = ?; 37 | 38 | insert: 39 | INSERT OR IGNORE INTO me( 40 | serverId, 41 | userId, 42 | meIsAdmin, 43 | meLanguage, 44 | meLastLoginAt, 45 | meTheme, 46 | meTimeZone, 47 | meEntrySortingDirection 48 | ) 49 | VALUES (?, ?, ?, ?, ?, ?, ?, ?); 50 | 51 | update: 52 | UPDATE me 53 | SET 54 | meIsAdmin = ?, 55 | meLanguage = ?, 56 | meLastLoginAt = ?, 57 | meTheme = ?, 58 | meTimeZone = ?, 59 | meEntrySortingDirection = ? 60 | WHERE serverId = ? 61 | AND userId = ?; -------------------------------------------------------------------------------- /database/src/main/sqldelight/com/constantin/microflux/database/Server.sq: -------------------------------------------------------------------------------- 1 | import com.constantin.microflux.data.ServerId; 2 | import com.constantin.microflux.data.ServerUrl; 3 | 4 | CREATE TABLE server( 5 | serverId INTEGER AS ServerId NOT NULL UNIQUE PRIMARY KEY AUTOINCREMENT, 6 | serverUrl TEXT AS ServerUrl NOT NULL UNIQUE 7 | ); 8 | 9 | selectAll: 10 | SELECT server.* 11 | FROM server; 12 | 13 | select: 14 | SELECT server.* 15 | FROM server 16 | WHERE server.serverId = ? 17 | LIMIT 1; 18 | 19 | selectForId: 20 | SELECT server.serverId 21 | FROM server 22 | WHERE server.serverUrl = ? 23 | LIMIT 1; 24 | 25 | delete: 26 | DELETE FROM server 27 | WHERE server.serverId = ?; 28 | 29 | insertImpl: 30 | INSERT OR IGNORE INTO server( 31 | serverUrl 32 | ) 33 | VALUES (?); 34 | 35 | update: 36 | UPDATE server 37 | SET serverUrl = ? 38 | WHERE serverId = ?; -------------------------------------------------------------------------------- /database/src/main/sqldelight/com/constantin/microflux/database/Settings.sq: -------------------------------------------------------------------------------- 1 | import com.constantin.microflux.data.ServerId; 2 | import com.constantin.microflux.data.UserId; 3 | import com.constantin.microflux.data.SettingsTheme; 4 | import com.constantin.microflux.data.SettingsAllowImagePreview; 5 | 6 | CREATE TABLE settings( 7 | serverId INTEGER AS ServerId NOT NULL, 8 | userId INTEGER AS UserId NOT NULL, 9 | settingsTheme INTEGER AS SettingsTheme NOT NULL, 10 | settingsAllowImagePreview INTEGER AS SettingsAllowImagePreview NOT NULL, 11 | PRIMARY KEY (serverId, userId), 12 | FOREIGN KEY (serverId, userId) REFERENCES user(serverId, userId) 13 | ON DELETE CASCADE 14 | ); 15 | 16 | select: 17 | SELECT settings.* 18 | FROM settings 19 | INNER JOIN user ON settings.serverId = user.serverId AND settings.userId = user.userId 20 | WHERE user.serverId = ? 21 | AND user.userId = ? 22 | LIMIT 1; 23 | 24 | delete: 25 | DELETE FROM settings 26 | WHERE settings.serverId = ? 27 | AND settings.userId = ?; 28 | 29 | theme: 30 | SELECT settings.settingsTheme 31 | FROM settings 32 | INNER JOIN user ON settings.serverId = user.serverId AND settings.userId = user.userId 33 | WHERE user.serverId = ? 34 | AND user.userId = ? 35 | LIMIT 1; 36 | 37 | allowImagePreview: 38 | SELECT settings.settingsAllowImagePreview 39 | FROM settings 40 | INNER JOIN user ON settings.serverId = user.serverId AND settings.userId = user.userId 41 | WHERE user.serverId = ? 42 | AND user.userId = ? 43 | LIMIT 1; 44 | 45 | insert: 46 | INSERT OR IGNORE INTO settings( 47 | serverId, 48 | userId, 49 | settingsTheme, 50 | settingsAllowImagePreview 51 | ) 52 | VALUES ?; 53 | 54 | updateSettingsTheme: 55 | UPDATE settings 56 | SET settingsTheme = ? 57 | WHERE serverId = ? 58 | AND userId = ?; 59 | 60 | updateSettingsAllowImagePreview: 61 | UPDATE settings 62 | SET settingsAllowImagePreview = ? 63 | WHERE serverId = ? 64 | AND userId = ?; -------------------------------------------------------------------------------- /database/src/main/sqldelight/com/constantin/microflux/database/User.sq: -------------------------------------------------------------------------------- 1 | import com.constantin.microflux.data.ServerId; 2 | import com.constantin.microflux.data.UserFirstTimeRun; 3 | import com.constantin.microflux.data.UserId; 4 | import com.constantin.microflux.data.UserName; 5 | import com.constantin.microflux.data.UserPassword; 6 | import com.constantin.microflux.data.UserSelected; 7 | 8 | CREATE TABLE user( 9 | serverId INTEGER AS ServerId NOT NULL, 10 | userId INTEGER AS UserId NOT NULL, 11 | userName TEXT AS UserName NOT NULL, 12 | userPassword BLOB AS UserPassword NOT NULL, 13 | userSelected INTEGER AS UserSelected NOT NULL, 14 | userFirstTimeRun INTEGER AS UserFirstTimeRun NOT NULL DEFAULT 1, 15 | PRIMARY KEY (serverId, userId), 16 | FOREIGN KEY (serverId) REFERENCES server(serverId) 17 | ON DELETE CASCADE 18 | ); 19 | 20 | CREATE VIEW account AS 21 | SELECT server.serverUrl, user.* 22 | FROM user 23 | INNER JOIN server ON user.serverId = server.serverId; 24 | 25 | selectAll: 26 | SELECT account.* 27 | FROM account 28 | ORDER BY userName ASC; 29 | 30 | selectCurent: 31 | SELECT account.* 32 | FROM account 33 | WHERE account.userSelected = 1 34 | LIMIT 1; 35 | 36 | selectNonCurent: 37 | SELECT account.* 38 | FROM account 39 | WHERE account.userSelected = 0; 40 | 41 | select: 42 | SELECT account.* 43 | FROM account 44 | WHERE account.serverId = ? 45 | AND account.userId = ? 46 | LIMIT 1; 47 | 48 | clearAll: 49 | DELETE FROM user; 50 | 51 | deleteSelected: 52 | DELETE FROM user 53 | WHERE user.userSelected = 1; 54 | 55 | delete: 56 | DELETE FROM user 57 | WHERE user.serverId = ? 58 | AND user.userId = ?; 59 | 60 | insert: 61 | INSERT OR IGNORE INTO user( 62 | serverId, 63 | userId, 64 | userName, 65 | userPassword, 66 | userSelected 67 | ) 68 | VALUES (?, ?, ?, ?, ?); 69 | 70 | update: 71 | UPDATE user 72 | SET userName = ?, 73 | userPassword = ?, 74 | userSelected = ? 75 | WHERE serverId = ? 76 | AND userId = ?; 77 | 78 | makeSelectedImpl: 79 | UPDATE user 80 | SET userSelected = 1 81 | WHERE serverId = ? 82 | AND userId = ?; 83 | 84 | unSelectAllImpl: 85 | UPDATE user 86 | SET userSelected = 0 87 | WHERE userSelected = 1; 88 | 89 | selectUserFirstTimeRun: 90 | SELECT user.userFirstTimeRun 91 | FROM user 92 | WHERE serverId = ? 93 | AND userId = ?; 94 | 95 | setUserRanFirstTime: 96 | UPDATE user 97 | SET userFirstTimeRun = 0 98 | WHERE serverId = ? 99 | AND userId = ?; -------------------------------------------------------------------------------- /database/src/main/sqldelight/com/constantin/microflux/database/Work.sq: -------------------------------------------------------------------------------- 1 | import com.constantin.microflux.data.ServerId; 2 | import com.constantin.microflux.data.UserId; 3 | import com.constantin.microflux.data.EntryId; 4 | import com.constantin.microflux.data.WorkType; 5 | 6 | CREATE TABLE work( 7 | serverId INTEGER AS ServerId NOT NULL, 8 | userId INTEGER AS UserId NOT NULL, 9 | entryId INTEGER AS EntryId NOT NULL, 10 | workType INTEGER AS WorkType NOT NULL, 11 | PRIMARY KEY (serverId, userId, entryId), 12 | FOREIGN KEY (serverId, userId) REFERENCES user(serverId, userId) 13 | ON DELETE CASCADE 14 | ); 15 | 16 | selectAll: 17 | SELECT work.* 18 | FROM work 19 | INNER JOIN user ON work.serverId = user.serverId AND work.userId = user.userId 20 | WHERE user.serverId = ? 21 | AND user.userId = ?; 22 | 23 | insert: 24 | INSERT OR IGNORE INTO work( 25 | serverId, 26 | userId, 27 | entryId, 28 | workType 29 | ) 30 | VALUES (?, ?, ?, ?); 31 | 32 | delete: 33 | DELETE FROM work 34 | WHERE work.serverId = ? 35 | AND work.entryId = ?; -------------------------------------------------------------------------------- /encryption/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /encryption/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("kotlin-android-extensions") 5 | id("kotlin-kapt") 6 | } 7 | 8 | android { 9 | compileSdkVersion(30) 10 | defaultConfig { 11 | minSdkVersion(24) 12 | targetSdkVersion(30) 13 | versionCode = 1 14 | versionName = "1.0" 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | getByName("release") { 19 | isMinifyEnabled = false 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 22 | ) 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_1_8 27 | targetCompatibility = JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | @Suppress("SuspiciousCollectionReassignment") 32 | freeCompilerArgs += listOf( 33 | "-progressive", 34 | "-XXLanguage:+NewInference", 35 | "-XXLanguage:+InlineClasses", 36 | "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", 37 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 38 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 39 | ) 40 | } 41 | packagingOptions { 42 | pickFirst("META-INF/*.kotlin_module") 43 | } 44 | } 45 | 46 | repositories { 47 | jcenter() 48 | } 49 | 50 | dependencies { 51 | // Modules 52 | implementation(project(":data")) 53 | // Tink for encryption 54 | implementation ("com.google.crypto.tink:tink-android:1.4.0-rc2") 55 | } -------------------------------------------------------------------------------- /encryption/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /encryption/src/main/java/com/constantin/microflux/encryption/AesEncryption.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.encryption 2 | 3 | import android.content.Context 4 | import com.google.crypto.tink.Aead 5 | import com.google.crypto.tink.aead.AesGcmKeyManager 6 | import com.google.crypto.tink.config.TinkConfig 7 | import com.google.crypto.tink.integration.android.AndroidKeysetManager 8 | import java.nio.charset.StandardCharsets 9 | 10 | 11 | class AesEncryption(context: Context) { 12 | companion object { 13 | private const val PREF_FILE_NAME = "microflux_pref" 14 | private const val TINK_KEY_SET_NAME = "microflux_keyset" 15 | private const val MASTER_KEY_URI = "android-keystore://microflux_master_key" 16 | private const val ASSOCIATED_DATA = "microflux_associated_data" 17 | } 18 | 19 | init { 20 | TinkConfig.register() 21 | } 22 | 23 | private val aead = AndroidKeysetManager.Builder() 24 | .withSharedPref(context, 25 | TINK_KEY_SET_NAME, 26 | PREF_FILE_NAME 27 | ) 28 | .withKeyTemplate(AesGcmKeyManager.aes256GcmTemplate()) 29 | .withMasterKeyUri(MASTER_KEY_URI) 30 | .build() 31 | .keysetHandle 32 | .getPrimitive(Aead::class.java) 33 | 34 | fun encryptData(data: String): ByteArray = aead.encrypt( 35 | data.toByteArray(StandardCharsets.UTF_8), 36 | ASSOCIATED_DATA.toByteArray(StandardCharsets.UTF_8) 37 | ) 38 | 39 | fun decryptData(data: ByteArray): String = String( 40 | aead.decrypt(data, ASSOCIATED_DATA.toByteArray(StandardCharsets.UTF_8)), 41 | StandardCharsets.UTF_8 42 | ) 43 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | kotlin.code.style=official 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ConstantinCezarBegu/Microflux/84e8a146cd27096aa41dd7f1a446e8bd974c8d01/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jul 11 00:20:53 EDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https://services.gradle.org/distributions/gradle-6.7-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /network/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /network/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("kotlin-android-extensions") 5 | id("kotlin-kapt") 6 | id("org.jetbrains.kotlin.plugin.serialization") 7 | } 8 | 9 | android { 10 | compileSdkVersion(30) 11 | defaultConfig { 12 | minSdkVersion(24) 13 | targetSdkVersion(30) 14 | versionCode = 1 15 | versionName = "1.0" 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | getByName("release") { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 23 | ) 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | @Suppress("SuspiciousCollectionReassignment") 33 | freeCompilerArgs += listOf( 34 | "-progressive", 35 | "-XXLanguage:+NewInference", 36 | "-XXLanguage:+InlineClasses", 37 | "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", 38 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 39 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview", 40 | "-Xopt-in=kotlin.RequiresOptIn" 41 | ) 42 | } 43 | packagingOptions { 44 | pickFirst("META-INF/*.kotlin_module") 45 | } 46 | } 47 | 48 | repositories { 49 | jcenter() 50 | } 51 | 52 | dependencies { 53 | // Modules 54 | implementation(project(":data")) 55 | // Ktor 56 | implementation("io.ktor:ktor-client-android:1.4.0") 57 | implementation("io.ktor:ktor-client-auth-jvm:1.4.0") 58 | implementation("io.ktor:ktor-client-serialization-jvm:1.4.1") 59 | } -------------------------------------------------------------------------------- /network/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/CategoryNetwork.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network 2 | 3 | import com.constantin.microflux.data.Result 4 | import com.constantin.microflux.network.data.* 5 | import com.constantin.microflux.network.util.* 6 | import io.ktor.client.HttpClient 7 | import io.ktor.http.ContentType 8 | import io.ktor.http.contentType 9 | 10 | class CategoryNetwork( 11 | private val client: HttpClient 12 | ) { 13 | 14 | companion object { 15 | const val CATEGORY_URL = "/v1/categories" 16 | } 17 | 18 | suspend fun get( 19 | accountUrl: AccountUrl, 20 | accountUsername: AccountUsername, 21 | accountPassword: AccountPassword 22 | ): Result> = client.get( 23 | urlString = "${accountUrl}${CATEGORY_URL}", 24 | auth = Credentials.basic( 25 | userName = accountUsername, 26 | password = accountPassword 27 | ) 28 | ) 29 | 30 | suspend fun add( 31 | accountUrl: AccountUrl, 32 | accountUsername: AccountUsername, 33 | accountPassword: AccountPassword, 34 | categoryRequest: CategoryRequest 35 | ): Result = client.post( 36 | urlString = "${accountUrl}${CATEGORY_URL}", 37 | auth = Credentials.basic( 38 | userName = accountUsername, 39 | password = accountPassword 40 | ) 41 | ) { 42 | contentType(ContentType.Application.Json) 43 | body = categoryRequest 44 | } 45 | 46 | suspend fun update( 47 | accountUrl: AccountUrl, 48 | accountUsername: AccountUsername, 49 | accountPassword: AccountPassword, 50 | categoryId: CategoryId, 51 | categoryResponse: CategoryResponse 52 | ): Result = client.put( 53 | urlString = "${accountUrl}${CATEGORY_URL}/${categoryId}", 54 | auth = Credentials.basic( 55 | userName = accountUsername, 56 | password = accountPassword 57 | ) 58 | ) { 59 | contentType(ContentType.Application.Json) 60 | body = categoryResponse 61 | } 62 | 63 | suspend fun delete( 64 | accountUrl: AccountUrl, 65 | accountUsername: AccountUsername, 66 | accountPassword: AccountPassword, 67 | categoryId: CategoryId 68 | ): Result = client.delete( 69 | urlString = "${accountUrl}${CATEGORY_URL}/${categoryId}", 70 | auth = Credentials.basic( 71 | userName = accountUsername, 72 | password = accountPassword 73 | ) 74 | ) 75 | } -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/FeedNetwork.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network 2 | 3 | import com.constantin.microflux.data.Result 4 | import com.constantin.microflux.network.data.* 5 | import com.constantin.microflux.network.util.* 6 | import io.ktor.client.HttpClient 7 | import io.ktor.http.ContentType 8 | import io.ktor.http.contentType 9 | 10 | class FeedNetwork( 11 | private val client: HttpClient 12 | ) { 13 | 14 | companion object { 15 | const val FEED_URL = "/v1/feeds" 16 | } 17 | 18 | suspend fun get( 19 | accountUrl: AccountUrl, 20 | accountUsername: AccountUsername, 21 | accountPassword: AccountPassword 22 | ): Result> = client.get( 23 | urlString = "${accountUrl}${FEED_URL}", 24 | auth = Credentials.basic( 25 | userName = accountUsername, 26 | password = accountPassword 27 | ) 28 | ) 29 | 30 | suspend fun getFeed( 31 | accountUrl: AccountUrl, 32 | accountUsername: AccountUsername, 33 | accountPassword: AccountPassword, 34 | feedId: FeedId 35 | ): Result = client.get( 36 | urlString = "${accountUrl}${FEED_URL}/${feedId}", 37 | auth = Credentials.basic( 38 | userName = accountUsername, 39 | password = accountPassword 40 | ) 41 | ) 42 | 43 | suspend fun getIcon( 44 | accountUrl: AccountUrl, 45 | accountUsername: AccountUsername, 46 | accountPassword: AccountPassword, 47 | feedId: FeedId 48 | ): Result = client.get( 49 | urlString = "${accountUrl}${FEED_URL}/${feedId}/icon", 50 | auth = Credentials.basic( 51 | userName = accountUsername, 52 | password = accountPassword 53 | ) 54 | ) 55 | 56 | suspend fun add( 57 | accountUrl: AccountUrl, 58 | accountUsername: AccountUsername, 59 | accountPassword: AccountPassword, 60 | createFeedRequest: CreateFeedRequest 61 | ): Result = client.post( 62 | urlString = "${accountUrl}${FEED_URL}", 63 | auth = Credentials.basic( 64 | userName = accountUsername, 65 | password = accountPassword 66 | ) 67 | ) { 68 | contentType(ContentType.Application.Json) 69 | body = createFeedRequest 70 | } 71 | 72 | suspend fun update( 73 | accountUrl: AccountUrl, 74 | accountUsername: AccountUsername, 75 | accountPassword: AccountPassword, 76 | feedId: FeedId, 77 | updateFeedRequest: UpdateFeedRequest 78 | ): Result = client.put( 79 | urlString = "${accountUrl}${FEED_URL}/${feedId}", 80 | auth = Credentials.basic( 81 | userName = accountUsername, 82 | password = accountPassword 83 | ) 84 | ) { 85 | contentType(ContentType.Application.Json) 86 | body = updateFeedRequest 87 | } 88 | 89 | suspend fun delete( 90 | accountUrl: AccountUrl, 91 | accountUsername: AccountUsername, 92 | accountPassword: AccountPassword, 93 | feedId: FeedId 94 | ): Result = client.delete( 95 | urlString = "${accountUrl}${FEED_URL}/${feedId}", 96 | auth = Credentials.basic( 97 | userName = accountUsername, 98 | password = accountPassword 99 | ) 100 | ) 101 | } -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/MeNetwork.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network 2 | 3 | import com.constantin.microflux.data.Result 4 | import com.constantin.microflux.network.data.AccountPassword 5 | import com.constantin.microflux.network.data.AccountUrl 6 | import com.constantin.microflux.network.data.AccountUsername 7 | import com.constantin.microflux.network.data.MeResponse 8 | import com.constantin.microflux.network.util.Credentials 9 | import com.constantin.microflux.network.util.get 10 | import io.ktor.client.HttpClient 11 | 12 | class MeNetwork( 13 | private val client: HttpClient 14 | ) { 15 | 16 | companion object { 17 | const val ME_URL = "/v1/me" 18 | } 19 | 20 | suspend fun get( 21 | accountUrl: AccountUrl, 22 | accountUsername: AccountUsername, 23 | accountPassword: AccountPassword 24 | ): Result { 25 | return client.get( 26 | urlString = "${accountUrl}${ME_URL}", 27 | auth = Credentials.basic( 28 | userName = accountUsername, 29 | password = accountPassword 30 | ) 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/MinifluxService.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.engine.HttpClientEngineConfig 5 | import io.ktor.client.engine.HttpClientEngineFactory 6 | import io.ktor.client.features.* 7 | import io.ktor.client.features.json.JsonFeature 8 | import io.ktor.client.features.json.serializer.KotlinxSerializer 9 | import kotlinx.serialization.json.Json 10 | import kotlinx.serialization.json.JsonConfiguration 11 | 12 | class MinifluxService( 13 | engine: HttpClientEngineFactory 14 | ) { 15 | private val client = HttpClient(engine) { 16 | install(JsonFeature) { 17 | serializer = KotlinxSerializer(Json { ignoreUnknownKeys = true }) 18 | } 19 | HttpResponseValidator { 20 | validateResponse { response -> 21 | val statusCode = response.status.value 22 | 23 | when (statusCode) { 24 | in 300..399 -> throw RedirectResponseException(response) 25 | in 400..499 -> throw ClientRequestException(response) 26 | in 500..599 -> throw ServerResponseException(response) 27 | } 28 | 29 | if (statusCode >= 600) { 30 | throw ResponseException(response) 31 | } 32 | } 33 | } 34 | } 35 | 36 | val entry = EntryNetwork(client = client) 37 | val feed = FeedNetwork(client = client) 38 | val category = CategoryNetwork(client = client) 39 | val me = MeNetwork(client = client) 40 | } -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/data/Account.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.data 2 | 3 | typealias AccountUrl = String 4 | typealias AccountUsername = String 5 | typealias AccountPassword = String -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/data/Category.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | typealias CategoryId = Long 7 | typealias CategoryTitle = String 8 | 9 | @Serializable 10 | data class CategoryResponse( 11 | @SerialName("id") 12 | val id: CategoryId, 13 | @SerialName("title") 14 | val title: CategoryTitle, 15 | @SerialName("user_id") 16 | val userId: MeUserId 17 | ) 18 | 19 | @Serializable 20 | data class CategoryRequest( 21 | @SerialName("title") 22 | val title: CategoryTitle 23 | ) -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/data/Entry.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | typealias EntryId = Long 7 | typealias EntryTitle = String 8 | typealias EntryUrl = String 9 | typealias EntryCommentUrl = String 10 | typealias EntryAuthor = String 11 | typealias EntryContent = String 12 | typealias EntryHash = String 13 | typealias EntryPublishedAt = String 14 | typealias EntryStatus = String 15 | typealias EntryStarred = Boolean 16 | typealias EntryListTotal = Long 17 | typealias EntrySearch = String 18 | typealias EntryAfter = String 19 | 20 | @Serializable 21 | data class EntryResponse( 22 | @SerialName("id") 23 | val id: EntryId, 24 | @SerialName("user_id") 25 | val userId: MeUserId, 26 | @SerialName("feed_id") 27 | val feedId: FeedId, 28 | @SerialName("title") 29 | val title: EntryTitle, 30 | @SerialName("url") 31 | val url: EntryUrl, 32 | @SerialName("comments_url") 33 | val commentUrl: EntryCommentUrl, 34 | @SerialName("author") 35 | val author: EntryAuthor, 36 | @SerialName("content") 37 | val content: EntryContent, 38 | @SerialName("hash") 39 | val hash: EntryHash, 40 | @SerialName("published_at") 41 | val publishedAt: EntryPublishedAt, 42 | @SerialName("status") 43 | val status: EntryStatus, 44 | @SerialName("starred") 45 | val starred: EntryStarred, 46 | @SerialName("feed") 47 | val feed: FeedResponse 48 | ) 49 | 50 | @Serializable 51 | data class EntryListResponse( 52 | @SerialName("total") 53 | val total: EntryListTotal, 54 | @SerialName("entries") 55 | val entryList: List 56 | ) { 57 | companion object { 58 | val EMPTY = EntryListResponse( 59 | total = 0, 60 | entryList = listOf() 61 | ) 62 | } 63 | } 64 | 65 | @Serializable 66 | data class UpdateEntryStatusRequest( 67 | @SerialName("entry_ids") 68 | val entryIds: List, 69 | @SerialName("status") 70 | val status: EntryStatus 71 | ) -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/data/Me.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.data 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | typealias MeUserId = Long 7 | typealias MeUsername = String 8 | typealias MeIsAdmin = Boolean 9 | typealias MeTheme = String 10 | typealias MeLanguage = String 11 | typealias MeTimezone = String 12 | typealias MeEntrySortingDirection = String 13 | typealias MeLastLoginAt = String 14 | 15 | @Serializable 16 | data class MeResponse( 17 | @SerialName("id") 18 | val id: MeUserId, 19 | @SerialName("username") 20 | val username: MeUsername, 21 | @SerialName("is_admin") 22 | val isAdmin: MeIsAdmin, 23 | @SerialName("theme") 24 | val theme: MeTheme, 25 | @SerialName("language") 26 | val language: MeLanguage, 27 | @SerialName("timezone") 28 | val timezone: MeTimezone, 29 | @SerialName("entry_sorting_direction") 30 | val entrySortingDirection: MeEntrySortingDirection, 31 | @SerialName("last_login_at") 32 | val lastLoginAt: MeLastLoginAt 33 | ) -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/util/Credentials.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.util 2 | 3 | import android.util.Base64 4 | 5 | class Credentials { 6 | companion object { 7 | fun basic(userName: String, password: String) = "${userName}:${password}".run { 8 | Base64.encodeToString( 9 | toByteArray(charset("UTF-8")), 10 | Base64.NO_WRAP 11 | ).run { 12 | "Basic $this" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/util/Error.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.util 2 | 3 | import com.constantin.microflux.data.Result 4 | import io.ktor.client.features.ClientRequestException 5 | import io.ktor.client.features.RedirectResponseException 6 | import io.ktor.client.features.ResponseException 7 | import io.ktor.client.features.ServerResponseException 8 | import io.ktor.utils.io.errors.IOException 9 | 10 | inline fun error( 11 | block: () -> T 12 | )= try { 13 | val blockVar = block() 14 | Result.success(blockVar) 15 | } catch (e: ClientRequestException) { 16 | when (e.response?.status?.value) { 17 | 401 -> Result.Error.NetworkError.AuthorizationError 18 | 404 -> Result.Error.NetworkError.ServerUrlError 19 | else -> Result.Error.NetworkError.ConnectivityError 20 | } 21 | } catch (e: RedirectResponseException) { 22 | Result.Error.NetworkError.RedirectResponseError 23 | } catch (e: ServerResponseException) { 24 | Result.Error.NetworkError.ServerResponseError 25 | } catch (e: ResponseException) { 26 | Result.Error.NetworkError.ResponseError 27 | } catch (e: IOException) { 28 | Result.Error.NetworkError.IOError 29 | } -------------------------------------------------------------------------------- /network/src/main/java/com/constantin/microflux/network/util/Ktor.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.network.util 2 | 3 | import com.constantin.microflux.data.Result 4 | import io.ktor.client.HttpClient 5 | import io.ktor.client.request.* 6 | import io.ktor.http.HttpHeaders 7 | import io.ktor.http.takeFrom 8 | 9 | suspend inline fun HttpClient.get( 10 | urlString: String, 11 | auth: String, 12 | crossinline block: HttpRequestBuilder.() -> Unit = {} 13 | ): Result = error { 14 | get { 15 | headers.append(HttpHeaders.Authorization, auth) 16 | url.takeFrom(urlString) 17 | block() 18 | } 19 | } 20 | 21 | suspend inline fun HttpClient.post( 22 | urlString: String, 23 | auth: String, 24 | crossinline block: HttpRequestBuilder.() -> Unit = {} 25 | ): Result = error { 26 | post { 27 | headers.append(HttpHeaders.Authorization, auth) 28 | url.takeFrom(urlString) 29 | block() 30 | } 31 | } 32 | 33 | suspend inline fun HttpClient.put( 34 | urlString: String, 35 | auth: String, 36 | crossinline block: HttpRequestBuilder.() -> Unit = {} 37 | ): Result = error { 38 | put { 39 | headers.append(HttpHeaders.Authorization, auth) 40 | url.takeFrom(urlString) 41 | block() 42 | } 43 | } 44 | 45 | suspend inline fun HttpClient.delete( 46 | urlString: String, 47 | auth: String, 48 | crossinline block: HttpRequestBuilder.() -> Unit = {} 49 | ): Result = error { 50 | delete { 51 | headers.append(HttpHeaders.Authorization, auth) 52 | url.takeFrom(urlString) 53 | block() 54 | } 55 | } -------------------------------------------------------------------------------- /repository/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /repository/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("kotlin-android-extensions") 5 | } 6 | 7 | android { 8 | compileSdkVersion(30) 9 | defaultConfig { 10 | minSdkVersion(24) 11 | targetSdkVersion(30) 12 | versionCode = 1 13 | versionName = "1.0" 14 | multiDexEnabled = true 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | getByName("release") { 19 | isMinifyEnabled = false 20 | proguardFiles( 21 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 22 | ) 23 | } 24 | } 25 | compileOptions { 26 | isCoreLibraryDesugaringEnabled = true 27 | sourceCompatibility = JavaVersion.VERSION_1_8 28 | targetCompatibility = JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | @Suppress("SuspiciousCollectionReassignment") 33 | freeCompilerArgs += listOf( 34 | "-progressive", 35 | "-XXLanguage:+NewInference", 36 | "-XXLanguage:+InlineClasses", 37 | "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", 38 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 39 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 40 | ) 41 | } 42 | packagingOptions { 43 | pickFirst("META-INF/*.kotlin_module") 44 | } 45 | } 46 | 47 | repositories { 48 | jcenter() 49 | } 50 | 51 | dependencies { 52 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.0.10") 53 | // Modules 54 | api(project(":network")) 55 | api(project(":database")) 56 | api(project(":data")) 57 | // Jsoup 58 | implementation("org.jsoup:jsoup:1.13.1") 59 | // Coroutines 60 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") 61 | } -------------------------------------------------------------------------------- /repository/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/ConstafluxRepository.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository 2 | 3 | import com.constantin.microflux.database.ConstafluxDatabase 4 | import com.constantin.microflux.network.MinifluxService 5 | import kotlin.coroutines.CoroutineContext 6 | 7 | class ConstafluxRepository( 8 | context: CoroutineContext, 9 | constafluxDatabase: ConstafluxDatabase, 10 | minifluxService: MinifluxService 11 | ) { 12 | 13 | private val workRepository = 14 | WorkRepository( 15 | context = context, 16 | constafluxDatabase = constafluxDatabase, 17 | minifluxService = minifluxService 18 | ) 19 | 20 | val accountRepository = 21 | AccountRepository( 22 | context = context, 23 | minifluxService = minifluxService, 24 | constafluxDatabase = constafluxDatabase 25 | ) 26 | 27 | val meRepository = 28 | MeRepository( 29 | context = context, 30 | minifluxService = minifluxService, 31 | constafluxDatabase = constafluxDatabase, 32 | getCurrentAccount = accountRepository::currentAccount 33 | ) 34 | 35 | val settingsRepository = 36 | SettingsRepository( 37 | context = context, 38 | constafluxDatabase = constafluxDatabase, 39 | getCurrentAccount = accountRepository::currentAccount 40 | ) 41 | 42 | val categoryRepository = 43 | CategoryRepository( 44 | context = context, 45 | minifluxService = minifluxService, 46 | constafluxDatabase = constafluxDatabase, 47 | getCurrentAccount = accountRepository::currentAccount 48 | ) 49 | 50 | val feedRepository = 51 | FeedRepository( 52 | context = context, 53 | minifluxService = minifluxService, 54 | constafluxDatabase = constafluxDatabase, 55 | getCurrentAccount = accountRepository::currentAccount, 56 | syncCategory = categoryRepository::fetch 57 | ) 58 | 59 | val entryRepository = 60 | EntryRepository( 61 | context = context, 62 | minifluxService = minifluxService, 63 | constafluxDatabase = constafluxDatabase, 64 | getCurrentAccount = accountRepository::currentAccount, 65 | syncEntry = workRepository::syncEntry, 66 | syncFeed = feedRepository::fetch 67 | 68 | ) 69 | 70 | val backGroundProcessRepository = 71 | NotificationRepository( 72 | constafluxDatabase = constafluxDatabase, 73 | accountRepository = accountRepository, 74 | categoryRepository = categoryRepository, 75 | feedRepository = feedRepository, 76 | entryRepository = entryRepository 77 | ) 78 | } -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/MeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository 2 | 3 | import com.constantin.microflux.data.Result 4 | import com.constantin.microflux.database.Account 5 | import com.constantin.microflux.database.ConstafluxDatabase 6 | import com.constantin.microflux.database.upsert 7 | import com.constantin.microflux.database.util.flowMapToOne 8 | import com.constantin.microflux.network.MinifluxService 9 | import com.constantin.microflux.repository.transformation.toMe 10 | import kotlinx.coroutines.withContext 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | class MeRepository( 14 | private val context: CoroutineContext, 15 | private val minifluxService: MinifluxService, 16 | private val constafluxDatabase: ConstafluxDatabase, 17 | private val getCurrentAccount: () -> Account 18 | ) { 19 | 20 | fun getMe( 21 | account: Account = getCurrentAccount() 22 | ) = constafluxDatabase.meQueries.select( 23 | serverId = account.serverId, 24 | userId = account.userId 25 | ).flowMapToOne(context) 26 | 27 | suspend fun fetch( 28 | account: Account = getCurrentAccount() 29 | ): Result = withContext(context) { 30 | val result = minifluxService.me.get( 31 | accountUrl = account.serverUrl.url, 32 | accountUsername = account.userName.name, 33 | accountPassword = account.userPassword.password 34 | ) 35 | if (result is Result.Success) { 36 | constafluxDatabase.meQueries.upsert( 37 | me = result.data.toMe( 38 | serverId = account.serverId, 39 | userId = account.userId 40 | ) 41 | ) 42 | Result.success() 43 | } else result.extractError() 44 | } 45 | } -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository 2 | 3 | import com.constantin.microflux.data.SettingsAllowImagePreview 4 | import com.constantin.microflux.data.SettingsTheme 5 | import com.constantin.microflux.database.Account 6 | import com.constantin.microflux.database.ConstafluxDatabase 7 | import com.constantin.microflux.database.util.flowMapToOne 8 | import com.constantin.microflux.database.util.flowMapToOneOrNull 9 | import kotlinx.coroutines.NonCancellable 10 | import kotlinx.coroutines.withContext 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | class SettingsRepository( 14 | private val context: CoroutineContext, 15 | private val constafluxDatabase: ConstafluxDatabase, 16 | private val getCurrentAccount: () -> Account 17 | ) { 18 | 19 | fun getTheme( 20 | account: Account = getCurrentAccount() 21 | ) = constafluxDatabase.settingsQueries.theme( 22 | serverId = account.serverId, 23 | userId = account.userId 24 | ).flowMapToOneOrNull(context) 25 | 26 | fun getSettings( 27 | account: Account = getCurrentAccount() 28 | ) = constafluxDatabase.settingsQueries.select( 29 | serverId = account.serverId, 30 | userId = account.userId 31 | ).flowMapToOne(context) 32 | 33 | suspend fun changeSettingsTheme( 34 | account: Account = getCurrentAccount(), 35 | settingsTheme: SettingsTheme 36 | ) { 37 | withContext(context + NonCancellable) { 38 | constafluxDatabase.settingsQueries.updateSettingsTheme( 39 | serverId = account.serverId, 40 | userId = account.userId, 41 | settingsTheme = settingsTheme 42 | ) 43 | } 44 | } 45 | 46 | suspend fun changeAllowImagePreview( 47 | account: Account = getCurrentAccount(), 48 | settingsAllowImagePreview: SettingsAllowImagePreview 49 | ) { 50 | withContext(context + NonCancellable) { 51 | constafluxDatabase.settingsQueries.updateSettingsAllowImagePreview( 52 | serverId = account.serverId, 53 | userId = account.userId, 54 | settingsAllowImagePreview = settingsAllowImagePreview 55 | ) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/WorkRepository.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository 2 | 3 | import com.constantin.microflux.data.* 4 | import com.constantin.microflux.database.Account 5 | import com.constantin.microflux.database.ConstafluxDatabase 6 | import com.constantin.microflux.database.toId 7 | import com.constantin.microflux.network.MinifluxService 8 | import com.constantin.microflux.util.forEachAsync 9 | import kotlinx.coroutines.NonCancellable 10 | import kotlinx.coroutines.withContext 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | class WorkRepository( 14 | private val context: CoroutineContext, 15 | private val minifluxService: MinifluxService, 16 | private val constafluxDatabase: ConstafluxDatabase 17 | ) { 18 | 19 | private fun getWork( 20 | account: Account 21 | ) = constafluxDatabase.workQueries.selectAll( 22 | serverId = account.serverId, 23 | userId = account.userId 24 | ).executeAsList() 25 | 26 | private fun deleteWork( 27 | serverId: ServerId, 28 | entryId: EntryId 29 | ) = constafluxDatabase.workQueries.delete( 30 | serverId = serverId, 31 | entryId = entryId 32 | ) 33 | 34 | suspend fun syncEntry( 35 | account: Account 36 | ): Result = withContext(context) { 37 | var result: Result = Result.success() 38 | 39 | getWork(account).forEachAsync { work -> 40 | val workResult = when (work.workType) { 41 | WorkType.STATUS_MARK_AS_UNREAD -> { 42 | minifluxService.entry.updateStatus( 43 | accountUrl = account.serverUrl.url, 44 | accountUsername = account.userName.name, 45 | accountPassword = account.userPassword.password, 46 | entryIds = listOf(work.entryId).toId(), 47 | status = EntryStatus.UN_READ.status 48 | ) 49 | } 50 | WorkType.STATUS_MARK_AS_READ -> { 51 | minifluxService.entry.updateStatus( 52 | accountUrl = account.serverUrl.url, 53 | accountUsername = account.userName.name, 54 | accountPassword = account.userPassword.password, 55 | entryIds = listOf(work.entryId).toId(), 56 | status = EntryStatus.READ.status 57 | ) 58 | } 59 | WorkType.STAR -> { 60 | minifluxService.entry.updateStarred( 61 | accountUrl = account.serverUrl.url, 62 | accountUsername = account.userName.name, 63 | accountPassword = account.userPassword.password, 64 | entryIds = listOf(work.entryId).toId() 65 | ) 66 | } 67 | else -> Result.success() 68 | } 69 | 70 | if (workResult is Result.Success) { 71 | withContext(NonCancellable) { 72 | deleteWork( 73 | serverId = work.serverId, 74 | entryId = work.entryId 75 | ) 76 | } 77 | } else { 78 | result = workResult 79 | } 80 | } 81 | result 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/transformation/Category.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.transformation 2 | 3 | 4 | import com.constantin.microflux.data.CategoryId 5 | import com.constantin.microflux.data.CategoryTitle 6 | import com.constantin.microflux.data.ServerId 7 | import com.constantin.microflux.data.UserId 8 | import com.constantin.microflux.database.Category 9 | import com.constantin.microflux.network.data.CategoryResponse 10 | 11 | fun Category.toCategoryResponse() = CategoryResponse( 12 | id = this.categoryId.id, 13 | userId = this.userId.id, 14 | title = this.categoryTitle.title 15 | ) 16 | 17 | 18 | fun CategoryResponse.toCategory(serverId: ServerId) = Category( 19 | serverId = serverId, 20 | userId = UserId(this.userId), 21 | categoryId = CategoryId(this.id), 22 | categoryTitle = CategoryTitle(this.title) 23 | ) 24 | 25 | fun List.toCategoryList(serverId: ServerId) = map { 26 | it.toCategory(serverId) 27 | } 28 | 29 | 30 | fun List.toCategoryTitleList() = map { 31 | it.categoryTitle.title 32 | } -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/transformation/Entry.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.transformation 2 | 3 | import com.constantin.microflux.data.* 4 | import com.constantin.microflux.database.Entry 5 | import com.constantin.microflux.database.EntryListPreview 6 | import com.constantin.microflux.network.data.EntryResponse 7 | import com.constantin.microflux.repository.util.stringToEntryTime 8 | import org.jsoup.Jsoup 9 | 10 | fun EntryResponse.toEntry(serverId: ServerId): Entry { 11 | 12 | val entryTime = publishedAt.stringToEntryTime() 13 | return Entry( 14 | serverId = serverId, 15 | entryId = EntryId(id), 16 | feedId = FeedId(feedId), 17 | entryTitle = EntryTitle(title), 18 | entryUrl = EntryUrl(url), 19 | entryPreviewImage = EntryPreviewImage( 20 | try { 21 | Jsoup.parse(content) 22 | .select("img") 23 | .first() 24 | .attr("src") 25 | } catch (e: NullPointerException) { 26 | "" 27 | } 28 | ), 29 | entryAuthor = EntryAuthor(author), 30 | entryContent = EntryContent(content), 31 | entryPublishedAtDisplay = entryTime.entryPublishedAtDisplay, 32 | entryPublishedAtRaw = entryTime.entryPublishedAtRaw, 33 | entryPublishedAtUnix = entryTime.entryPublishedAtUnix, 34 | entryStatus = EntryStatus(status), 35 | entryStarred = EntryStarred(starred) 36 | ) 37 | } 38 | 39 | 40 | fun List.toEntryList(serverId: ServerId) = map { 41 | it.toEntry(serverId) 42 | } 43 | 44 | fun List.toEntryIdList() = map { 45 | it.entryId.id 46 | } -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/transformation/Feed.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.transformation 2 | 3 | import com.constantin.microflux.data.* 4 | import com.constantin.microflux.database.Feed 5 | import com.constantin.microflux.network.data.FeedResponse 6 | import com.constantin.microflux.repository.util.stringToEntryTime 7 | 8 | 9 | fun FeedResponse.toFeed(serverId: ServerId, feedIcon: FeedIcon) = Feed( 10 | serverId = serverId, 11 | feedId = FeedId(id), 12 | categoryId = CategoryId(category.id), 13 | feedTitle = FeedTitle(title), 14 | feedSiteUrl = FeedSiteUrl(siteUrl), 15 | feedUrl = FeedUrl(feedUrl), 16 | feedCheckedAtDisplay = FeedCheckedAtDisplay(checkedAt.stringToEntryTime().entryPublishedAtDisplay.publishedAt), 17 | feedIcon = feedIcon, 18 | feedScraperRules = FeedScraperRules(scraperRules), 19 | feedRewriteRules = FeedRewriteRules(rewriteRules), 20 | feedCrawler = FeedCrawler(crawler), 21 | feedUsername = FeedUsername(username), 22 | feedPassword = FeedPassword(password), 23 | feedUserAgent = FeedUserAgent(userAgent), 24 | feedAllowNotification = FeedAllowNotification.ON, 25 | feedAllowImagePreview = FeedAllowImagePreview.ON, 26 | feedLastUpdateAtUnix = FeedLastUpdateAtUnix.EMPTY, 27 | feedNotificationCount = FeedNotificationCount.INVALID 28 | ) -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/transformation/Me.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.transformation 2 | 3 | import com.constantin.microflux.data.* 4 | import com.constantin.microflux.database.Me 5 | import com.constantin.microflux.network.data.MeResponse 6 | 7 | fun MeResponse.toMe(serverId: ServerId, userId: UserId) = Me( 8 | serverId = serverId, 9 | userId = userId, 10 | meIsAdmin = MeIsAdmin(isAdmin), 11 | meLanguage = MeLanguage(language), 12 | meLastLoginAt = MeLastLoginAt(lastLoginAt), 13 | meTheme = MeTheme(theme), 14 | meTimeZone = MeTimeZone(timezone), 15 | meEntrySortingDirection = MeEntrySortingDirection(entrySortingDirection) 16 | ) -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/transformation/NotificationInformationBundle.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.transformation 2 | 3 | import com.constantin.microflux.data.* 4 | import com.constantin.microflux.database.Account 5 | 6 | data class FeedNotificationInformation( 7 | val serverId: ServerId, 8 | val userId: UserId, 9 | val userName: UserName, 10 | val feedId: FeedId, 11 | val feedTitle: FeedTitle, 12 | val feedIcon: FeedIcon, 13 | val notificationItemsCount: FeedNotificationCount, 14 | val notify: Boolean 15 | ) 16 | 17 | data class NotificationInformationBundle( 18 | val accountsFeedsInformation: List, 19 | val invalidAccounts: List 20 | ) 21 | 22 | data class AccountFeedNotificationData( 23 | val feedsInformation: List, 24 | val feedTotalCount: Long 25 | ) -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/util/DisplayTime.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.util 2 | 3 | import com.constantin.microflux.data.EntryPublishedAtDisplay 4 | import com.constantin.microflux.data.EntryPublishedAtRaw 5 | import com.constantin.microflux.data.EntryPublishedAtUnix 6 | import java.time.Duration 7 | import java.time.LocalDateTime 8 | import java.time.OffsetDateTime 9 | import java.time.Period 10 | 11 | data class EntryTime( 12 | val entryPublishedAtUnix: EntryPublishedAtUnix, 13 | val entryPublishedAtDisplay: EntryPublishedAtDisplay, 14 | val entryPublishedAtRaw: EntryPublishedAtRaw 15 | ) 16 | 17 | fun String.stringToEntryTime(): EntryTime { 18 | val offsetDateTime = OffsetDateTime.parse(this) 19 | val timeNow = LocalDateTime.now(offsetDateTime.toZonedDateTime().zone) 20 | val timeOther = offsetDateTime.toLocalDateTime() 21 | 22 | val duration = Duration.between(timeOther, timeNow) 23 | val deltaMinutes = duration.toMinutes() 24 | val deltaHours = duration.toHours() 25 | val deltaDays = duration.toDays() 26 | val periodDiff = Period.between(timeOther.toLocalDate(), timeNow.toLocalDate()) 27 | val deltaYears = periodDiff.years 28 | val deltaMonth = periodDiff.months 29 | 30 | val entryPublishedAtDisplay = 31 | if (deltaMinutes < 60) if (deltaMinutes == 0L) "now" else "$deltaMinutes minute${if (deltaMinutes > 1) "s" else ""} ago" 32 | else if (deltaHours < 24) "$deltaHours hour${if (deltaHours > 1) "s" else ""} ago" 33 | else if (deltaDays < timeNow.toLocalDate().lengthOfMonth()) 34 | if (deltaDays == 1L) "yesterday" else "$deltaDays days ago" 35 | else if (deltaMonth < 12) "$deltaMonth month${if (deltaMonth > 1) "s" else ""} ago" 36 | else "$deltaYears year${if (deltaYears > 1) "s" else ""} ago" 37 | 38 | 39 | return EntryTime( 40 | entryPublishedAtRaw = EntryPublishedAtRaw(this), 41 | entryPublishedAtDisplay = EntryPublishedAtDisplay(entryPublishedAtDisplay), 42 | entryPublishedAtUnix = EntryPublishedAtUnix(offsetDateTime.toEpochSecond()) 43 | ) 44 | } -------------------------------------------------------------------------------- /repository/src/main/java/com/constantin/microflux/repository/util/String.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.repository.util 2 | 3 | import android.util.Base64 4 | 5 | fun String.decodeBase64(): ByteArray = 6 | Base64.decode( 7 | this.replace(Regex("^(image/.*;base64,)"), "") 8 | .toByteArray(), 0 9 | ) -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":data") 2 | include(":network") 3 | include(":database") 4 | include(":repository") 5 | include(":viewmodel") 6 | include(":androidapp") 7 | rootProject.name = "ConstaFlux2" 8 | include(":encryption") 9 | -------------------------------------------------------------------------------- /viewmodel/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /viewmodel/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("kotlin-android") 4 | id("kotlin-android-extensions") 5 | } 6 | 7 | android { 8 | compileSdkVersion(30) 9 | defaultConfig { 10 | minSdkVersion(24) 11 | targetSdkVersion(30) 12 | versionCode = 1 13 | versionName = "1.0" 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | getByName("release") { 18 | isMinifyEnabled = false 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" 21 | ) 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility = JavaVersion.VERSION_1_8 26 | targetCompatibility = JavaVersion.VERSION_1_8 27 | } 28 | kotlinOptions { 29 | jvmTarget = "1.8" 30 | @Suppress("SuspiciousCollectionReassignment") 31 | freeCompilerArgs += listOf( 32 | "-progressive", 33 | "-XXLanguage:+NewInference", 34 | "-XXLanguage:+InlineClasses", 35 | "-Xuse-experimental=kotlin.ExperimentalUnsignedTypes", 36 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi", 37 | "-Xuse-experimental=kotlinx.coroutines.FlowPreview" 38 | ) 39 | } 40 | packagingOptions { 41 | pickFirst("META-INF/*.kotlin_module") 42 | } 43 | } 44 | 45 | dependencies { 46 | // modules 47 | api(project(":repository")) 48 | // Lifecycle 49 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01") 50 | // viewmodel 51 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01") 52 | // Coroutines 53 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") 54 | } -------------------------------------------------------------------------------- /viewmodel/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/AccountViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.constantin.microflux.data.* 5 | import com.constantin.microflux.database.Account 6 | import com.constantin.microflux.module.util.BaseViewModel 7 | import com.constantin.microflux.module.util.load 8 | import com.constantin.microflux.repository.ConstafluxRepository 9 | import kotlinx.coroutines.Deferred 10 | import kotlinx.coroutines.Job 11 | import kotlinx.coroutines.async 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.first 15 | import kotlinx.coroutines.launch 16 | import kotlin.coroutines.CoroutineContext 17 | 18 | abstract class AccountViewmodel( 19 | private val context: CoroutineContext, 20 | private val repository: ConstafluxRepository 21 | ) : BaseViewModel() { 22 | abstract val account: Deferred? 23 | 24 | private val _upsertAccountProgression = MutableStateFlow>(Result.complete()) 25 | val upsertAccountProgression: StateFlow> = _upsertAccountProgression 26 | 27 | fun upsertAccount( 28 | serverUrl: ServerUrl, 29 | userName: UserName, 30 | userPassword: UserPassword 31 | ) = viewModelScope.launch { 32 | _upsertAccountProgression.load { 33 | repository.accountRepository.upsertAccount( 34 | serverUrl = serverUrl, 35 | userName = userName, 36 | userPassword = userPassword 37 | ) 38 | } 39 | } 40 | 41 | abstract fun deleteAccount(): Job? 42 | } 43 | 44 | class CreateAccountViewModel( 45 | context: CoroutineContext, 46 | repository: ConstafluxRepository 47 | ) : AccountViewmodel(context, repository) { 48 | override val account: Deferred? = null 49 | override fun deleteAccount(): Job? = null 50 | } 51 | 52 | class UpdateAccountViewModel( 53 | private val context: CoroutineContext, 54 | private val repository: ConstafluxRepository, 55 | serverId: ServerId, 56 | userId: UserId 57 | ) : AccountViewmodel(context, repository) { 58 | override val account = viewModelScope.async(context) { 59 | repository.accountRepository.getAccount( 60 | serverId = serverId, 61 | userId = userId 62 | ).first() 63 | } 64 | 65 | override fun deleteAccount() = viewModelScope.launch { 66 | repository.accountRepository.deleteCurrentAccount() 67 | } 68 | 69 | } 70 | 71 | class AccountDialogViewmodel( 72 | private val context: CoroutineContext, 73 | private val repository: ConstafluxRepository 74 | ) : BaseViewModel() { 75 | 76 | val currentAccount = repository.accountRepository.getCurrentAccount() 77 | 78 | val nonCurrentAccounts = repository.accountRepository.getNonCurrentAccounts() 79 | 80 | fun changeAccounts( 81 | account: Account 82 | ) = viewModelScope.launch { 83 | repository.accountRepository.changeAccount( 84 | serverId = account.serverId, 85 | userId = account.userId 86 | ) 87 | } 88 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/EntryDescriptionViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.constantin.microflux.data.EntryId 5 | import com.constantin.microflux.data.EntryStatus 6 | import com.constantin.microflux.data.Result 7 | import com.constantin.microflux.module.util.BaseViewModel 8 | import com.constantin.microflux.module.util.load 9 | import com.constantin.microflux.repository.ConstafluxRepository 10 | import kotlinx.coroutines.async 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.flow.StateFlow 13 | import kotlinx.coroutines.flow.first 14 | import kotlinx.coroutines.launch 15 | import kotlin.coroutines.CoroutineContext 16 | 17 | class EntryDescriptionViewModel( 18 | private val context: CoroutineContext, 19 | private val repository: ConstafluxRepository, 20 | private val entryId: EntryId 21 | ) : BaseViewModel() { 22 | 23 | val currentAccount get() = repository.accountRepository.currentAccount 24 | 25 | val entry = viewModelScope.async(context) { 26 | repository.entryRepository.getEntry(entryId = entryId).first() 27 | } 28 | 29 | private val _updateEntryStatusProgression = MutableStateFlow>(Result.complete()) 30 | val updateEntryStatusProgression: StateFlow> = _updateEntryStatusProgression 31 | 32 | fun updateEntryStatus(entryStatus: EntryStatus) = viewModelScope.launch { 33 | _updateEntryStatusProgression.load { 34 | repository.entryRepository.updateStatus( 35 | entryIds = listOf(entryId), 36 | entryStatus = entryStatus 37 | ) 38 | } 39 | } 40 | 41 | private val _updateEntryStarredProgression = MutableStateFlow>(Result.complete()) 42 | val updateEntryStarredProgression: StateFlow> = _updateEntryStarredProgression 43 | 44 | fun updateEntryStarred() = viewModelScope.launch { 45 | _updateEntryStarredProgression.load { 46 | repository.entryRepository.updateStarred( 47 | entryIds = listOf(entryId) 48 | ) 49 | } 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/NavigationViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.module.util.BaseViewModel 4 | import com.constantin.microflux.repository.ConstafluxRepository 5 | import kotlin.coroutines.CoroutineContext 6 | 7 | class NavigationViewModel( 8 | context: CoroutineContext, 9 | repository: ConstafluxRepository 10 | ) : BaseViewModel() { 11 | val currentAccount = repository.accountRepository.currentAccount 12 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import androidx.lifecycle.viewModelScope 4 | import com.constantin.microflux.data.SettingsAllowImagePreview 5 | import com.constantin.microflux.data.SettingsTheme 6 | import com.constantin.microflux.module.util.BaseViewModel 7 | import com.constantin.microflux.repository.ConstafluxRepository 8 | import kotlinx.coroutines.async 9 | import kotlinx.coroutines.flow.first 10 | import kotlinx.coroutines.launch 11 | import kotlin.coroutines.CoroutineContext 12 | 13 | class SettingsViewModel( 14 | private val context: CoroutineContext, 15 | private val repository: ConstafluxRepository 16 | ) : BaseViewModel() { 17 | 18 | val me = viewModelScope.async(context) { 19 | repository.meRepository.getMe().first() 20 | } 21 | 22 | val user get() = repository.accountRepository.currentAccount 23 | 24 | val settings = viewModelScope.async(context) { 25 | repository.settingsRepository.getSettings().first() 26 | } 27 | 28 | fun updateSettingsTheme( 29 | settingsTheme: SettingsTheme 30 | ) = viewModelScope.launch { 31 | repository.settingsRepository.changeSettingsTheme( 32 | settingsTheme = settingsTheme 33 | ) 34 | } 35 | 36 | fun updateAllowImagePreview( 37 | settingsAllowImagePreview: SettingsAllowImagePreview 38 | ) = viewModelScope.launch { 39 | repository.settingsRepository.changeAllowImagePreview( 40 | settingsAllowImagePreview = settingsAllowImagePreview 41 | ) 42 | } 43 | 44 | fun logout() = viewModelScope.launch { 45 | repository.accountRepository.deleteCurrentAccount() 46 | } 47 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/State.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.data.* 4 | 5 | sealed class State { 6 | object CreateAccount : State() 7 | data class UpdateAccount(val serverId: ServerId, val userId: UserId) : State() 8 | object AccountDialog : State() 9 | object Entries : State() 10 | data class EntryDescription(val entryId: EntryId) : State() 11 | object Feed : State() 12 | data class FeedDialog(val feedId: FeedId) : State() 13 | data class FeedEntries(val feedId: FeedId) : State() 14 | object Category : State() 15 | data class CategoryDialog(val categoryId: CategoryId) : State() 16 | data class CategoryFeeds(val categoryId: CategoryId) : State() 17 | object Navigation : State() 18 | object Settings : State() 19 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/ViewmodelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module 2 | 3 | import com.constantin.microflux.module.util.BaseViewModel 4 | import com.constantin.microflux.repository.ConstafluxRepository 5 | import kotlin.coroutines.CoroutineContext 6 | 7 | class ViewmodelFactory( 8 | private val context: CoroutineContext, 9 | private val constafluxRepository: ConstafluxRepository 10 | ) { 11 | fun create(state: State): BaseViewModel { 12 | return when (state) { 13 | is State.CreateAccount -> CreateAccountViewModel( 14 | context = context, 15 | repository = constafluxRepository 16 | ) 17 | is State.UpdateAccount -> UpdateAccountViewModel( 18 | context = context, 19 | repository = constafluxRepository, 20 | serverId = state.serverId, 21 | userId = state.userId 22 | ) 23 | is State.AccountDialog -> AccountDialogViewmodel( 24 | context = context, 25 | repository = constafluxRepository 26 | ) 27 | is State.Entries -> AllEntryViewModel( 28 | context = context, 29 | repository = constafluxRepository 30 | ) 31 | is State.EntryDescription -> EntryDescriptionViewModel( 32 | context = context, 33 | repository = constafluxRepository, 34 | entryId = state.entryId 35 | ) 36 | is State.FeedEntries -> FeedEntryViewModel( 37 | context = context, 38 | repository = constafluxRepository, 39 | feedId = state.feedId 40 | ) 41 | is State.Feed -> AllFeedViewModel( 42 | context = context, 43 | repository = constafluxRepository 44 | ) 45 | is State.FeedDialog -> FeedDialogViewModel( 46 | context = context, 47 | repository = constafluxRepository, 48 | feedId = state.feedId 49 | ) 50 | is State.CategoryFeeds -> CategoryFeedViewModel( 51 | context = context, 52 | repository = constafluxRepository, 53 | categoryId = state.categoryId 54 | ) 55 | is State.Category -> CategoryViewModel( 56 | context = context, 57 | repository = constafluxRepository 58 | ) 59 | is State.CategoryDialog -> CategoryDialogViewModel( 60 | context = context, 61 | repository = constafluxRepository, 62 | categoryId = state.categoryId 63 | ) 64 | is State.Navigation -> NavigationViewModel( 65 | context = context, 66 | repository = constafluxRepository 67 | ) 68 | is State.Settings -> SettingsViewModel( 69 | context = context, 70 | repository = constafluxRepository 71 | ) 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/util/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module.util 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.CoroutineScope 6 | 7 | abstract class BaseViewModel constructor() : ViewModel() { 8 | val clientScope: CoroutineScope = viewModelScope 9 | override fun onCleared() { 10 | super.onCleared() 11 | } 12 | } -------------------------------------------------------------------------------- /viewmodel/src/main/java/com/constantin/microflux/module/util/load.kt: -------------------------------------------------------------------------------- 1 | package com.constantin.microflux.module.util 2 | 3 | import com.constantin.microflux.data.Result 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | 6 | suspend inline fun MutableStateFlow>.load( 7 | triggerLoading: Boolean = true, 8 | crossinline load: suspend () -> Result 9 | ) { 10 | if (triggerLoading) { 11 | value = Result.inProgress() 12 | value = load() 13 | value = Result.complete() 14 | } else load() 15 | } 16 | --------------------------------------------------------------------------------