refactor: Rework Dialog

This commit is contained in:
Ahmad Ansori Palembani 2025-01-02 21:42:53 +07:00
parent 1a16d84e61
commit 49b10c1b4f
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
14 changed files with 223 additions and 130 deletions

View file

@ -5,8 +5,13 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.ViewCompositionStrategy
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import yokai.domain.DialogHostState
import yokai.presentation.theme.YokaiTheme import yokai.presentation.theme.YokaiTheme
abstract class BaseComposeController(bundle: Bundle? = null) : abstract class BaseComposeController(bundle: Bundle? = null) :
@ -25,8 +30,14 @@ abstract class BaseComposeController(bundle: Bundle? = null) :
) )
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent { setContent {
val dialogHostState = remember { DialogHostState() }
YokaiTheme { YokaiTheme {
ScreenContent() CompositionLocalProvider(
LocalDialogHostState provides dialogHostState,
LocalBackPress provides router::handleBack,
) {
ScreenContent()
}
} }
} }
} }

View file

@ -1014,7 +1014,12 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showNotificationPermissionPrompt() showNotificationPermissionPrompt()
AppUpdateNotifier.releasePageUrl = result.release.releaseLink AppUpdateNotifier.releasePageUrl = result.release.releaseLink
AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router) if (
// FIXME: Show Compose version of NewUpdateDialog for AboutController
router.backstack.lastOrNull()?.controller !is AboutController
) {
AboutController.NewUpdateDialogController(body, url, isBeta).showDialog(router)
}
} }
} }
} catch (error: Exception) { } catch (error: Exception) {
@ -1037,6 +1042,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
@SuppressLint("MissingSuperCall") @SuppressLint("MissingSuperCall")
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
splashState.ready = true
if (!handleIntentAction(intent)) { if (!handleIntentAction(intent)) {
super.onNewIntent(intent) super.onNewIntent(intent)
} }
@ -1092,7 +1098,11 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
SHORTCUT_UPDATE_NOTES -> { SHORTCUT_UPDATE_NOTES -> {
val extras = intent.extras ?: return false val extras = intent.extras ?: return false
if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library if (router.backstack.isEmpty()) nav.selectedItemId = R.id.nav_library
if (router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController) { if (
router.backstack.lastOrNull()?.controller !is AboutController.NewUpdateDialogController &&
// FIXME: Show Compose version of NewUpdateDialog for AboutController
router.backstack.lastOrNull()?.controller !is AboutController
) {
AboutController.NewUpdateDialogController(extras).showDialog(router) AboutController.NewUpdateDialogController(extras).showDialog(router)
} }
} }
@ -1122,7 +1132,6 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
else -> return false else -> return false
} }
splashState.ready = true
return true return true
} }

View file

