fix(android): clarify channel and voice status icons

This commit is contained in:
Edison Jwa
2026-05-20 14:52:34 +09:00
parent 34121b2eb7
commit b21fc16ee6
8 changed files with 154 additions and 21 deletions
+75 -14
View File
@@ -255,6 +255,8 @@ class _BetaHomeState extends State<_BetaHome> {
bool _audioStarted = false;
rust.BridgeAudioStats? _audioStats;
Timer? _statsTimer;
int _statsTick = 0;
bool _voiceStatusRefreshInFlight = false;
StreamSubscription<rust.BridgeEvent>? _eventsSub;
// v1 voice subsystem state (SDD-094/095/096/097). Driven by
@@ -563,15 +565,35 @@ class _BetaHomeState extends State<_BetaHome> {
final s = await rust.audioStats();
if (!mounted) return;
setState(() => _audioStats = s);
_statsTick += 1;
if (_statsTick % 5 == 0) {
unawaited(_refreshSnapshotForVoiceStatus());
}
} catch (_) {}
});
}
Future<void> _refreshSnapshotForVoiceStatus() async {
if (_voiceStatusRefreshInFlight) return;
_voiceStatusRefreshInFlight = true;
try {
final snap = await rust.snapshot();
if (!mounted) return;
setState(() => _applySnapshot(snap));
} catch (_) {
// Best-effort visual refresh only. Connection/loss paths still
// surface via the normal bridge events and explicit refreshes.
} finally {
_voiceStatusRefreshInFlight = false;
}
}
@override
void dispose() {
HardwareKeyboard.instance.removeHandler(_handleFocusedPttKey);
_eventsSub?.cancel();
_statsTimer?.cancel();
_voiceStatusRefreshInFlight = false;
_hostCtl.dispose();
_nickCtl.dispose();
_passwordCtl.dispose();
@@ -978,6 +1000,8 @@ class _BetaHomeState extends State<_BetaHome> {
Future<void> _onDisconnect() async {
_statsTimer?.cancel();
_statsTimer = null;
_statsTick = 0;
_voiceStatusRefreshInFlight = false;
try {
await rust.disconnect();
} catch (_) {}
@@ -1014,6 +1038,8 @@ class _BetaHomeState extends State<_BetaHome> {
for (final client in snap.clients) {
if (client.id == snap.ownClientId) {
_currentVoiceChannelId = client.channel;
_inputMuted = client.inputMuted;
_outputMuted = client.outputMuted;
_pendingVoiceChannelId = null;
_inChannel = true;
_canJoinVoiceChannel = true;
@@ -1412,8 +1438,11 @@ class _BetaHomeState extends State<_BetaHome> {
);
final snapshotView = _SnapshotView(
snapshot: _snapshot!,
audioStats: _audioStats,
currentVoiceChannelId: _currentVoiceChannelId,
pendingVoiceChannelId: _pendingVoiceChannelId,
localInputMuted: _inputMuted || _hardMute,
localOutputMuted: _outputMuted,
hasJoinPending: _pendingVoiceChannelId != null,
canJoinVoiceChannel: _canJoinVoiceChannel,
onJoinChannel: (ch) => _onJoinChannel(ch),
@@ -2056,8 +2085,11 @@ class PttCapabilityBadge extends StatelessWidget {
class _SnapshotView extends StatelessWidget {
const _SnapshotView({
required this.snapshot,
required this.audioStats,
required this.currentVoiceChannelId,
required this.pendingVoiceChannelId,
required this.localInputMuted,
required this.localOutputMuted,
required this.hasJoinPending,
required this.canJoinVoiceChannel,
required this.onJoinChannel,
@@ -2065,8 +2097,11 @@ class _SnapshotView extends StatelessWidget {
});
final rust.BridgeSnapshot snapshot;
final rust.BridgeAudioStats? audioStats;
final BigInt? currentVoiceChannelId;
final BigInt? pendingVoiceChannelId;
final bool localInputMuted;
final bool localOutputMuted;
final bool hasJoinPending;
final bool canJoinVoiceChannel;
final ValueChanged<rust.BridgeChannel> onJoinChannel;
@@ -2143,19 +2178,13 @@ class _SnapshotView extends StatelessWidget {
child: ListTile(
dense: true,
leading: Icon(
ch.id == currentVoiceChannelId ? Icons.volume_up : Icons.tag,
ch.hasPassword ? Icons.lock_outline : Icons.tag,
color: ch.hasPassword
? theme.colorScheme.onSurfaceVariant
: null,
),
title: Text(ch.name),
subtitle: Text('id=${ch.id} parent=${ch.parent}'),
trailing: ch.hasPassword && ch.id != currentVoiceChannelId
? Tooltip(
message: l10n.channelPasswordTitle,
child: Icon(
Icons.lock_outline,
color: theme.colorScheme.onSurfaceVariant,
),
)
: null,
selected: ch.id == currentVoiceChannelId,
onTap:
hasJoinPending ||
@@ -2175,10 +2204,7 @@ class _SnapshotView extends StatelessWidget {
child: ListTile(
dense: true,
visualDensity: VisualDensity.compact,
leading: Icon(
cl.isServerQuery ? Icons.terminal : Icons.person,
size: 18,
),
leading: _clientVoiceStatusIcon(theme, cl),
title: Text(
cl.name,
style: cl.isServerQuery
@@ -2191,6 +2217,41 @@ class _SnapshotView extends StatelessWidget {
],
);
}
Widget _clientVoiceStatusIcon(ThemeData theme, rust.BridgeClient client) {
final isSelf = client.id == snapshot.ownClientId;
final outputMuted = isSelf ? localOutputMuted : client.outputMuted;
final inputMuted = isSelf ? localInputMuted : client.inputMuted;
final speaking = isSelf
? (audioStats?.pttActive ?? false)
: client.isSpeaking;
final IconData icon;
final Color color;
final String tooltip;
if (outputMuted) {
icon = Icons.volume_off;
color = theme.colorScheme.error;
tooltip = 'Speaker muted';
} else if (inputMuted) {
icon = Icons.mic_off;
color = theme.colorScheme.error;
tooltip = 'Microphone muted';
} else if (speaking) {
icon = Icons.volume_up;
color = theme.colorScheme.primary;
tooltip = 'Speaking';
} else {
icon = Icons.volume_up_outlined;
color = theme.colorScheme.onSurfaceVariant;
tooltip = 'Not speaking';
}
return Tooltip(
message: tooltip,
child: Icon(icon, size: 18, color: color),
);
}
}
/// Result of a successful PTT binding capture. Carries only the