diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/DateStoreState.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/DateStoreState.kt index 729d1aeec..eb9beedfd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/DateStoreState.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/DateStoreState.kt @@ -16,9 +16,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import kotlinx.serialization.KSerializer import kotlinx.serialization.cbor.Cbor -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.serializer import splitties.init.appCtx val dataStoreScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @@ -29,11 +29,12 @@ val dataStateFlow = dataStore.data.stateIn(dataStoreScope, SharingStarted.Eagerl @Composable inline fun rememberMutableStateInDataStore( key: String, + serializer: KSerializer = serializer(), crossinline defaultValue: @DisallowComposableCalls () -> T, ) = with(dataStateFlow) { remember { val keyObj = byteArrayPreferencesKey(key) - val r = value[keyObj]?.let { bytes -> Cbor.decodeFromByteArray(bytes) } ?: defaultValue() + val r = value[keyObj]?.let { bytes -> Cbor.decodeFromByteArray(serializer, bytes) } ?: defaultValue() mutableStateOf(r) }.also { mutableState -> LifecycleResumeEffect(key) { @@ -41,7 +42,7 @@ inline fun rememberMutableStateInDataStore( dataStoreScope.launch { val keyObj = byteArrayPreferencesKey(key) dataStore.edit { p -> - p[keyObj] = Cbor.encodeToByteArray(mutableState.value) + p[keyObj] = Cbor.encodeToByteArray(serializer, mutableState.value) } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt index a976a3a1f..95425185d 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt @@ -2,7 +2,6 @@ package com.hippo.ehviewer.ui.tools import android.content.Context import androidx.annotation.StringRes -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -19,7 +18,6 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -49,7 +47,6 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.InputChip @@ -84,17 +81,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.lerp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.SecureFlagPolicy @@ -738,34 +731,6 @@ class DialogState : MutableComposable by mutableStateOf(null) { } } -@Composable -fun BoxScope.PausableAlertDialog( - confirmButton: @Composable () -> Unit, - dismissButton: @Composable () -> Unit, - title: @Composable () -> Unit, - text: @Composable () -> Unit, - idleIcon: ImageVector, -) { - var showDialog by remember { mutableStateOf(true) } - if (showDialog) { - AlertDialog( - onDismissRequest = { showDialog = false }, - confirmButton = confirmButton, - dismissButton = dismissButton, - title = title, - text = text, - ) - } - val fraction by animateFloatAsState(if (showDialog) 0f else 1f) - val width = LocalWindowInfo.current.containerSize.width - FilledTonalIconButton( - onClick = { showDialog = true }, - modifier = Modifier.offset { IntOffset(x = lerp(width / 2, 0, fraction), y = 0) }.graphicsLayer { alpha = fraction }.align(Alignment.CenterStart), - ) { - Icon(imageVector = idleIcon, contentDescription = null) - } -} - @Composable private fun CheckableItem(text: String, checked: Boolean, modifier: Modifier = Modifier) { val textStyle = MaterialTheme.typography.titleMedium diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt new file mode 100644 index 000000000..6381551d6 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/PausableDialog.kt @@ -0,0 +1,73 @@ +package com.hippo.ehviewer.ui.tools + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.gestures.draggable2D +import androidx.compose.foundation.gestures.rememberDraggable2DState +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.offset +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.Icon +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.round +import androidx.compose.ui.util.lerp +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +fun lerp(start: IntOffset, stop: IntOffset, fraction: Float) = IntOffset( + lerp(start.x, stop.x, fraction), + lerp(start.y, stop.y, fraction), +) + +object OffsetSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("Offset", PrimitiveKind.LONG) + override fun deserialize(decoder: Decoder) = Offset(decoder.decodeLong()) + override fun serialize(encoder: Encoder, value: Offset) = encoder.encodeLong(value.packedValue) +} + +@Composable +fun BoxScope.PausableAlertDialog( + confirmButton: @Composable () -> Unit, + dismissButton: @Composable () -> Unit, + title: @Composable () -> Unit, + text: @Composable () -> Unit, + idleIcon: ImageVector, +) { + var showDialog by remember { mutableStateOf(true) } + if (showDialog) { + AlertDialog( + onDismissRequest = { showDialog = false }, + confirmButton = confirmButton, + dismissButton = dismissButton, + title = title, + text = text, + ) + } + val fraction by animateFloatAsState(if (showDialog) 0f else 1f) + val info = LocalWindowInfo.current.containerSize + + // Disappear at screen center + val disappear = IntOffset(info.width / 2, 0) + var idle by rememberMutableStateInDataStore("DialogBubble", OffsetSerializer) { Offset.Zero } + val state = rememberDraggable2DState { delta -> idle += delta } + FilledTonalIconButton( + onClick = { showDialog = true }, + modifier = Modifier.offset { lerp(disappear, idle.round(), fraction) }.graphicsLayer { alpha = fraction }.align(Alignment.CenterStart).draggable2D(state = state), + ) { + Icon(imageVector = idleIcon, contentDescription = null) + } +}