Merge branch 'dev'
This commit is contained in:
@@ -215,10 +215,14 @@ data class VoiceQueueEntry(
|
||||
object VoicePlaybackCoordinator {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
private val speedSteps = listOf(1f, 1.5f, 2f)
|
||||
private const val SEEK_STALE_SAMPLE_WINDOW_MS = 900L
|
||||
private const val SEEK_SETTLE_TOLERANCE_MS = 180
|
||||
private var player: MediaPlayer? = null
|
||||
private var currentAttachmentId: String? = null
|
||||
private var appContext: Context? = null
|
||||
private var progressJob: Job? = null
|
||||
private var pendingSeekTargetMs: Int? = null
|
||||
private var pendingSeekExpiresAtMs: Long = 0L
|
||||
private val queueEntries = LinkedHashMap<String, VoiceQueueEntry>()
|
||||
private val _playingAttachmentId = MutableStateFlow<String?>(null)
|
||||
val playingAttachmentId: StateFlow<String?> = _playingAttachmentId.asStateFlow()
|
||||
@@ -246,6 +250,29 @@ object VoicePlaybackCoordinator {
|
||||
queueEntries.remove(attachmentId)
|
||||
}
|
||||
|
||||
private fun setPendingSeek(targetMs: Int) {
|
||||
pendingSeekTargetMs = targetMs.coerceAtLeast(0)
|
||||
pendingSeekExpiresAtMs = SystemClock.elapsedRealtime() + SEEK_STALE_SAMPLE_WINDOW_MS
|
||||
}
|
||||
|
||||
private fun clearPendingSeek() {
|
||||
pendingSeekTargetMs = null
|
||||
pendingSeekExpiresAtMs = 0L
|
||||
}
|
||||
|
||||
private fun resolvePositionForUi(sampledPositionMs: Int): Int {
|
||||
val pendingTarget = pendingSeekTargetMs ?: return sampledPositionMs
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val settled = kotlin.math.abs(sampledPositionMs - pendingTarget) <= SEEK_SETTLE_TOLERANCE_MS
|
||||
val expired = now >= pendingSeekExpiresAtMs
|
||||
return if (settled || expired) {
|
||||
clearPendingSeek()
|
||||
sampledPositionMs
|
||||
} else {
|
||||
pendingTarget
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle(
|
||||
attachmentId: String,
|
||||
sourceFile: File,
|
||||
@@ -269,6 +296,7 @@ object VoicePlaybackCoordinator {
|
||||
}
|
||||
|
||||
stop()
|
||||
clearPendingSeek()
|
||||
val mediaPlayer = MediaPlayer()
|
||||
try {
|
||||
mediaPlayer.setAudioAttributes(
|
||||
@@ -322,6 +350,7 @@ object VoicePlaybackCoordinator {
|
||||
}
|
||||
|
||||
if (currentAttachmentId == attachmentId && player != null) {
|
||||
clearPendingSeek()
|
||||
val active = player ?: return
|
||||
_playingAttachmentId.value = attachmentId
|
||||
_playingDialogKey.value = dialogKey.trim().ifBlank { null }
|
||||
@@ -333,6 +362,7 @@ object VoicePlaybackCoordinator {
|
||||
}
|
||||
|
||||
stop()
|
||||
clearPendingSeek()
|
||||
val mediaPlayer = MediaPlayer()
|
||||
try {
|
||||
mediaPlayer.setAudioAttributes(
|
||||
@@ -364,6 +394,7 @@ object VoicePlaybackCoordinator {
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
clearPendingSeek()
|
||||
val active = player ?: return
|
||||
runCatching {
|
||||
if (active.isPlaying) active.pause()
|
||||
@@ -403,7 +434,7 @@ object VoicePlaybackCoordinator {
|
||||
val duration = runCatching { active.duration }.getOrDefault(_durationMs.value).coerceAtLeast(0)
|
||||
if (duration <= 0) return
|
||||
val clampedPosition = positionMs.coerceIn(0, duration)
|
||||
runCatching {
|
||||
val seekResult = runCatching {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
active.seekTo(clampedPosition.toLong(), MediaPlayer.SEEK_CLOSEST_SYNC)
|
||||
} else {
|
||||
@@ -411,6 +442,11 @@ object VoicePlaybackCoordinator {
|
||||
active.seekTo(clampedPosition)
|
||||
}
|
||||
}
|
||||
if (seekResult.isSuccess) {
|
||||
setPendingSeek(clampedPosition)
|
||||
} else {
|
||||
clearPendingSeek()
|
||||
}
|
||||
if (keepPaused) {
|
||||
// Some devices auto-resume after seek; for paused scrub we force pause every time.
|
||||
runCatching {
|
||||
@@ -461,7 +497,8 @@ object VoicePlaybackCoordinator {
|
||||
scope.launch {
|
||||
while (isActive && currentAttachmentId == attachmentId) {
|
||||
val active = player ?: break
|
||||
_positionMs.value = active.currentPosition.coerceAtLeast(0)
|
||||
val sampledPosition = active.currentPosition.coerceAtLeast(0)
|
||||
_positionMs.value = resolvePositionForUi(sampledPosition)
|
||||
_durationMs.value = active.duration.coerceAtLeast(0)
|
||||
if (!active.isPlaying) break
|
||||
delay(120)
|
||||
@@ -471,6 +508,7 @@ object VoicePlaybackCoordinator {
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
clearPendingSeek()
|
||||
val active = player
|
||||
player = null
|
||||
currentAttachmentId = null
|
||||
@@ -2543,17 +2581,34 @@ private fun VoiceAttachment(
|
||||
var waveformWidthPx by remember(attachment.id) { mutableFloatStateOf(0f) }
|
||||
var isWaveformTouchLocked by remember(attachment.id) { mutableStateOf(false) }
|
||||
var suppressMainActionUntilMs by remember(attachment.id) { mutableLongStateOf(0L) }
|
||||
val smoothProgress by
|
||||
animateFloatAsState(
|
||||
targetValue = progress,
|
||||
val progressAnimator = remember(attachment.id) { Animatable(0f) }
|
||||
LaunchedEffect(attachment.id, isActiveTrack) {
|
||||
if (!isActiveTrack) {
|
||||
progressAnimator.snapTo(0f)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(progress, isPlaying, isScrubbing, isActiveTrack) {
|
||||
if (!isActiveTrack || isScrubbing) return@LaunchedEffect
|
||||
|
||||
val target = progress.coerceIn(0f, 1f)
|
||||
val delta = kotlin.math.abs(target - progressAnimator.value)
|
||||
|
||||
// Для seek/резких скачков сразу фиксируем позицию без "пробега" от старого значения.
|
||||
if (delta >= 0.12f) {
|
||||
progressAnimator.snapTo(target)
|
||||
} else {
|
||||
progressAnimator.animateTo(
|
||||
targetValue = target,
|
||||
animationSpec =
|
||||
if (isPlaying) {
|
||||
tween(durationMillis = 140, easing = LinearEasing)
|
||||
tween(durationMillis = 120, easing = LinearEasing)
|
||||
} else {
|
||||
tween(durationMillis = 180, easing = FastOutSlowInEasing)
|
||||
},
|
||||
label = "voice_wave_progress"
|
||||
tween(durationMillis = 150, easing = FastOutSlowInEasing)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
val smoothProgress = progressAnimator.value
|
||||
val displayProgress = if (isScrubbing) scrubProgress else smoothProgress
|
||||
val liveWaveLevel =
|
||||
remember(isPlaying, progress, waves) {
|
||||
@@ -3033,6 +3088,9 @@ private fun VoiceAttachment(
|
||||
if (isDraggingWaveform) {
|
||||
val releaseProgress = progressFromWaveX(lastTouchX)
|
||||
scrubProgress = releaseProgress
|
||||
scope.launch {
|
||||
progressAnimator.snapTo(releaseProgress)
|
||||
}
|
||||
VoicePlaybackCoordinator.seekToProgress(
|
||||
releaseProgress,
|
||||
keepPaused = !wasPlayingOnTouchDown
|
||||
|
||||
Reference in New Issue
Block a user