Draggable Dialog Bubble (#1979)
* Draggable Dialog Bubble * Cleanup --------- Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user