Draggable Dialog Bubble (#1979)

* Draggable Dialog Bubble

* Cleanup

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
renovate[bot]andDude so hot authored and GitHub committed 2024-11-26 14:30:04 +08:00
1 parent 464fa7c0c9
commit 2b803d4193
3 files changed
+78 -39

No files matched your search

@@ -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 <reified T> rememberMutableStateInDataStore(
key: String,
serializer: KSerializer<T> = serializer<T>(),
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 <reified T> rememberMutableStateInDataStore(
dataStoreScope.launch {
val keyObj = byteArrayPreferencesKey(key)
dataStore.edit { p ->
p[keyObj] = Cbor.encodeToByteArray(mutableState.value)
p[keyObj] = Cbor.encodeToByteArray(serializer, mutableState.value)
}
}
}
@@ -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
@@ -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<Offset> {
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)
}
}