mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-23 21:12:34 +01:00
Small Rewrite
- Allow App specific migrations - move AppComponent to apps - Update checker for android - Rewrite Reader and Categories to use a launcher
This commit is contained in:
@@ -29,6 +29,7 @@ dependencies {
|
||||
implementation(libs.core)
|
||||
implementation(libs.appCompat)
|
||||
implementation(libs.activityCompose)
|
||||
implementation(libs.work)
|
||||
|
||||
// Android Lifecycle
|
||||
implementation(libs.lifecycleCommon)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".data.AndroidDownloadService"
|
||||
android:name=".data.download.AndroidDownloadService"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
34
android/src/main/java/ca/gosyer/jui/android/AppMigrations.kt
Normal file
34
android/src/main/java/ca/gosyer/jui/android/AppMigrations.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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<UpdateCheckWorker>(
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,7 @@ class MigrationPreferences(private val preferenceStore: PreferenceStore) {
|
||||
fun version(): Preference<Int> {
|
||||
return preferenceStore.getInt("version", 0)
|
||||
}
|
||||
fun appVersion(): Preference<Int> {
|
||||
return preferenceStore.getInt("app_version", 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GithubRelease>()
|
||||
val updateFound = _updateFound.asSharedFlow()
|
||||
fun checkForUpdates() = flow {
|
||||
// if (!updatePreferences.enabled().get()) return
|
||||
val latestRelease = client.get<GithubRelease>(
|
||||
"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<GithubRelease>("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
|
||||
|
||||
@@ -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
|
||||
|
||||
32
desktop/src/main/kotlin/ca/gosyer/AppMigrations.kt
Normal file
32
desktop/src/main/kotlin/ca/gosyer/AppMigrations.kt
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
@@ -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<GithubRelease>()
|
||||
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()
|
||||
|
||||
@@ -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<Pair<Int, Long>?>(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<KeyEvent>() }
|
||||
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<KeyEvent>() }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<ProvidedValue<out Any>>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -72,6 +72,7 @@ fun MangaScreenContent(
|
||||
}
|
||||
}
|
||||
val readerLauncher = rememberReaderLauncher()
|
||||
readerLauncher.Reader()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
||||
@@ -83,6 +83,9 @@ expect class ReaderLauncher {
|
||||
chapterIndex: Int,
|
||||
mangaId: Long
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Reader()
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -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<SettingsLibraryViewModel>()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,5 +37,6 @@ class UpdatesScreen : Screen {
|
||||
deleteDownloadedChapter = vm::deleteDownloadedChapter,
|
||||
stopDownloadingChapter = vm::stopDownloadingChapter
|
||||
)
|
||||
readerLauncher.Reader()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user