@ -1,28 +1,24 @@
package eu.kanade.tachiyomi.ui.more package eu.kanade.tachiyomi.ui.more
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Spanned
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import cafe.adriel.voyager.core.stack.StackEvent
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.CrossfadeTransition import cafe.adriel.voyager.transitions.CrossfadeTransition
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.LocalRouter
import eu.kanade.tachiyomi.util.system.materialAlertDialog import eu.kanade.tachiyomi.util.system.materialAlertDialog
import eu.kanade.tachiyomi.util.view.setNegativeButton import eu.kanade.tachiyomi.util.view.setNegativeButton
import eu.kanade.tachiyomi.util.view.setPositiveButton import eu.kanade.tachiyomi.util.view.setPositiveButton
import eu.kanade.tachiyomi.util.view.setTitle import eu.kanade.tachiyomi.util.view.setTitle
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import yokai.domain.ComposableAlertDialog
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.settings.screen.about.AboutScreen import yokai.presentation.settings.screen.about.AboutScreen
import android.R as AR import android.R as AR
@ -34,17 +30,12 @@ class AboutController : BaseComposeController() {
Navigator( Navigator(
screen = AboutScreen(), screen = AboutScreen(),
content = { content = {
CompositionLocalProvider( CrossfadeTransition(navigator = it)
LocalAlertDialog provides ComposableAlertDialog(null),
LocalBackPress provides router::handleBack,
LocalRouter provides router,
) {
CrossfadeTransition(navigator = it)
}
}, },
) )
} }
@Deprecated("Use [DialogHostState.showNewUpdateDialog] instead", ReplaceWith("DialogHostState.showNewUpdateDialog()"))
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) { class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
constructor(body: String, url: String, isBeta: Boolean?) : this( constructor(body: String, url: String, isBeta: Boolean?) : this(
@ -56,9 +47,7 @@ class AboutController : BaseComposeController() {
) )
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val releaseBody = (args.getString(BODY_KEY) ?: "") val info = activity!!.parseReleaseNotes(args.getString(BODY_KEY) ?: "")
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val isBeta = args.getBoolean(IS_BETA, false) val isBeta = args.getBoolean(IS_BETA, false)
@ -96,3 +85,8 @@ class AboutController : BaseComposeController() {
} }
} }
} }
fun Context.parseReleaseNotes(releaseNotes: String): Spanned {
val releaseBody = releaseNotes.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
return Markwon.create(this).toMarkdown(releaseBody)
}

View file

@ -1,11 +1,7 @@
package eu.kanade.tachiyomi.ui.setting package eu.kanade.tachiyomi.ui.setting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import yokai.domain.ComposableAlertDialog
import yokai.presentation.settings.ComposableSettings import yokai.presentation.settings.ComposableSettings
abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface { abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface {
@ -18,11 +14,6 @@ abstract class SettingsComposeController: BaseComposeController(), SettingsContr
@Composable @Composable
override fun ScreenContent() { override fun ScreenContent() {
CompositionLocalProvider( getComposableSettings().Content()
LocalAlertDialog provides ComposableAlertDialog(null),
LocalBackPress provides router::handleBack,
) {
getComposableSettings().Content()
}
} }
} }

View file

