├── .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 |
4 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.idea/kotlinScripting.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/menu/menu_entry_description.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/menu/menu_feed_category_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/menu/menu_list_category_feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/menu/menu_list_entry.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/androidapp/src/main/res/menu/menu_list_selection.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
4 |
5 |
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 |
4 |
5 |
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 |
--------------------------------------------------------------------------------