From bc1bd8942416487ef7891f1996410fcfd380d937 Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Wed, 20 May 2026 11:15:51 +0900 Subject: [PATCH] fix(android): strengthen user voice indicators --- apps/chanora_flutter/lib/main.dart | 96 ++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index dc076c4..9a045f8 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -2197,34 +2197,75 @@ class _SnapshotView extends StatelessWidget { ), ), for (final cl in byChannel[ch.id] ?? const []) - Padding( - padding: EdgeInsets.only( - left: ((depthById[ch.id] ?? 0) * indentPerLevel) + 64, - ), - child: ListTile( - dense: true, - visualDensity: VisualDensity.compact, - leading: _clientVoiceStatusIcon(theme, cl), - title: Text( - cl.name, - style: cl.isServerQuery - ? TextStyle(color: theme.colorScheme.onSurfaceVariant) - : null, - ), - ), - ), + _clientTile(theme, cl, (depthById[ch.id] ?? 0) * indentPerLevel), ], ], ); } - Widget _clientVoiceStatusIcon(ThemeData theme, rust.BridgeClient client) { + Widget _clientTile( + ThemeData theme, + rust.BridgeClient client, + double channelIndent, + ) { + final status = _clientVoiceStatusIcon(theme, client); + final nameStyle = client.isServerQuery + ? TextStyle(color: theme.colorScheme.onSurfaceVariant) + : status.isSpeaking + ? TextStyle( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w600, + ) + : null; + + final tile = ListTile( + dense: true, + visualDensity: VisualDensity.compact, + leading: status.icon, + title: Text(client.name, style: nameStyle), + ); + + return Padding( + padding: EdgeInsets.only(left: channelIndent + 64, right: 8), + child: AnimatedContainer( + duration: const Duration(milliseconds: 120), + curve: Curves.easeOut, + decoration: status.isSpeaking + ? BoxDecoration( + color: theme.colorScheme.primaryContainer.withValues( + alpha: 0.45, + ), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: theme.colorScheme.primary.withValues(alpha: 0.55), + width: 1.2, + ), + boxShadow: [ + BoxShadow( + color: theme.colorScheme.primary.withValues(alpha: 0.14), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ) + : null, + child: tile, + ), + ); + } + + ({Widget icon, bool isSpeaking}) _clientVoiceStatusIcon( + ThemeData theme, + rust.BridgeClient client, + ) { final isSelf = client.id == snapshot.ownClientId; + final inCurrentChannel = client.channel == currentVoiceChannelId; final outputMuted = isSelf ? localOutputMuted : client.outputMuted; final inputMuted = isSelf ? localInputMuted : client.inputMuted; - final speaking = isSelf + final rawSpeaking = isSelf ? (audioStats?.pttActive ?? false) : client.isSpeaking; + final speaking = rawSpeaking && !outputMuted && !inputMuted; final IconData icon; final Color color; @@ -2238,18 +2279,25 @@ class _SnapshotView extends StatelessWidget { color = theme.colorScheme.error; tooltip = 'Microphone muted'; } else if (speaking) { - icon = Icons.volume_up; + icon = isSelf ? Icons.mic : Icons.volume_up; color = theme.colorScheme.primary; tooltip = 'Speaking'; - } else { - icon = Icons.volume_up_outlined; + } else if (inCurrentChannel) { + icon = isSelf ? Icons.mic_none : Icons.volume_up_outlined; color = theme.colorScheme.onSurfaceVariant; tooltip = 'Not speaking'; + } else { + icon = Icons.person_outline; + color = theme.colorScheme.onSurfaceVariant; + tooltip = 'Outside current channel'; } - return Tooltip( - message: tooltip, - child: Icon(icon, size: 18, color: color), + return ( + icon: Tooltip( + message: tooltip, + child: Icon(icon, size: 18, color: color), + ), + isSpeaking: speaking, ); } }