feat: интегрировать LockIcon, SlideToCancel, waveform и controls в панель записи
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -896,6 +896,7 @@ fun MessageInputBar(
|
||||
var pressStartY by remember { mutableFloatStateOf(0f) }
|
||||
var slideDx by remember { mutableFloatStateOf(0f) }
|
||||
var slideDy by remember { mutableFloatStateOf(0f) }
|
||||
var lockProgress by remember { mutableFloatStateOf(0f) }
|
||||
var pendingLongPressJob by remember { mutableStateOf<Job?>(null) }
|
||||
var pendingRecordAfterPermission by remember { mutableStateOf(false) }
|
||||
var voiceRecordStartedAtMs by remember { mutableLongStateOf(0L) }
|
||||
@@ -946,6 +947,7 @@ fun MessageInputBar(
|
||||
slideDy = 0f
|
||||
pressStartX = 0f
|
||||
pressStartY = 0f
|
||||
lockProgress = 0f
|
||||
pendingLongPressJob?.cancel()
|
||||
pendingLongPressJob = null
|
||||
}
|
||||
@@ -1197,6 +1199,12 @@ fun MessageInputBar(
|
||||
val cancelDragThresholdPx = with(density) { 92.dp.toPx() }
|
||||
val lockDragThresholdPx = with(density) { 70.dp.toPx() }
|
||||
|
||||
var showLockTooltip by remember { mutableStateOf(false) }
|
||||
val lockHintShownCount = remember {
|
||||
context.getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
||||
.getInt(LOCK_HINT_PREF_KEY, 0)
|
||||
}
|
||||
|
||||
fun tryStartRecordingForCurrentMode(): Boolean {
|
||||
return if (recordMode == RecordMode.VOICE) {
|
||||
setRecordUiState(RecordUiState.RECORDING, "hold-threshold-passed")
|
||||
@@ -1233,6 +1241,29 @@ fun MessageInputBar(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(recordUiState) {
|
||||
if (recordUiState == RecordUiState.RECORDING && lockHintShownCount < LOCK_HINT_MAX_SHOWS) {
|
||||
delay(200)
|
||||
if (recordUiState == RecordUiState.RECORDING) {
|
||||
showLockTooltip = true
|
||||
context.getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putInt(LOCK_HINT_PREF_KEY, lockHintShownCount + 1)
|
||||
.apply()
|
||||
delay(3000)
|
||||
showLockTooltip = false
|
||||
}
|
||||
} else {
|
||||
showLockTooltip = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(lockProgress) {
|
||||
if (lockProgress > 0.2f) {
|
||||
showLockTooltip = false
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
pendingRecordAfterPermission = false
|
||||
@@ -1909,9 +1940,9 @@ fun MessageInputBar(
|
||||
.heightIn(min = 48.dp)
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
recordingInputRowHeightPx = coordinates.size.height
|
||||
recordingInputRowY = coordinates.positionInWindow().y
|
||||
},
|
||||
recordingInputRowHeightPx = coordinates.size.height
|
||||
recordingInputRowY = coordinates.positionInWindow().y
|
||||
},
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Box(
|
||||
@@ -1920,8 +1951,9 @@ fun MessageInputBar(
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(recordingPanelColor)
|
||||
.padding(start = 13.dp, end = 94.dp) // record panel paddings
|
||||
.padding(start = 13.dp, end = 94.dp)
|
||||
) {
|
||||
// Left: blink dot + timer (all states)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterStart)
|
||||
@@ -1931,8 +1963,25 @@ fun MessageInputBar(
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RecordBlinkDot(isDarkTheme = isDarkTheme)
|
||||
Spacer(modifier = Modifier.width(6.dp)) // TimerView margin from RecordDot
|
||||
if (recordUiState == RecordUiState.PAUSED) {
|
||||
// Static dot (no blink) when paused
|
||||
Box(
|
||||
modifier = Modifier.size(28.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (isDarkTheme) Color(0xFFFF5A5A) else Color(0xFFE84D4D)
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
RecordBlinkDot(isDarkTheme = isDarkTheme)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = formatVoiceRecordTimer(voiceElapsedMs),
|
||||
color = recordingTextColor,
|
||||
@@ -1941,67 +1990,107 @@ fun MessageInputBar(
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text =
|
||||
if (recordUiState == RecordUiState.LOCKED) {
|
||||
"CANCEL"
|
||||
} else {
|
||||
"Slide left to cancel • up to lock"
|
||||
// Center: SlideToCancel or Waveform+Controls
|
||||
AnimatedContent(
|
||||
targetState = recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
},
|
||||
color = if (recordUiState == RecordUiState.LOCKED) PrimaryBlue else recordingTextColor.copy(alpha = 0.82f),
|
||||
fontSize = if (recordUiState == RecordUiState.LOCKED) 15.sp else 13.sp,
|
||||
fontWeight = if (recordUiState == RecordUiState.LOCKED) FontWeight.Bold else FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.graphicsLayer {
|
||||
alpha = recordUiAlpha
|
||||
translationX = with(density) { recordUiShift.toPx() }
|
||||
}
|
||||
.then(
|
||||
if (recordUiState == RecordUiState.LOCKED) {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
inputJumpLog(
|
||||
"tap CANCEL (locked) mode=$recordMode state=$recordUiState " +
|
||||
"voice=$isVoiceRecording kb=$isKeyboardVisible emojiBox=${coordinator.isEmojiBoxVisible} " +
|
||||
inputHeightsSnapshot()
|
||||
)
|
||||
stopVoiceRecording(send = false)
|
||||
transitionSpec = {
|
||||
fadeIn(tween(200)) togetherWith fadeOut(tween(200))
|
||||
},
|
||||
label = "record_center_content"
|
||||
) { isLockedOrPaused ->
|
||||
if (isLockedOrPaused) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
VoiceWaveformBar(
|
||||
waves = voiceWaves,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
RecordLockedControls(
|
||||
isPaused = recordUiState == RecordUiState.PAUSED,
|
||||
isDarkTheme = isDarkTheme,
|
||||
onDelete = {
|
||||
inputJumpLog("tap DELETE (locked/paused) mode=$recordMode state=$recordUiState")
|
||||
stopVoiceRecording(send = false)
|
||||
},
|
||||
onTogglePause = {
|
||||
inputJumpLog("tap PAUSE/RESUME mode=$recordMode state=$recordUiState")
|
||||
if (recordUiState == RecordUiState.PAUSED) {
|
||||
resumeVoiceRecording()
|
||||
} else {
|
||||
pauseVoiceRecording()
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SlideToCancel(
|
||||
slideDx = slideDx,
|
||||
cancelThresholdPx = cancelDragThresholdPx,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mic button area with LockIcon overlay
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.offset(x = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// LockIcon positioned above the mic button
|
||||
if (recordUiState == RecordUiState.RECORDING ||
|
||||
recordUiState == RecordUiState.LOCKED ||
|
||||
recordUiState == RecordUiState.PAUSED
|
||||
) {
|
||||
LockIcon(
|
||||
lockProgress = lockProgress,
|
||||
isLocked = recordUiState == RecordUiState.LOCKED,
|
||||
isPaused = recordUiState == RecordUiState.PAUSED,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.offset(y = (-60).dp)
|
||||
.zIndex(10f)
|
||||
)
|
||||
|
||||
// Tooltip
|
||||
if (showLockTooltip && recordUiState == RecordUiState.RECORDING) {
|
||||
LockTooltip(
|
||||
visible = showLockTooltip,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier = Modifier
|
||||
.offset(x = (-80).dp, y = (-60).dp)
|
||||
.zIndex(11f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
VoiceButtonBlob(
|
||||
voiceLevel = voiceLevel,
|
||||
isDarkTheme = isDarkTheme,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
// Visual-only enlargement like Telegram record circle,
|
||||
// while keeping layout hitbox at normal input size.
|
||||
scaleX = 2.05f
|
||||
scaleY = 2.05f
|
||||
clip = false
|
||||
}
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = 2.05f
|
||||
scaleY = 2.05f
|
||||
clip = false
|
||||
}
|
||||
)
|
||||
|
||||
if (recordUiState == RecordUiState.LOCKED) {
|
||||
if (recordUiState == RecordUiState.LOCKED || recordUiState == RecordUiState.PAUSED) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredSize(82.dp)
|
||||
@@ -2017,7 +2106,7 @@ fun MessageInputBar(
|
||||
indication = null
|
||||
) {
|
||||
inputJumpLog(
|
||||
"tap SEND (locked) mode=$recordMode state=$recordUiState voice=$isVoiceRecording " +
|
||||
"tap SEND (locked/paused) mode=$recordMode state=$recordUiState voice=$isVoiceRecording " +
|
||||
"kb=$isKeyboardVisible emojiBox=${coordinator.isEmojiBoxVisible} ${inputHeightsSnapshot()}"
|
||||
)
|
||||
stopVoiceRecording(send = true)
|
||||
@@ -2033,11 +2122,10 @@ fun MessageInputBar(
|
||||
}
|
||||
} else {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.requiredSize(82.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue.copy(alpha = 0.92f)),
|
||||
modifier = Modifier
|
||||
.requiredSize(82.dp)
|
||||
.clip(CircleShape)
|
||||
.background(PrimaryBlue.copy(alpha = 0.92f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
@@ -2257,6 +2345,7 @@ fun MessageInputBar(
|
||||
slideDy = dy
|
||||
|
||||
if (recordUiState == RecordUiState.RECORDING) {
|
||||
lockProgress = ((-dy) / lockDragThresholdPx).coerceIn(0f, 1f)
|
||||
if (dx <= -cancelDragThresholdPx) {
|
||||
inputJumpLog(
|
||||
"gesture CANCEL dx=${dx.toInt()} threshold=${cancelDragThresholdPx.toInt()} mode=$recordMode"
|
||||
@@ -2266,6 +2355,8 @@ fun MessageInputBar(
|
||||
resetGestureState()
|
||||
finished = true
|
||||
} else if (dy <= -lockDragThresholdPx) {
|
||||
view.performHapticFeedback(android.view.HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
lockProgress = 1f
|
||||
setRecordUiState(
|
||||
RecordUiState.LOCKED,
|
||||
"slide-lock dy=${dy.toInt()}"
|
||||
|
||||
Reference in New Issue
Block a user