From 5cdf715dd7f74a00df5850e38bcc6f78f4ccd444 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 5 Mar 2022 19:36:39 -0500 Subject: [PATCH] Small Rewrite - Allow App specific migrations - move AppComponent to apps - Update checker for android - Rewrite Reader and Categories to use a launcher --- android/build.gradle.kts | 1 + android/src/main/AndroidManifest.xml | 2 +- .../main/java/ca/gosyer/jui/android/App.kt | 3 +- .../ca/gosyer/jui/android}/AppComponent.kt | 21 ++-- .../ca/gosyer/jui/android/AppMigrations.kt | 34 ++++++ .../ca/gosyer/jui/android/MainActivity.kt | 4 +- .../ca/gosyer/jui/android/ReaderActivity.kt | 1 - .../{ => download}/AndroidDownloadService.kt | 5 +- .../data/{ => notification}/Notifications.kt | 2 +- .../android/data/update/UpdateCheckWorker.kt | 80 +++++++++++++ .../data/migration/MigrationPreferences.kt | 3 + .../ca/gosyer/data/update/UpdateChecker.kt | 34 +++--- .../main/kotlin/ca/gosyer}/AppComponent.kt | 19 ++- .../main/kotlin/ca/gosyer/AppMigrations.kt | 32 ++++++ desktop/src/main/kotlin/ca/gosyer/main.kt | 2 +- gradle/libs.versions.toml | 2 + .../kotlin/ca/gosyer/ui/base/UiComponent.kt | 3 + .../ca/gosyer/ui/categories/OpenCategories.kt | 20 +++- .../ca/gosyer/ui/reader/AndroidReaderMenu.kt | 4 + .../kotlin/ca/gosyer/ui/base/UiComponent.kt | 3 + .../gosyer/ui/categories/CategoriesWindow.kt | 37 ++++-- .../ui/main/components/TrayViewModel.kt | 24 +++- .../ca/gosyer/ui/reader/DesktopReaderMenu.kt | 108 ++++++++---------- .../kotlin/ca/gosyer/ui/AppComponent.kt | 18 --- .../kotlin/ca/gosyer/ui/base/UiComponent.kt | 3 + .../gosyer/ui/categories/CategoriesScreen.kt | 12 +- .../ui/manga/components/MangaScreenContent.kt | 1 + .../kotlin/ca/gosyer/ui/reader/ReaderMenu.kt | 3 + .../ui/settings/SettingsLibraryScreen.kt | 9 +- .../ca/gosyer/ui/updates/UpdatesScreen.kt | 1 + 30 files changed, 349 insertions(+), 142 deletions(-) rename {presentation/src/androidMain/kotlin/ca/gosyer/ui => android/src/main/java/ca/gosyer/jui/android}/AppComponent.kt (61%) create mode 100644 android/src/main/java/ca/gosyer/jui/android/AppMigrations.kt rename android/src/main/java/ca/gosyer/jui/android/data/{ => download}/AndroidDownloadService.kt (98%) rename android/src/main/java/ca/gosyer/jui/android/data/{ => notification}/Notifications.kt (98%) create mode 100644 android/src/main/java/ca/gosyer/jui/android/data/update/UpdateCheckWorker.kt rename {presentation/src/desktopMain/kotlin/ca/gosyer/ui => desktop/src/main/kotlin/ca/gosyer}/AppComponent.kt (58%) create mode 100644 desktop/src/main/kotlin/ca/gosyer/AppMigrations.kt delete mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/AppComponent.kt 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() } }