fix(android): clarify channel and voice status icons
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -356,6 +356,15 @@ class BridgeClient {
|
||||
/// Nickname.
|
||||
final String name;
|
||||
|
||||
/// True when this client has muted microphone/input capture.
|
||||
final bool inputMuted;
|
||||
|
||||
/// True when this client has muted speaker/output audio.
|
||||
final bool outputMuted;
|
||||
|
||||
/// True when recent inbound voice activity was observed for this client.
|
||||
final bool isSpeaking;
|
||||
|
||||
/// True for TeamSpeak ServerQuery clients.
|
||||
final bool isServerQuery;
|
||||
|
||||
@@ -363,12 +372,21 @@ class BridgeClient {
|
||||
required this.id,
|
||||
required this.channel,
|
||||
required this.name,
|
||||
required this.inputMuted,
|
||||
required this.outputMuted,
|
||||
required this.isSpeaking,
|
||||
required this.isServerQuery,
|
||||
});
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
id.hashCode ^ channel.hashCode ^ name.hashCode ^ isServerQuery.hashCode;
|
||||
id.hashCode ^
|
||||
channel.hashCode ^
|
||||
name.hashCode ^
|
||||
inputMuted.hashCode ^
|
||||
outputMuted.hashCode ^
|
||||
isSpeaking.hashCode ^
|
||||
isServerQuery.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -378,6 +396,9 @@ class BridgeClient {
|
||||
id == other.id &&
|
||||
channel == other.channel &&
|
||||
name == other.name &&
|
||||
inputMuted == other.inputMuted &&
|
||||
outputMuted == other.outputMuted &&
|
||||
isSpeaking == other.isSpeaking &&
|
||||
isServerQuery == other.isServerQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -1172,13 +1172,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
BridgeClient dco_decode_bridge_client(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
final arr = raw as List<dynamic>;
|
||||
if (arr.length != 4)
|
||||
throw Exception('unexpected arr length: expect 4 but see ${arr.length}');
|
||||
if (arr.length != 7)
|
||||
throw Exception('unexpected arr length: expect 7 but see ${arr.length}');
|
||||
return BridgeClient(
|
||||
id: dco_decode_u_64(arr[0]),
|
||||
channel: dco_decode_u_64(arr[1]),
|
||||
name: dco_decode_String(arr[2]),
|
||||
isServerQuery: dco_decode_bool(arr[3]),
|
||||
inputMuted: dco_decode_bool(arr[3]),
|
||||
outputMuted: dco_decode_bool(arr[4]),
|
||||
isSpeaking: dco_decode_bool(arr[5]),
|
||||
isServerQuery: dco_decode_bool(arr[6]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1535,11 +1538,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
var var_id = sse_decode_u_64(deserializer);
|
||||
var var_channel = sse_decode_u_64(deserializer);
|
||||
var var_name = sse_decode_String(deserializer);
|
||||
var var_inputMuted = sse_decode_bool(deserializer);
|
||||
var var_outputMuted = sse_decode_bool(deserializer);
|
||||
var var_isSpeaking = sse_decode_bool(deserializer);
|
||||
var var_isServerQuery = sse_decode_bool(deserializer);
|
||||
return BridgeClient(
|
||||
id: var_id,
|
||||
channel: var_channel,
|
||||
name: var_name,
|
||||
inputMuted: var_inputMuted,
|
||||
outputMuted: var_outputMuted,
|
||||
isSpeaking: var_isSpeaking,
|
||||
isServerQuery: var_isServerQuery,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user