diff --git a/android/build.gradle.kts b/android/build.gradle.kts
index 2d23f216..52e5073f 100644
--- a/android/build.gradle.kts
+++ b/android/build.gradle.kts
@@ -29,6 +29,7 @@ dependencies {
implementation(libs.core)
implementation(libs.appCompat)
implementation(libs.activityCompose)
+ implementation(libs.work)
// Android Lifecycle
implementation(libs.lifecycleCommon)
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 7347525c..c2b7c833 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -34,7 +34,7 @@
\ No newline at end of file
diff --git a/android/src/main/java/ca/gosyer/jui/android/App.kt b/android/src/main/java/ca/gosyer/jui/android/App.kt
index c830c611..06ca0aaf 100644
--- a/android/src/main/java/ca/gosyer/jui/android/App.kt
+++ b/android/src/main/java/ca/gosyer/jui/android/App.kt
@@ -14,8 +14,7 @@ import androidx.lifecycle.lifecycleScope
import ca.gosyer.core.logging.CKLogger
import ca.gosyer.core.prefs.getAsFlow
import ca.gosyer.data.ui.model.ThemeMode
-import ca.gosyer.jui.android.data.Notifications
-import ca.gosyer.ui.AppComponent
+import ca.gosyer.jui.android.data.notification.Notifications
import kotlinx.coroutines.flow.launchIn
class App : Application(), DefaultLifecycleObserver {
diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/AppComponent.kt b/android/src/main/java/ca/gosyer/jui/android/AppComponent.kt
similarity index 61%
rename from presentation/src/androidMain/kotlin/ca/gosyer/ui/AppComponent.kt
rename to android/src/main/java/ca/gosyer/jui/android/AppComponent.kt
index d1c77968..3c544b25 100644
--- a/presentation/src/androidMain/kotlin/ca/gosyer/ui/AppComponent.kt
+++ b/android/src/main/java/ca/gosyer/jui/android/AppComponent.kt
@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-package ca.gosyer.ui
+package ca.gosyer.jui.android
import android.annotation.SuppressLint
import android.content.Context
@@ -13,18 +13,25 @@ import ca.gosyer.data.DataComponent
import ca.gosyer.data.create
import ca.gosyer.ui.base.UiComponent
import ca.gosyer.ui.base.create
-import ca.gosyer.uicore.vm.ContextWrapper
import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
@AppScope
@Component
-actual abstract class AppComponent constructor(
- val context: Context,
- actual val dataComponent: DataComponent = DataComponent.create(context),
+abstract class AppComponent(
+ context: Context,
+ val dataComponent: DataComponent = DataComponent.create(context),
@Component
- actual val uiComponent: UiComponent = UiComponent.create(dataComponent)
+ val uiComponent: UiComponent = UiComponent.create(dataComponent)
) {
- actual abstract val contextWrapper: ContextWrapper
+
+ abstract val appMigrations: AppMigrations
+
+ @get:AppScope
+ @get:Provides
+ protected val appMigrationsFactory: AppMigrations
+ get() = AppMigrations(dataComponent.migrationPreferences, uiComponent.contextWrapper)
+
companion object {
@SuppressLint("StaticFieldLeak")
private var appComponentInstance: AppComponent? = null
diff --git a/android/src/main/java/ca/gosyer/jui/android/AppMigrations.kt b/android/src/main/java/ca/gosyer/jui/android/AppMigrations.kt
new file mode 100644
index 00000000..1be0bc72
--- /dev/null
+++ b/android/src/main/java/ca/gosyer/jui/android/AppMigrations.kt
@@ -0,0 +1,34 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package ca.gosyer.jui.android
+
+import ca.gosyer.data.migration.MigrationPreferences
+import ca.gosyer.jui.android.data.update.UpdateCheckWorker
+import ca.gosyer.uicore.vm.ContextWrapper
+import me.tatarka.inject.annotations.Inject
+
+class AppMigrations @Inject constructor(
+ private val migrationPreferences: MigrationPreferences,
+ private val contextWrapper: ContextWrapper
+) {
+
+ fun runMigrations(): Boolean {
+ val oldVersion = migrationPreferences.appVersion().get()
+ if (oldVersion < BuildConfig.VERSION_CODE) {
+ migrationPreferences.appVersion().set(BuildConfig.VERSION_CODE)
+
+ UpdateCheckWorker.setupTask(contextWrapper.context)
+
+ // Fresh install
+ if (oldVersion == 0) {
+ return false
+ }
+ return true
+ }
+ return false
+ }
+}
diff --git a/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt b/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt
index f9f74bb6..c148214e 100644
--- a/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt
+++ b/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt
@@ -4,8 +4,7 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider
-import ca.gosyer.jui.android.data.AndroidDownloadService
-import ca.gosyer.ui.AppComponent
+import ca.gosyer.jui.android.data.download.AndroidDownloadService
import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.ui.main.MainMenu
@@ -16,6 +15,7 @@ class MainActivity : AppCompatActivity() {
val appComponent = AppComponent.getInstance(applicationContext)
if (savedInstanceState == null) {
appComponent.dataComponent.migrations.runMigrations()
+ appComponent.appMigrations.runMigrations()
}
AndroidDownloadService.start(this, AndroidDownloadService.Actions.START)
diff --git a/android/src/main/java/ca/gosyer/jui/android/ReaderActivity.kt b/android/src/main/java/ca/gosyer/jui/android/ReaderActivity.kt
index 5b3bbf4e..7786bf6e 100644
--- a/android/src/main/java/ca/gosyer/jui/android/ReaderActivity.kt
+++ b/android/src/main/java/ca/gosyer/jui/android/ReaderActivity.kt
@@ -16,7 +16,6 @@ import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.NativeKeyEvent
import androidx.compose.ui.input.key.key
import androidx.lifecycle.lifecycleScope
-import ca.gosyer.ui.AppComponent
import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.ui.reader.ReaderMenu
import ca.gosyer.ui.reader.supportedKeyList
diff --git a/android/src/main/java/ca/gosyer/jui/android/data/AndroidDownloadService.kt b/android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt
similarity index 98%
rename from android/src/main/java/ca/gosyer/jui/android/data/AndroidDownloadService.kt
rename to android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt
index 6ae89bad..f09be393 100644
--- a/android/src/main/java/ca/gosyer/jui/android/data/AndroidDownloadService.kt
+++ b/android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt
@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-package ca.gosyer.jui.android.data
+package ca.gosyer.jui.android.data.download
import android.app.Notification
import android.app.Service
@@ -23,12 +23,13 @@ import ca.gosyer.data.download.model.DownloadState
import ca.gosyer.data.download.model.DownloadStatus
import ca.gosyer.data.server.requests.downloadsQuery
import ca.gosyer.i18n.MR
+import ca.gosyer.jui.android.AppComponent
import ca.gosyer.jui.android.R
+import ca.gosyer.jui.android.data.notification.Notifications
import ca.gosyer.jui.android.util.acquireWakeLock
import ca.gosyer.jui.android.util.notification
import ca.gosyer.jui.android.util.notificationBuilder
import ca.gosyer.jui.android.util.notificationManager
-import ca.gosyer.ui.AppComponent
import dev.icerock.moko.resources.desc.desc
import dev.icerock.moko.resources.format
import io.ktor.client.features.websocket.ws
diff --git a/android/src/main/java/ca/gosyer/jui/android/data/Notifications.kt b/android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt
similarity index 98%
rename from android/src/main/java/ca/gosyer/jui/android/data/Notifications.kt
rename to android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt
index dd9c172d..4d7376d1 100644
--- a/android/src/main/java/ca/gosyer/jui/android/data/Notifications.kt
+++ b/android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt
@@ -4,7 +4,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-package ca.gosyer.jui.android.data
+package ca.gosyer.jui.android.data.notification
import android.content.Context
import androidx.core.app.NotificationManagerCompat
diff --git a/android/src/main/java/ca/gosyer/jui/android/data/update/UpdateCheckWorker.kt b/android/src/main/java/ca/gosyer/jui/android/data/update/UpdateCheckWorker.kt
new file mode 100644
index 00000000..da4f0ace
--- /dev/null
+++ b/android/src/main/java/ca/gosyer/jui/android/data/update/UpdateCheckWorker.kt
@@ -0,0 +1,80 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package ca.gosyer.jui.android.data.update
+
+import android.content.Context
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import ca.gosyer.data.update.UpdateChecker.Update
+import ca.gosyer.i18n.MR
+import ca.gosyer.jui.android.AppComponent
+import ca.gosyer.jui.android.data.notification.Notifications
+import ca.gosyer.jui.android.util.notificationBuilder
+import ca.gosyer.jui.android.util.notificationManager
+import dev.icerock.moko.resources.desc.desc
+import kotlinx.coroutines.flow.singleOrNull
+import java.util.concurrent.TimeUnit
+
+class UpdateCheckWorker(private val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
+ override suspend fun doWork(): Result {
+ return try {
+ val update = AppComponent.getInstance(context.applicationContext)
+ .dataComponent
+ .let {
+ if (it.updatePreferences.enabled().get()) {
+ it.updateChecker
+ .checkForUpdates()
+ .singleOrNull()
+ } else null
+ }
+
+ if (update is Update.UpdateFound) {
+ context.notificationBuilder(Notifications.CHANNEL_APP_UPDATE) {
+ setContentTitle(MR.strings.new_update_title.desc().toString(context))
+ setContentText(update.release.version)
+ setSmallIcon(android.R.drawable.stat_sys_download_done)
+ }.let {
+ context.notificationManager.notify(Notifications.ID_UPDATES_TO_APP, it.build())
+ }
+ }
+ Result.success()
+ } catch (e: Exception) {
+ Result.failure()
+ }
+ }
+
+ companion object {
+ private const val TAG = "UpdateChecker"
+
+ fun setupTask(context: Context) {
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .build()
+
+ val request = PeriodicWorkRequestBuilder(
+ 7,
+ TimeUnit.DAYS,
+ 3,
+ TimeUnit.HOURS
+ )
+ .addTag(TAG)
+ .setConstraints(constraints)
+ .build()
+
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
+ }
+
+ fun cancelTask(context: Context) {
+ WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+ }
+ }
+}
diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/migration/MigrationPreferences.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/migration/MigrationPreferences.kt
index 7e290882..7b3c31c2 100644
--- a/data/src/jvmMain/kotlin/ca/gosyer/data/migration/MigrationPreferences.kt
+++ b/data/src/jvmMain/kotlin/ca/gosyer/data/migration/MigrationPreferences.kt
@@ -13,4 +13,7 @@ class MigrationPreferences(private val preferenceStore: PreferenceStore) {
fun version(): Preference {
return preferenceStore.getInt("version", 0)
}
+ fun appVersion(): Preference {
+ return preferenceStore.getInt("app_version", 0)
+ }
}
diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/update/UpdateChecker.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/update/UpdateChecker.kt
index 6dbbe1c5..362a7401 100644
--- a/data/src/jvmMain/kotlin/ca/gosyer/data/update/UpdateChecker.kt
+++ b/data/src/jvmMain/kotlin/ca/gosyer/data/update/UpdateChecker.kt
@@ -6,35 +6,35 @@
package ca.gosyer.data.update
-import ca.gosyer.core.lang.launch
-import ca.gosyer.core.lang.withIOContext
import ca.gosyer.data.build.BuildKonfig
import ca.gosyer.data.server.Http
import ca.gosyer.data.update.model.GithubRelease
import io.ktor.client.request.get
-import kotlinx.coroutines.DelicateCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
class UpdateChecker @Inject constructor(
private val updatePreferences: UpdatePreferences,
private val client: Http
) {
- private val _updateFound = MutableSharedFlow()
- val updateFound = _updateFound.asSharedFlow()
+ fun checkForUpdates() = flow {
+ // if (!updatePreferences.enabled().get()) return
+ val latestRelease = client.get(
+ "https://api.github.com/repos/$GITHUB_REPO/releases/latest"
+ )
- @OptIn(DelicateCoroutinesApi::class)
- fun checkForUpdates() {
- if (!updatePreferences.enabled().get()) return
- launch {
- val latestRelease = withIOContext {
- client.get("https://api.github.com/repos/$GITHUB_REPO/releases/latest")
- }
- if (isNewVersion(latestRelease.version)) {
- _updateFound.emit(latestRelease)
- }
+ if (isNewVersion(latestRelease.version)) {
+ emit(Update.UpdateFound(latestRelease))
+ } else {
+ emit(Update.NoUpdatesFound)
}
+ }.flowOn(Dispatchers.IO)
+
+ sealed class Update {
+ data class UpdateFound(val release: GithubRelease) : Update()
+ object NoUpdatesFound : Update()
}
// Thanks to Tachiyomi for inspiration
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/AppComponent.kt b/desktop/src/main/kotlin/ca/gosyer/AppComponent.kt
similarity index 58%
rename from presentation/src/desktopMain/kotlin/ca/gosyer/ui/AppComponent.kt
rename to desktop/src/main/kotlin/ca/gosyer/AppComponent.kt
index 5daed559..3d8b5472 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/AppComponent.kt
+++ b/desktop/src/main/kotlin/ca/gosyer/AppComponent.kt
@@ -4,24 +4,31 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
-package ca.gosyer.ui
+package ca.gosyer
import ca.gosyer.core.di.AppScope
import ca.gosyer.data.DataComponent
import ca.gosyer.data.create
import ca.gosyer.ui.base.UiComponent
import ca.gosyer.ui.base.create
-import ca.gosyer.uicore.vm.ContextWrapper
import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Provides
@AppScope
@Component
-actual abstract class AppComponent constructor(
- actual val dataComponent: DataComponent = DataComponent.create(),
+abstract class AppComponent constructor(
+ val dataComponent: DataComponent = DataComponent.create(),
@Component
- actual val uiComponent: UiComponent = UiComponent.create(dataComponent)
+ val uiComponent: UiComponent = UiComponent.create(dataComponent)
) {
- actual abstract val contextWrapper: ContextWrapper
+
+ abstract val appMigrations: AppMigrations
+
+ @get:AppScope
+ @get:Provides
+ protected val appMigrationsFactory: AppMigrations
+ get() = AppMigrations(dataComponent.migrationPreferences, uiComponent.contextWrapper)
+
companion object {
private var appComponentInstance: AppComponent? = null
diff --git a/desktop/src/main/kotlin/ca/gosyer/AppMigrations.kt b/desktop/src/main/kotlin/ca/gosyer/AppMigrations.kt
new file mode 100644
index 00000000..8ee2c5a9
--- /dev/null
+++ b/desktop/src/main/kotlin/ca/gosyer/AppMigrations.kt
@@ -0,0 +1,32 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+package ca.gosyer
+
+import ca.gosyer.data.migration.MigrationPreferences
+import ca.gosyer.desktop.build.BuildConfig
+import ca.gosyer.uicore.vm.ContextWrapper
+import me.tatarka.inject.annotations.Inject
+
+class AppMigrations @Inject constructor(
+ private val migrationPreferences: MigrationPreferences,
+ private val contextWrapper: ContextWrapper
+) {
+
+ fun runMigrations(): Boolean {
+ val oldVersion = migrationPreferences.appVersion().get()
+ if (oldVersion < BuildConfig.MIGRATION_CODE) {
+ migrationPreferences.appVersion().set(BuildConfig.MIGRATION_CODE)
+
+ // Fresh install
+ if (oldVersion == 0) {
+ return false
+ }
+ return true
+ }
+ return false
+ }
+}
diff --git a/desktop/src/main/kotlin/ca/gosyer/main.kt b/desktop/src/main/kotlin/ca/gosyer/main.kt
index af0f3376..899f72a9 100644
--- a/desktop/src/main/kotlin/ca/gosyer/main.kt
+++ b/desktop/src/main/kotlin/ca/gosyer/main.kt
@@ -32,7 +32,6 @@ import ca.gosyer.data.server.ServerService.ServerResult
import ca.gosyer.data.ui.model.ThemeMode
import ca.gosyer.desktop.build.BuildConfig
import ca.gosyer.i18n.MR
-import ca.gosyer.ui.AppComponent
import ca.gosyer.ui.base.dialog.getMaterialDialogProperties
import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.ui.main.MainMenu
@@ -70,6 +69,7 @@ suspend fun main() {
val dataComponent = appComponent.dataComponent
val uiComponent = appComponent.uiComponent
dataComponent.migrations.runMigrations()
+ appComponent.appMigrations.runMigrations()
dataComponent.downloadService.init()
// dataComponent.libraryUpdateService.init()
val serverService = dataComponent.serverService
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 566574f2..ba4cb561 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -17,6 +17,7 @@ materialDialogs = "0.6.6"
core = "1.7.0"
appCompat = "1.4.1"
activityCompose = "1.4.0"
+work = "2.6.0"
# Android Lifecycle
lifecycle = "2.4.1"
@@ -75,6 +76,7 @@ materialDialogsCore = { module = "ca.gosyer:compose-material-dialogs-core", vers
core = { module = "androidx.core:core-ktx", version.ref = "core" }
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "appCompat" }
activityCompose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
+work = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
# Android Lifecycle
lifecycleCommon = { module = "androidx.lifecycle:lifecycle-common", version.ref = "lifecycle" }
diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/UiComponent.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
index 8c11fbab..b83fa8c2 100644
--- a/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
+++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
@@ -10,6 +10,7 @@ import ca.gosyer.core.di.AppScope
import ca.gosyer.data.DataComponent
import ca.gosyer.ui.base.image.KamelConfigProvider
import ca.gosyer.ui.base.vm.ViewModelFactoryImpl
+import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.LocalViewModelFactory
import io.kamel.core.config.KamelConfig
import io.kamel.image.config.LocalKamelConfig
@@ -28,6 +29,8 @@ actual abstract class UiComponent(
actual abstract val kamelConfig: KamelConfig
+ actual abstract val contextWrapper: ContextWrapper
+
@get:AppScope
@get:Provides
protected actual val kamelConfigFactory: KamelConfig
diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/categories/OpenCategories.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/categories/OpenCategories.kt
index 235333ed..a969592b 100644
--- a/presentation/src/androidMain/kotlin/ca/gosyer/ui/categories/OpenCategories.kt
+++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/categories/OpenCategories.kt
@@ -6,8 +6,24 @@
package ca.gosyer.ui.categories
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
-actual fun openCategoriesMenu(notifyFinished: () -> Unit, navigator: Navigator) {
- navigator push CategoriesScreen(notifyFinished)
+actual class CategoriesLauncher(private val navigator: Navigator?) {
+
+ actual fun open() {
+ navigator?.push(CategoriesScreen())
+ }
+
+ @Composable
+ actual fun CategoriesWindow() {
+ }
+}
+
+@Composable
+actual fun rememberCategoriesLauncher(notifyFinished: () -> Unit): CategoriesLauncher {
+ val navigator = LocalNavigator.current
+ return remember(navigator) { CategoriesLauncher(navigator) }
}
diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/reader/AndroidReaderMenu.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/reader/AndroidReaderMenu.kt
index b7cae3be..edf8ab3f 100644
--- a/presentation/src/androidMain/kotlin/ca/gosyer/ui/reader/AndroidReaderMenu.kt
+++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/reader/AndroidReaderMenu.kt
@@ -23,6 +23,10 @@ actual class ReaderLauncher(private val context: Context) {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
}.let(context::startActivity)
}
+
+ @Composable
+ actual fun Reader() {
+ }
}
@Composable
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/UiComponent.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
index 494a9260..0026af91 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
@@ -10,6 +10,7 @@ import ca.gosyer.core.di.AppScope
import ca.gosyer.data.DataComponent
import ca.gosyer.ui.base.image.KamelConfigProvider
import ca.gosyer.ui.base.vm.ViewModelFactoryImpl
+import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.LocalViewModelFactory
import io.kamel.core.config.KamelConfig
import io.kamel.image.config.LocalKamelConfig
@@ -28,6 +29,8 @@ actual abstract class UiComponent(
actual abstract val kamelConfig: KamelConfig
+ actual abstract val contextWrapper: ContextWrapper
+
@get:AppScope
@get:Provides
protected actual val kamelConfigFactory: KamelConfig
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesWindow.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesWindow.kt
index e912c414..ce112d89 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesWindow.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/categories/CategoriesWindow.kt
@@ -6,22 +6,39 @@
package ca.gosyer.ui.categories
-import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.window.Window
import ca.gosyer.presentation.build.BuildKonfig
-import ca.gosyer.ui.AppComponent
-import ca.gosyer.ui.util.compose.ThemedWindow
-import ca.gosyer.ui.util.lang.launchApplication
import cafe.adriel.voyager.navigator.Navigator
-import kotlinx.coroutines.DelicateCoroutinesApi
-@OptIn(DelicateCoroutinesApi::class)
-actual fun openCategoriesMenu(notifyFinished: () -> Unit, navigator: Navigator) {
- launchApplication {
- CompositionLocalProvider(*remember { AppComponent.getInstance().uiComponent.getHooks() }) {
- ThemedWindow(::exitApplication, title = "${BuildKonfig.NAME} - Categories") {
+actual class CategoriesLauncher(private val notifyFinished: () -> Unit) {
+
+ private var isOpen by mutableStateOf(false)
+
+ actual fun open() {
+ isOpen = true
+ }
+
+ @Composable
+ actual fun CategoriesWindow() {
+ if (isOpen) {
+ Window(
+ onCloseRequest = { isOpen = false },
+ title = "${BuildKonfig.NAME} - Categories",
+ icon = painterResource("icon.png")
+ ) {
Navigator(remember { CategoriesScreen(notifyFinished) })
}
}
}
}
+
+@Composable
+actual fun rememberCategoriesLauncher(notifyFinished: () -> Unit): CategoriesLauncher {
+ return remember(notifyFinished) { CategoriesLauncher(notifyFinished) }
+}
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt
index 1788bd8d..9f6c8c3b 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/main/components/TrayViewModel.kt
@@ -7,23 +7,39 @@
package ca.gosyer.ui.main.components
import ca.gosyer.data.update.UpdateChecker
+import ca.gosyer.data.update.UpdatePreferences
+import ca.gosyer.data.update.model.GithubRelease
import ca.gosyer.uicore.vm.ContextWrapper
import ca.gosyer.uicore.vm.ViewModel
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import me.tatarka.inject.annotations.Inject
class TrayViewModel @Inject constructor(
- private val updateChecker: UpdateChecker,
+ updateChecker: UpdateChecker,
+ updatePreferences: UpdatePreferences,
contextWrapper: ContextWrapper
) : ViewModel(contextWrapper) {
override val scope = MainScope()
+ private val _updateFound = MutableSharedFlow()
+ val updateFound = _updateFound.asSharedFlow()
+
init {
- updateChecker.checkForUpdates()
+ if (updatePreferences.enabled().get()) {
+ updateChecker.checkForUpdates()
+ .onEach {
+ if (it is UpdateChecker.Update.UpdateFound) {
+ _updateFound.emit(it.release)
+ }
+ }
+ .launchIn(scope)
+ }
}
- val updateFound
- get() = updateChecker.updateFound
override fun onDispose() {
super.onDispose()
diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/DesktopReaderMenu.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/DesktopReaderMenu.kt
index cc84bda4..83e8e3c7 100644
--- a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/DesktopReaderMenu.kt
+++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/reader/DesktopReaderMenu.kt
@@ -9,32 +9,73 @@ package ca.gosyer.ui.reader
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.currentCompositionLocalContext
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.window.Window
-import androidx.compose.ui.window.WindowPlacement
+import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.rememberWindowState
-import ca.gosyer.data.ui.model.WindowSettings
import ca.gosyer.presentation.build.BuildKonfig
-import ca.gosyer.ui.AppComponent
-import ca.gosyer.ui.base.theme.AppTheme
-import ca.gosyer.ui.util.compose.WindowGet
import ca.gosyer.ui.util.lang.launchApplication
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
actual class ReaderLauncher {
+
+ private var isOpen by mutableStateOf?>(null)
+
actual fun launch(
chapterIndex: Int,
mangaId: Long
) {
- openReaderMenu(chapterIndex, mangaId)
+ isOpen = chapterIndex to mangaId
+ }
+
+ @OptIn(DelicateCoroutinesApi::class)
+ @Composable
+ actual fun Reader() {
+ val localParams = currentCompositionLocalContext
+ DisposableEffect(isOpen) {
+ isOpen?.let { (chapterIndex, mangaId) ->
+ launchApplication {
+ val scope = rememberCoroutineScope()
+ val hotkeyFlow = remember { MutableSharedFlow() }
+ val windowState = rememberWindowState(
+ position = WindowPosition.Aligned(Alignment.Center)
+ )
+ val icon = painterResource("icon.png")
+ CompositionLocalProvider(localParams) {
+ Window(
+ onCloseRequest = ::exitApplication,
+ title = "${BuildKonfig.NAME} - Reader",
+ icon = icon,
+ state = windowState,
+ onKeyEvent = {
+ if (it.type != KeyEventType.KeyDown) return@Window false
+ scope.launch {
+ hotkeyFlow.emit(it)
+ }
+ it.key in supportedKeyList
+ }
+ ) {
+ ReaderMenu(chapterIndex, mangaId, hotkeyFlow)
+ }
+ }
+ }
+ }
+ isOpen = null
+ onDispose {}
+ }
}
}
@@ -42,58 +83,3 @@ actual class ReaderLauncher {
actual fun rememberReaderLauncher(): ReaderLauncher {
return remember { ReaderLauncher() }
}
-
-@OptIn(DelicateCoroutinesApi::class)
-fun openReaderMenu(chapterIndex: Int, mangaId: Long) {
- val windowSettings = AppComponent.getInstance().dataComponent.uiPreferences
- .readerWindow()
- val (
- position,
- size,
- placement
- ) = WindowGet.from(windowSettings.get())
-
- val hooks = AppComponent.getInstance().uiComponent.getHooks()
-
- launchApplication {
- val scope = rememberCoroutineScope()
- val hotkeyFlow = remember { MutableSharedFlow() }
- val icon = painterResource("icon.png")
- val windowState = rememberWindowState(size = size, position = position, placement = placement)
- DisposableEffect(Unit) {
- onDispose {
- windowSettings.set(
- WindowSettings(
- windowState.position.x.value.toInt(),
- windowState.position.y.value.toInt(),
- windowState.size.width.value.toInt(),
- windowState.size.height.value.toInt(),
- windowState.placement == WindowPlacement.Maximized,
- windowState.placement == WindowPlacement.Fullscreen
- )
- )
- }
- }
- Window(
- onCloseRequest = ::exitApplication,
- title = "${BuildKonfig.NAME} - Reader",
- icon = icon,
- state = windowState,
- onKeyEvent = {
- if (it.type != KeyEventType.KeyDown) return@Window false
- scope.launch {
- hotkeyFlow.emit(it)
- }
- it.key in supportedKeyList
- }
- ) {
- CompositionLocalProvider(
- *hooks
- ) {
- AppTheme {
- ReaderMenu(chapterIndex, mangaId, hotkeyFlow)
- }
- }
- }
- }
-}
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/AppComponent.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/AppComponent.kt
deleted file mode 100644
index 8ed9f13c..00000000
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/AppComponent.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package ca.gosyer.ui
-
-import ca.gosyer.data.DataComponent
-import ca.gosyer.ui.base.UiComponent
-import ca.gosyer.uicore.vm.ContextWrapper
-
-expect abstract class AppComponent {
- val dataComponent: DataComponent
- val uiComponent: UiComponent
-
- abstract val contextWrapper: ContextWrapper
-}
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/UiComponent.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
index a3ddb20b..8ecdcc2f 100644
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
+++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/base/UiComponent.kt
@@ -10,6 +10,7 @@ import androidx.compose.runtime.ProvidedValue
import ca.gosyer.data.DataComponent
import ca.gosyer.ui.base.image.KamelConfigProvider
import ca.gosyer.ui.base.vm.ViewModelFactoryImpl
+import ca.gosyer.uicore.vm.ContextWrapper
import io.kamel.core.config.KamelConfig
expect abstract class UiComponent {
@@ -20,6 +21,8 @@ expect abstract class UiComponent {
abstract val kamelConfig: KamelConfig
+ abstract val contextWrapper: ContextWrapper
+
protected val kamelConfigFactory: KamelConfig
fun getHooks(): Array>
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt
index 8ae50917..2ec5b612 100644
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt
+++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/categories/CategoriesScreen.kt
@@ -13,9 +13,17 @@ import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
-import cafe.adriel.voyager.navigator.Navigator
-expect fun openCategoriesMenu(notifyFinished: () -> Unit, navigator: Navigator)
+expect class CategoriesLauncher {
+
+ fun open()
+
+ @Composable
+ fun CategoriesWindow()
+}
+
+@Composable
+expect fun rememberCategoriesLauncher(notifyFinished: () -> Unit): CategoriesLauncher
class CategoriesScreen(
@Transient
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt
index 93b7641c..8ed22517 100644
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt
+++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/manga/components/MangaScreenContent.kt
@@ -72,6 +72,7 @@ fun MangaScreenContent(
}
}
val readerLauncher = rememberReaderLauncher()
+ readerLauncher.Reader()
Scaffold(
topBar = {
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt
index 43b5665c..7c6b2c27 100644
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt
+++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/reader/ReaderMenu.kt
@@ -83,6 +83,9 @@ expect class ReaderLauncher {
chapterIndex: Int,
mangaId: Long
)
+
+ @Composable
+ fun Reader()
}
@Composable
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt
index 4817d7e9..9760d421 100644
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt
+++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/settings/SettingsLibraryScreen.kt
@@ -27,7 +27,7 @@ import ca.gosyer.ui.base.components.rememberScrollbarAdapter
import ca.gosyer.ui.base.navigation.Toolbar
import ca.gosyer.ui.base.prefs.PreferenceRow
import ca.gosyer.ui.base.prefs.SwitchPreference
-import ca.gosyer.ui.categories.openCategoriesMenu
+import ca.gosyer.ui.categories.rememberCategoriesLauncher
import ca.gosyer.uicore.prefs.PreferenceMutableStateFlow
import ca.gosyer.uicore.resources.stringResource
import ca.gosyer.uicore.vm.ContextWrapper
@@ -36,8 +36,6 @@ import ca.gosyer.uicore.vm.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
-import cafe.adriel.voyager.navigator.LocalNavigator
-import cafe.adriel.voyager.navigator.currentOrThrow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
@@ -51,12 +49,13 @@ class SettingsLibraryScreen : Screen {
@Composable
override fun Content() {
val vm = viewModel()
- val navigator = LocalNavigator.currentOrThrow
+ val categoriesLauncher = rememberCategoriesLauncher(vm::refreshCategoryCount)
SettingsLibraryScreenContent(
showAllCategory = vm.showAllCategory,
categoriesSize = vm.categories.collectAsState().value,
- openCategoriesScreen = { openCategoriesMenu(vm::refreshCategoryCount, navigator) }
+ openCategoriesScreen = categoriesLauncher::open
)
+ categoriesLauncher.CategoriesWindow()
}
}
diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt
index 2c5c4ba2..cf758d3e 100644
--- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt
+++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/updates/UpdatesScreen.kt
@@ -37,5 +37,6 @@ class UpdatesScreen : Screen {
deleteDownloadedChapter = vm::deleteDownloadedChapter,
stopDownloadingChapter = vm::stopDownloadingChapter
)
+ readerLauncher.Reader()
}
}