@ -5,14 +5,14 @@ import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import yokai.domain.ComposableAlertDialog import yokai.domain.DialogHostState
val <T> ProvidableCompositionLocal<T?>.currentOrThrow val <T> ProvidableCompositionLocal<T?>.currentOrThrow
@Composable @Composable
get(): T = this.current ?: throw RuntimeException("CompositionLocal is null") get(): T = this.current ?: throw RuntimeException("CompositionLocal is null")
val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null } val LocalBackPress: ProvidableCompositionLocal<(() -> Unit)?> = staticCompositionLocalOf { null }
val LocalAlertDialog: ProvidableCompositionLocal<ComposableAlertDialog?> = compositionLocalOf { null } val LocalDialogHostState: ProvidableCompositionLocal<DialogHostState?> = compositionLocalOf { null }
@Deprecated( @Deprecated(
message = "Scheduled for removal once Conductor is fully replaced by Voyager", message = "Scheduled for removal once Conductor is fully replaced by Voyager",
replaceWith = ReplaceWith("LocalNavigator", "cafe.adriel.voyager.navigator.LocalNavigator"), replaceWith = ReplaceWith("LocalNavigator", "cafe.adriel.voyager.navigator.LocalNavigator"),

View file

@ -1,10 +0,0 @@
package yokai.domain
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class ComposableAlertDialog(initial: (@Composable () -> Unit)?) {
var content: (@Composable () -> Unit)? by mutableStateOf(initial)
}

View file

@ -0,0 +1,28 @@
package yokai.domain
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
typealias ComposableDialog = (@Composable () -> Unit)?
typealias ComposableDialogState = MutableState<ComposableDialog>
class DialogHostState(initial: ComposableDialog = null) : ComposableDialogState by mutableStateOf(initial) {
val mutex = Mutex()
fun closeDialog() {
value = null
}
suspend inline fun <R> dialog(crossinline dialog: @Composable (CancellableContinuation<R>) -> Unit) = mutex.withLock {
try {
suspendCancellableCoroutine { cont -> value = { dialog(cont) } }
} finally {
closeDialog()
}
}
}

View file

@ -1,11 +1,7 @@
package yokai.presentation.extension.repo package yokai.presentation.extension.repo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import yokai.domain.ComposableAlertDialog
class ExtensionRepoController() : class ExtensionRepoController() :
BaseComposeController() { BaseComposeController() {
@ -18,14 +14,9 @@ class ExtensionRepoController() :
@Composable @Composable
override fun ScreenContent() { override fun ScreenContent() {
CompositionLocalProvider( ExtensionRepoScreen(
LocalAlertDialog provides ComposableAlertDialog(null), title = "Extension Repos",
LocalBackPress provides router::handleBack, repoUrl = repoUrl,
) { )
ExtensionRepoScreen(
title = "Extension Repos",
repoUrl = repoUrl,
)
}
} }
} }

View file

@ -19,6 +19,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -26,13 +27,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import eu.kanade.tachiyomi.util.compose.currentOrThrow import eu.kanade.tachiyomi.util.compose.currentOrThrow
import eu.kanade.tachiyomi.util.isTablet import eu.kanade.tachiyomi.util.isTablet
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import yokai.domain.ComposableAlertDialog import kotlinx.coroutines.launch
import yokai.domain.DialogHostState
import yokai.domain.extension.repo.model.ExtensionRepo import yokai.domain.extension.repo.model.ExtensionRepo
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.AppBarType import yokai.presentation.AppBarType
@ -51,10 +53,12 @@ fun ExtensionRepoScreen(
) { ) {
val onBackPress = LocalBackPress.currentOrThrow val onBackPress = LocalBackPress.currentOrThrow
val context = LocalContext.current val context = LocalContext.current
val alertDialog = LocalDialogHostState.currentOrThrow
val scope = rememberCoroutineScope()
val repoState by viewModel.repoState.collectAsState() val repoState by viewModel.repoState.collectAsState()
var inputText by remember { mutableStateOf("") } var inputText by remember { mutableStateOf("") }
val listState = rememberLazyListState() val listState = rememberLazyListState()
val alertDialog = LocalAlertDialog.currentOrThrow
YokaiScaffold( YokaiScaffold(
onNavigationIconClicked = onBackPress, onNavigationIconClicked = onBackPress,
@ -79,7 +83,7 @@ fun ExtensionRepoScreen(
val repos = (repoState as ExtensionRepoState.Success).repos val repos = (repoState as ExtensionRepoState.Success).repos
alertDialog.content?.let { it() } alertDialog.value?.invoke()
LazyColumn( LazyColumn(
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
@ -113,7 +117,7 @@ fun ExtensionRepoScreen(
ExtensionRepoItem( ExtensionRepoItem(
extensionRepo = repo, extensionRepo = repo,
onDeleteClick = { repoToDelete -> onDeleteClick = { repoToDelete ->
alertDialog.content = { ExtensionRepoDeletePrompt(repoToDelete, alertDialog, viewModel) } scope.launch { alertDialog.awaitExtensionRepoDeletePrompt(repoToDelete, viewModel) }
}, },
) )
} }
@ -127,46 +131,45 @@ fun ExtensionRepoScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.event.collectLatest { event -> viewModel.event.collectLatest { event ->
if (event is ExtensionRepoEvent.LocalizedMessage) when (event) {
context.toast(event.stringRes) is ExtensionRepoEvent.NoOp -> {}
if (event is ExtensionRepoEvent.Success) is ExtensionRepoEvent.LocalizedMessage -> context.toast(event.stringRes)
inputText = "" is ExtensionRepoEvent.Success -> inputText = ""
if (event is ExtensionRepoEvent.ShowDialog) is ExtensionRepoEvent.ShowDialog -> {
alertDialog.content = { when(event.dialog) {
if (event.dialog is RepoDialog.Conflict) { is RepoDialog.Conflict -> {
ExtensionRepoReplacePrompt( alertDialog.awaitExtensionRepoReplacePrompt(
oldRepo = event.dialog.oldRepo, oldRepo = event.dialog.oldRepo,
newRepo = event.dialog.newRepo, newRepo = event.dialog.newRepo,
onDismissRequest = { alertDialog.content = null }, onMigrate = { viewModel.replaceRepo(event.dialog.newRepo) },
onMigrate = { viewModel.replaceRepo(event.dialog.newRepo) }, )
) }
} }
} }
}
} }
} }
} }
@Composable suspend fun DialogHostState.awaitExtensionRepoReplacePrompt(
fun ExtensionRepoReplacePrompt(
oldRepo: ExtensionRepo, oldRepo: ExtensionRepo,
newRepo: ExtensionRepo, newRepo: ExtensionRepo,
onDismissRequest: () -> Unit,
onMigrate: () -> Unit, onMigrate: () -> Unit,
) { ): Unit = dialog { cont ->
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = { cont.cancel() },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
onMigrate() onMigrate()
onDismissRequest() cont.cancel()
}, },
) { ) {
Text(text = stringResource(MR.strings.action_replace_repo)) Text(text = stringResource(MR.strings.action_replace_repo))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = { cont.cancel() }) {
Text(text = stringResource(AR.string.cancel)) Text(text = stringResource(AR.string.cancel))
} }
}, },
@ -179,8 +182,10 @@ fun ExtensionRepoReplacePrompt(
) )
} }
@Composable suspend fun DialogHostState.awaitExtensionRepoDeletePrompt(
fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlertDialog, viewModel: ExtensionRepoViewModel) { repoToDelete: String,
viewModel: ExtensionRepoViewModel,
): Unit = dialog { cont ->
AlertDialog( AlertDialog(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
title = { title = {
@ -199,12 +204,12 @@ fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlert
fontSize = 14.sp, fontSize = 14.sp,
) )
}, },
onDismissRequest = { alertDialog.content = null }, onDismissRequest = { cont.cancel() },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
viewModel.deleteRepo(repoToDelete) viewModel.deleteRepo(repoToDelete)
alertDialog.content = null cont.cancel()
} }
) { ) {
Text( Text(
@ -215,7 +220,7 @@ fun ExtensionRepoDeletePrompt(repoToDelete: String, alertDialog: ComposableAlert
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { alertDialog.content = null }) { TextButton(onClick = { cont.cancel() }) {
Text( Text(
text = stringResource(MR.strings.cancel), text = stringResource(MR.strings.cancel),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,

View file

@ -17,8 +17,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastForEachIndexed
import eu.kanade.tachiyomi.core.storage.preference.collectAsState import eu.kanade.tachiyomi.core.storage.preference.collectAsState
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress import eu.kanade.tachiyomi.util.compose.LocalBackPress
import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import eu.kanade.tachiyomi.util.compose.currentOrThrow import eu.kanade.tachiyomi.util.compose.currentOrThrow
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -44,7 +44,7 @@ fun SettingsScaffold(
val preferences: PreferencesHelper by injectLazy() val preferences: PreferencesHelper by injectLazy()
val useLargeAppBar by preferences.useLargeToolbar().collectAsState() val useLargeAppBar by preferences.useLargeToolbar().collectAsState()
val onBackPress = LocalBackPress.currentOrThrow val onBackPress = LocalBackPress.currentOrThrow
val alertDialog = LocalAlertDialog.currentOrThrow val alertDialog = LocalDialogHostState.currentOrThrow
YokaiScaffold( YokaiScaffold(
onNavigationIconClicked = onBackPress, onNavigationIconClicked = onBackPress,
@ -54,7 +54,7 @@ fun SettingsScaffold(
scrollBehavior = appBarScrollBehavior, scrollBehavior = appBarScrollBehavior,
snackbarHost = snackbarHost, snackbarHost = snackbarHost,
) { innerPadding -> ) { innerPadding ->
alertDialog.content?.let { it() } alertDialog.value?.invoke()
content(innerPadding) content(innerPadding)
} }

View file

@ -33,11 +33,10 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import eu.kanade.tachiyomi.util.compose.currentOrThrow import eu.kanade.tachiyomi.util.compose.currentOrThrow
import eu.kanade.tachiyomi.util.relativeTimeSpanString import eu.kanade.tachiyomi.util.relativeTimeSpanString
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.launchNonCancellableIO import eu.kanade.tachiyomi.util.system.launchNonCancellableIO
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.system.withUIContext
@ -57,9 +56,9 @@ import yokai.presentation.component.preference.storageLocationText
import yokai.presentation.component.preference.widget.BasePreferenceWidget import yokai.presentation.component.preference.widget.BasePreferenceWidget
import yokai.presentation.component.preference.widget.PrefsHorizontalPadding import yokai.presentation.component.preference.widget.PrefsHorizontalPadding
import yokai.presentation.settings.ComposableSettings import yokai.presentation.settings.ComposableSettings
import yokai.presentation.settings.screen.data.CreateBackup
import yokai.presentation.settings.screen.data.RestoreBackup
import yokai.presentation.settings.screen.data.StorageInfo import yokai.presentation.settings.screen.data.StorageInfo
import yokai.presentation.settings.screen.data.awaitCreateBackup
import yokai.presentation.settings.screen.data.awaitRestoreBackup
import yokai.presentation.settings.screen.data.storageLocationPicker import yokai.presentation.settings.screen.data.storageLocationPicker
import yokai.util.lang.getString import yokai.util.lang.getString
@ -101,7 +100,7 @@ object SettingsDataScreen : ComposableSettings {
private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup { private fun getBackupAndRestoreGroup(backupPreferences: BackupPreferences): Preference.PreferenceGroup {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val alertDialog = LocalAlertDialog.currentOrThrow val alertDialog = LocalDialogHostState.currentOrThrow
val extensionManager = remember { Injekt.get<ExtensionManager>() } val extensionManager = remember { Injekt.get<ExtensionManager>() }
val storageManager = remember { Injekt.get<StorageManager>() } val storageManager = remember { Injekt.get<StorageManager>() }
@ -122,14 +121,11 @@ object SettingsDataScreen : ComposableSettings {
Pair(null, e) Pair(null, e)
} }
alertDialog.content = { scope.launch {
RestoreBackup( alertDialog.awaitRestoreBackup(
context = context, context = context,
uri = it, uri = it,
pair = results, pair = results,
onDismissRequest = {
alertDialog.content = null
}
) )
} }
} }
@ -166,11 +162,10 @@ object SettingsDataScreen : ComposableSettings {
return@SegmentedButton return@SegmentedButton
} }
alertDialog.content = { scope.launch {
CreateBackup( alertDialog.awaitCreateBackup(
context = context, context = context,
uri = dir.uri, uri = dir.uri,
onDismissRequest = { alertDialog.content = null },
) )
} }
} else { } else {

View file

@ -0,0 +1,83 @@
package yokai.presentation.settings.screen.about
import android.os.Build
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.material.textview.MaterialTextView
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.ui.more.parseReleaseNotes
import java.io.Serializable
import yokai.domain.DialogHostState
import yokai.i18n.MR
data class NewUpdateData(
val body: String,
val url: String,
val isBeta: Boolean?,
) : Serializable
suspend fun DialogHostState.awaitNewUpdateDialog(
data: NewUpdateData,
onDismiss: () -> Unit = {},
): Unit = dialog { cont ->
val context = LocalContext.current
val appContext = context.applicationContext
val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
AlertDialog(
onDismissRequest = {
onDismiss()
cont.cancel()
},
title = {
Text(
text = stringResource(
if (data.isBeta == true) {
MR.strings.new_beta_version_available
} else {
MR.strings.new_version_available
}
)
)
},
confirmButton = {
TextButton(onClick = {
AppDownloadInstallJob.start(appContext, data.url, true)
onDismiss()
cont.cancel()
}) {
Text(text = stringResource(if (isOnA12) MR.strings.update else MR.strings.download))
}
},
dismissButton = {
TextButton(
onClick = {
onDismiss()
cont.cancel()
}
) {
Text(text = stringResource(MR.strings.ignore))
}
},
text = { MarkdownText(data.body) }
)
}
@Composable
private fun MarkdownText(text: String) {
val context = LocalContext.current
AndroidView(
factory = {
MaterialTextView(it)
},
update = {
it.text = context.parseReleaseNotes(text)
},
)
}

View file

@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.bluelinelabs.conductor.Router
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.storage.preference.asDateFormat import eu.kanade.tachiyomi.core.storage.preference.asDateFormat
@ -36,9 +35,8 @@ import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier import eu.kanade.tachiyomi.data.updater.AppUpdateNotifier
import eu.kanade.tachiyomi.data.updater.AppUpdateResult import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.RELEASE_URL import eu.kanade.tachiyomi.data.updater.RELEASE_URL
import eu.kanade.tachiyomi.ui.more.AboutController.NewUpdateDialogController
import eu.kanade.tachiyomi.util.CrashLogUtil import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.compose.LocalRouter import eu.kanade.tachiyomi.util.compose.LocalDialogHostState
import eu.kanade.tachiyomi.util.compose.currentOrThrow import eu.kanade.tachiyomi.util.compose.currentOrThrow
import eu.kanade.tachiyomi.util.lang.toTimestampString import eu.kanade.tachiyomi.util.lang.toTimestampString
import eu.kanade.tachiyomi.util.system.isOnline import eu.kanade.tachiyomi.util.system.isOnline
@ -52,6 +50,7 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import yokai.domain.DialogHostState
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.component.preference.widget.TextPreferenceWidget import yokai.presentation.component.preference.widget.TextPreferenceWidget
import yokai.presentation.core.components.LinkIcon import yokai.presentation.core.components.LinkIcon
@ -68,7 +67,7 @@ class AboutScreen : Screen() {
override fun Content() { override fun Content() {
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow val navigator = LocalNavigator.currentOrThrow
val router = LocalRouter.currentOrThrow val dialogHostState = LocalDialogHostState.currentOrThrow
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
@ -109,7 +108,7 @@ class AboutScreen : Screen() {
onPreferenceClick = { onPreferenceClick = {
if (context.isOnline()) { if (context.isOnline()) {
scope.launch { scope.launch {
context.checkVersion(router) context.checkVersion(dialogHostState)
} }
} else { } else {
context.toast(MR.strings.no_network_connection) context.toast(MR.strings.no_network_connection)
@ -193,7 +192,7 @@ class AboutScreen : Screen() {
else -> "Release ${BuildConfig.VERSION_NAME}" else -> "Release ${BuildConfig.VERSION_NAME}"
} }
private suspend fun Context.checkVersion(router: Router) { private suspend fun Context.checkVersion(dialogState: DialogHostState) {
val updateChecker = AppUpdateChecker() val updateChecker = AppUpdateChecker()
withUIContext { toast(MR.strings.searching_for_updates) } withUIContext { toast(MR.strings.searching_for_updates) }
@ -208,14 +207,16 @@ class AboutScreen : Screen() {
} }
when (result) { when (result) {
is AppUpdateResult.NewUpdate -> { is AppUpdateResult.NewUpdate -> {
val body = result.release.info val data = NewUpdateData(
val url = result.release.downloadLink result.release.info,
val isBeta = result.release.preRelease == true result.release.downloadLink,
result.release.preRelease == true
)
// Create confirmation window // Create confirmation window
withUIContext { withUIContext {
AppUpdateNotifier.releasePageUrl = result.release.releaseLink AppUpdateNotifier.releasePageUrl = result.release.releaseLink
NewUpdateDialogController(body, url, isBeta).showDialog(router) dialogState.awaitNewUpdateDialog(data)
} }
} }
is AppUpdateResult.NoNewUpdate -> { is AppUpdateResult.NoNewUpdate -> {

View file

@ -8,10 +8,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
@ -22,17 +20,16 @@ import eu.kanade.tachiyomi.data.backup.create.BackupOptions
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import yokai.domain.DialogHostState
import yokai.i18n.MR import yokai.i18n.MR
import yokai.presentation.component.LabeledCheckbox import yokai.presentation.component.LabeledCheckbox
import android.R as AR import android.R as AR
@Composable suspend fun DialogHostState.awaitRestoreBackup(
fun RestoreBackup(
context: Context, context: Context,
uri: Uri, uri: Uri,
pair: Pair<Results?, Exception?>, pair: Pair<Results?, Exception?>,
onDismissRequest: () -> Unit, ): Unit = dialog { cont ->
) {
val (results, e) = pair val (results, e) = pair
if (results != null) { if (results != null) {
var message = stringResource(MR.strings.restore_content_full) var message = stringResource(MR.strings.restore_content_full)
@ -52,20 +49,20 @@ fun RestoreBackup(
} }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = { cont.cancel() },
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
context.toast(MR.strings.restoring_backup) context.toast(MR.strings.restoring_backup)
BackupRestoreJob.start(context, uri) BackupRestoreJob.start(context, uri)
onDismissRequest() cont.cancel()
}, },
) { ) {
Text(text = stringResource(MR.strings.restore)) Text(text = stringResource(MR.strings.restore))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = { cont.cancel() }) {
Text(text = stringResource(AR.string.cancel)) Text(text = stringResource(AR.string.cancel))
} }
}, },
@ -74,9 +71,9 @@ fun RestoreBackup(
) )
} else { } else {
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = { cont.cancel() },
confirmButton = { confirmButton = {
TextButton(onClick = onDismissRequest) { TextButton(onClick = { cont.cancel() }) {
Text(text = stringResource(AR.string.cancel)) Text(text = stringResource(AR.string.cancel))
} }
}, },
@ -86,29 +83,27 @@ fun RestoreBackup(
} }
} }
@Composable suspend fun DialogHostState.awaitCreateBackup(
fun CreateBackup(
context: Context, context: Context,
uri: Uri, uri: Uri,
onDismissRequest: () -> Unit, ): Unit = dialog { cont ->
) { var options by mutableStateOf(BackupOptions())
var options by remember { mutableStateOf(BackupOptions()) }
AlertDialog( AlertDialog(
onDismissRequest = onDismissRequest, onDismissRequest = { cont.cancel() },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
val actualUri = val actualUri =
UniFile.fromUri(context, uri)?.createFile(Backup.getBackupFilename())?.uri ?: return@TextButton UniFile.fromUri(context, uri)?.createFile(Backup.getBackupFilename())?.uri ?: return@TextButton
context.toast(MR.strings.creating_backup) context.toast(MR.strings.creating_backup)
BackupCreatorJob.startNow(context, actualUri, options) BackupCreatorJob.startNow(context, actualUri, options)
onDismissRequest() cont.cancel()
}) { }) {
Text(stringResource(MR.strings.create)) Text(stringResource(MR.strings.create))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { onDismissRequest() }) { TextButton(onClick = { cont.cancel() }) {
Text(stringResource(MR.strings.cancel)) Text(stringResource(MR.strings.cancel))
} }
}, },