fix(android): make top audio strip controls functional
This commit is contained in:
@@ -317,6 +317,8 @@ fun ChatScreen(
|
|||||||
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
|
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
|
||||||
var topAudioStrip by remember { mutableStateOf<TopAudioStrip?>(null) }
|
var topAudioStrip by remember { mutableStateOf<TopAudioStrip?>(null) }
|
||||||
var forceStopAudioSourceId by remember { mutableStateOf<String?>(null) }
|
var forceStopAudioSourceId by remember { mutableStateOf<String?>(null) }
|
||||||
|
var forceToggleAudioSourceId by remember { mutableStateOf<String?>(null) }
|
||||||
|
var forceCycleSpeedAudioSourceId by remember { mutableStateOf<String?>(null) }
|
||||||
var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
|
||||||
var pendingDeleteForAll by remember { mutableStateOf(false) }
|
var pendingDeleteForAll by remember { mutableStateOf(false) }
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
@@ -662,12 +664,15 @@ fun ChatScreen(
|
|||||||
Surface(
|
Surface(
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable { forceToggleAudioSourceId = strip.sourceId },
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(contentAlignment = Alignment.Center) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.PlayArrow,
|
imageVector = if (strip.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
|
||||||
contentDescription = "Play top audio",
|
contentDescription = if (strip.isPlaying) "Pause top audio" else "Play top audio",
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -688,9 +693,12 @@ fun ChatScreen(
|
|||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable { forceCycleSpeedAudioSourceId = strip.sourceId },
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "1x",
|
text = strip.speedLabel,
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
)
|
)
|
||||||
@@ -698,6 +706,8 @@ fun ChatScreen(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
forceStopAudioSourceId = strip.sourceId
|
forceStopAudioSourceId = strip.sourceId
|
||||||
|
forceToggleAudioSourceId = null
|
||||||
|
forceCycleSpeedAudioSourceId = null
|
||||||
topAudioStrip = null
|
topAudioStrip = null
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -762,11 +772,23 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
forceStopAudioSourceId = forceStopAudioSourceId,
|
forceStopAudioSourceId = forceStopAudioSourceId,
|
||||||
|
forceToggleAudioSourceId = forceToggleAudioSourceId,
|
||||||
|
forceCycleSpeedAudioSourceId = forceCycleSpeedAudioSourceId,
|
||||||
onForceStopAudioSourceHandled = { sourceId ->
|
onForceStopAudioSourceHandled = { sourceId ->
|
||||||
if (forceStopAudioSourceId == sourceId) {
|
if (forceStopAudioSourceId == sourceId) {
|
||||||
forceStopAudioSourceId = null
|
forceStopAudioSourceId = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onForceToggleAudioSourceHandled = { sourceId ->
|
||||||
|
if (forceToggleAudioSourceId == sourceId) {
|
||||||
|
forceToggleAudioSourceId = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onForceCycleSpeedAudioSourceHandled = { sourceId ->
|
||||||
|
if (forceCycleSpeedAudioSourceId == sourceId) {
|
||||||
|
forceCycleSpeedAudioSourceId = null
|
||||||
|
}
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
if (state.actionState.mode == MessageSelectionMode.MULTI) {
|
||||||
onToggleMessageMultiSelection(message)
|
onToggleMessageMultiSelection(message)
|
||||||
@@ -1789,7 +1811,11 @@ private fun MessageBubble(
|
|||||||
onAttachmentVideoClick: (String) -> Unit,
|
onAttachmentVideoClick: (String) -> Unit,
|
||||||
onAudioPlaybackChanged: (TopAudioStrip) -> Unit,
|
onAudioPlaybackChanged: (TopAudioStrip) -> Unit,
|
||||||
forceStopAudioSourceId: String?,
|
forceStopAudioSourceId: String?,
|
||||||
|
forceToggleAudioSourceId: String?,
|
||||||
|
forceCycleSpeedAudioSourceId: String?,
|
||||||
onForceStopAudioSourceHandled: (String) -> Unit,
|
onForceStopAudioSourceHandled: (String) -> Unit,
|
||||||
|
onForceToggleAudioSourceHandled: (String) -> Unit,
|
||||||
|
onForceCycleSpeedAudioSourceHandled: (String) -> Unit,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongPress: () -> Unit,
|
onLongPress: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -2009,7 +2035,11 @@ private fun MessageBubble(
|
|||||||
messageId = message.id,
|
messageId = message.id,
|
||||||
onPlaybackChanged = onAudioPlaybackChanged,
|
onPlaybackChanged = onAudioPlaybackChanged,
|
||||||
forceStopAudioSourceId = forceStopAudioSourceId,
|
forceStopAudioSourceId = forceStopAudioSourceId,
|
||||||
|
forceToggleAudioSourceId = forceToggleAudioSourceId,
|
||||||
|
forceCycleSpeedAudioSourceId = forceCycleSpeedAudioSourceId,
|
||||||
onForceStopAudioSourceHandled = onForceStopAudioSourceHandled,
|
onForceStopAudioSourceHandled = onForceStopAudioSourceHandled,
|
||||||
|
onForceToggleAudioSourceHandled = onForceToggleAudioSourceHandled,
|
||||||
|
onForceCycleSpeedAudioSourceHandled = onForceCycleSpeedAudioSourceHandled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2484,7 +2514,11 @@ private fun AudioAttachmentPlayer(
|
|||||||
messageId: Long,
|
messageId: Long,
|
||||||
onPlaybackChanged: (TopAudioStrip) -> Unit,
|
onPlaybackChanged: (TopAudioStrip) -> Unit,
|
||||||
forceStopAudioSourceId: String?,
|
forceStopAudioSourceId: String?,
|
||||||
|
forceToggleAudioSourceId: String?,
|
||||||
|
forceCycleSpeedAudioSourceId: String?,
|
||||||
onForceStopAudioSourceHandled: (String) -> Unit,
|
onForceStopAudioSourceHandled: (String) -> Unit,
|
||||||
|
onForceToggleAudioSourceHandled: (String) -> Unit,
|
||||||
|
onForceCycleSpeedAudioSourceHandled: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
var isPlaying by remember(url) { mutableStateOf(false) }
|
var isPlaying by remember(url) { mutableStateOf(false) }
|
||||||
var isPrepared by remember(url) { mutableStateOf(false) }
|
var isPrepared by remember(url) { mutableStateOf(false) }
|
||||||
@@ -2494,6 +2528,18 @@ private fun AudioAttachmentPlayer(
|
|||||||
var speedIndex by remember(url) { mutableStateOf(0) }
|
var speedIndex by remember(url) { mutableStateOf(0) }
|
||||||
var isSeeking by remember(url) { mutableStateOf(false) }
|
var isSeeking by remember(url) { mutableStateOf(false) }
|
||||||
var seekFraction by remember(url) { mutableStateOf(0f) }
|
var seekFraction by remember(url) { mutableStateOf(0f) }
|
||||||
|
fun emitTopStrip(playing: Boolean) {
|
||||||
|
onPlaybackChanged(
|
||||||
|
TopAudioStrip(
|
||||||
|
messageId = messageId,
|
||||||
|
sourceId = "player:$url",
|
||||||
|
title = playbackTitle,
|
||||||
|
subtitle = playbackSubtitle,
|
||||||
|
isPlaying = playing,
|
||||||
|
speedLabel = formatAudioSpeed(speedOptions[speedIndex]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
val mediaPlayer = remember(url) {
|
val mediaPlayer = remember(url) {
|
||||||
MediaPlayer().apply {
|
MediaPlayer().apply {
|
||||||
setAudioAttributes(
|
setAudioAttributes(
|
||||||
@@ -2511,15 +2557,7 @@ private fun AudioAttachmentPlayer(
|
|||||||
runCatching { player.seekTo(0) }
|
runCatching { player.seekTo(0) }
|
||||||
positionMs = 0
|
positionMs = 0
|
||||||
AppAudioFocusCoordinator.release("player:$url")
|
AppAudioFocusCoordinator.release("player:$url")
|
||||||
onPlaybackChanged(
|
emitTopStrip(false)
|
||||||
TopAudioStrip(
|
|
||||||
messageId = messageId,
|
|
||||||
sourceId = "player:$url",
|
|
||||||
title = playbackTitle,
|
|
||||||
subtitle = playbackSubtitle,
|
|
||||||
isPlaying = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
setDataSource(url)
|
setDataSource(url)
|
||||||
prepareAsync()
|
prepareAsync()
|
||||||
@@ -2531,15 +2569,7 @@ private fun AudioAttachmentPlayer(
|
|||||||
if (activeId != null && activeId != currentId && isPlaying) {
|
if (activeId != null && activeId != currentId && isPlaying) {
|
||||||
runCatching { mediaPlayer.pause() }
|
runCatching { mediaPlayer.pause() }
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
onPlaybackChanged(
|
emitTopStrip(false)
|
||||||
TopAudioStrip(
|
|
||||||
messageId = messageId,
|
|
||||||
sourceId = currentId,
|
|
||||||
title = playbackTitle,
|
|
||||||
subtitle = playbackSubtitle,
|
|
||||||
isPlaying = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2551,17 +2581,48 @@ private fun AudioAttachmentPlayer(
|
|||||||
positionMs = 0
|
positionMs = 0
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
AppAudioFocusCoordinator.release(currentId)
|
AppAudioFocusCoordinator.release(currentId)
|
||||||
onPlaybackChanged(
|
emitTopStrip(false)
|
||||||
TopAudioStrip(
|
|
||||||
messageId = messageId,
|
|
||||||
sourceId = currentId,
|
|
||||||
title = playbackTitle,
|
|
||||||
subtitle = playbackSubtitle,
|
|
||||||
isPlaying = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
onForceStopAudioSourceHandled(currentId)
|
onForceStopAudioSourceHandled(currentId)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(forceToggleAudioSourceId) {
|
||||||
|
val currentId = "player:$url"
|
||||||
|
if (forceToggleAudioSourceId != currentId) return@LaunchedEffect
|
||||||
|
if (!isPrepared) {
|
||||||
|
onForceToggleAudioSourceHandled(currentId)
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
if (isPlaying) {
|
||||||
|
runCatching { mediaPlayer.pause() }
|
||||||
|
isPlaying = false
|
||||||
|
AppAudioFocusCoordinator.release(currentId)
|
||||||
|
emitTopStrip(false)
|
||||||
|
} else {
|
||||||
|
if (durationMs > 0 && positionMs >= durationMs - 200) {
|
||||||
|
runCatching { mediaPlayer.seekTo(0) }
|
||||||
|
positionMs = 0
|
||||||
|
}
|
||||||
|
runCatching {
|
||||||
|
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
|
||||||
|
}
|
||||||
|
AppAudioFocusCoordinator.request(currentId)
|
||||||
|
runCatching { mediaPlayer.start() }
|
||||||
|
isPlaying = true
|
||||||
|
emitTopStrip(true)
|
||||||
|
}
|
||||||
|
onForceToggleAudioSourceHandled(currentId)
|
||||||
|
}
|
||||||
|
LaunchedEffect(forceCycleSpeedAudioSourceId) {
|
||||||
|
val currentId = "player:$url"
|
||||||
|
if (forceCycleSpeedAudioSourceId != currentId) return@LaunchedEffect
|
||||||
|
speedIndex = (speedIndex + 1) % speedOptions.size
|
||||||
|
if (isPrepared) {
|
||||||
|
runCatching {
|
||||||
|
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emitTopStrip(isPlaying)
|
||||||
|
onForceCycleSpeedAudioSourceHandled(currentId)
|
||||||
|
}
|
||||||
LaunchedEffect(isPlaying, isPrepared, isSeeking) {
|
LaunchedEffect(isPlaying, isPrepared, isSeeking) {
|
||||||
if (!isPlaying || !isPrepared || isSeeking) return@LaunchedEffect
|
if (!isPlaying || !isPrepared || isSeeking) return@LaunchedEffect
|
||||||
while (isPlaying) {
|
while (isPlaying) {
|
||||||
@@ -2582,15 +2643,7 @@ private fun AudioAttachmentPlayer(
|
|||||||
mediaPlayer.stop()
|
mediaPlayer.stop()
|
||||||
}
|
}
|
||||||
AppAudioFocusCoordinator.release("player:$url")
|
AppAudioFocusCoordinator.release("player:$url")
|
||||||
onPlaybackChanged(
|
emitTopStrip(false)
|
||||||
TopAudioStrip(
|
|
||||||
messageId = messageId,
|
|
||||||
sourceId = "player:$url",
|
|
||||||
title = playbackTitle,
|
|
||||||
subtitle = playbackSubtitle,
|
|
||||||
isPlaying = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
mediaPlayer.release()
|
mediaPlayer.release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2612,15 +2665,7 @@ private fun AudioAttachmentPlayer(
|
|||||||
mediaPlayer.pause()
|
mediaPlayer.pause()
|
||||||
isPlaying = false
|
isPlaying = false
|
||||||
AppAudioFocusCoordinator.release("player:$url")
|
AppAudioFocusCoordinator.release("player:$url")
|
||||||
onPlaybackChanged(
|
emitTopStrip(false)
|
||||||
TopAudioStrip(
|
|
||||||
messageId = messageId,
|
|
||||||
sourceId = "player:$url",
|
|
||||||
title = playbackTitle,
|
|
||||||
subtitle = playbackSubtitle,
|
|
||||||
isPlaying = false,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
if (durationMs > 0 && positionMs >= durationMs - 200) {
|
if (durationMs > 0 && positionMs >= durationMs - 200) {
|
||||||
runCatching { mediaPlayer.seekTo(0) }
|
runCatching { mediaPlayer.seekTo(0) }
|
||||||
@@ -2632,15 +2677,7 @@ private fun AudioAttachmentPlayer(
|
|||||||
AppAudioFocusCoordinator.request("player:$url")
|
AppAudioFocusCoordinator.request("player:$url")
|
||||||
mediaPlayer.start()
|
mediaPlayer.start()
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
onPlaybackChanged(
|
emitTopStrip(true)
|
||||||
TopAudioStrip(
|
|
||||||
messageId = messageId,
|
|
||||||
sourceId = "player:$url",
|
|
||||||
title = playbackTitle,
|
|
||||||
subtitle = playbackSubtitle,
|
|
||||||
isPlaying = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = isPrepared,
|
enabled = isPrepared,
|
||||||
@@ -2659,10 +2696,11 @@ private fun AudioAttachmentPlayer(
|
|||||||
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
|
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
emitTopStrip(isPlaying)
|
||||||
},
|
},
|
||||||
enabled = isPrepared,
|
enabled = isPrepared,
|
||||||
) {
|
) {
|
||||||
Text("${speedOptions[speedIndex]}x")
|
Text(formatAudioSpeed(speedOptions[speedIndex]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isVoice) {
|
if (isVoice) {
|
||||||
@@ -2794,6 +2832,14 @@ private fun formatDuration(ms: Int): String {
|
|||||||
return "$min:${sec.toString().padStart(2, '0')}"
|
return "$min:${sec.toString().padStart(2, '0')}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatAudioSpeed(speed: Float): String {
|
||||||
|
return if (speed % 1f == 0f) {
|
||||||
|
"${speed.toInt()}x"
|
||||||
|
} else {
|
||||||
|
"${speed}x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun resolveRemoteAudioDurationMs(url: String): Int? {
|
private fun resolveRemoteAudioDurationMs(url: String): Int? {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val retriever = MediaMetadataRetriever()
|
val retriever = MediaMetadataRetriever()
|
||||||
@@ -3008,7 +3054,11 @@ private fun ChatInfoTabContent(
|
|||||||
messageId = entry.sourceMessageId ?: entry.title.hashCode().toLong(),
|
messageId = entry.sourceMessageId ?: entry.title.hashCode().toLong(),
|
||||||
onPlaybackChanged = {},
|
onPlaybackChanged = {},
|
||||||
forceStopAudioSourceId = null,
|
forceStopAudioSourceId = null,
|
||||||
|
forceToggleAudioSourceId = null,
|
||||||
|
forceCycleSpeedAudioSourceId = null,
|
||||||
onForceStopAudioSourceHandled = {},
|
onForceStopAudioSourceHandled = {},
|
||||||
|
onForceToggleAudioSourceHandled = {},
|
||||||
|
onForceCycleSpeedAudioSourceHandled = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3152,4 +3202,5 @@ private data class TopAudioStrip(
|
|||||||
val title: String,
|
val title: String,
|
||||||
val subtitle: String,
|
val subtitle: String,
|
||||||
val isPlaying: Boolean,
|
val isPlaying: Boolean,
|
||||||
|
val speedLabel: String = "1x",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user