Reduce Linux setup ambiguity and surface desktop input/message failures honestly
Clarify ONNX Runtime guidance with direct-open install hints, restore desktop WebRTC VAD visibility, map mouse side buttons through focused PTT capture/runtime paths, and wait for server acks before showing chat sends as successful. Constraint: Linux release UX must stay functional when ONNX Runtime is optional and GNOME portal availability varies Rejected: Keep desktop VAD locked to Silero only | misleads users when ONNX Runtime is skipped Confidence: medium Scope-risk: moderate Directive: Preserve the protocol send-ack wait path for chat so UI success always tracks real server acceptance Tested: flutter analyze lib/main.dart lib/widgets/chat_views.dart lib/widgets/input_dialogs.dart lib/widgets/startup_dependency_screen.dart; flutter test test/widgets/input_dialogs_test.dart test/widgets/chat_views_test.dart test/services/startup_dependency_check_test.dart test/widgets/startup_dependency_screen_test.dart test/widgets/voice_settings_controls_test.dart test/widgets/audio_processing_config_state_test.dart; cargo test -p chanora_protocol --lib; cargo test -p chanora_audio ptt_backends --lib Not-tested: Live manual GNOME portal rebind/global PTT on a real desktop session; observer-bot chat against a live server after the sender-name fallback change
This commit is contained in:
+275
-218
@@ -35,6 +35,7 @@ import 'widgets/chat_views.dart';
|
||||
import 'widgets/connect_widgets.dart';
|
||||
import 'widgets/input_dialogs.dart';
|
||||
import 'widgets/snapshot_view.dart';
|
||||
import 'widgets/startup_dependency_screen.dart';
|
||||
import 'widgets/voice_platform.dart';
|
||||
import 'widgets/voice_bar.dart';
|
||||
import 'widgets/voice_compact.dart';
|
||||
@@ -121,7 +122,7 @@ class ChanoraApp extends StatelessWidget {
|
||||
navigatorKey: navigatorKey,
|
||||
localizationsDelegates: AppL10n.localizationsDelegates,
|
||||
supportedLocales: AppL10n.supportedLocales,
|
||||
home: const _BetaHome(),
|
||||
home: const StartupDependencyGate(child: _BetaHome()),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -204,7 +205,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
// only the platform-neutral label that already crossed into
|
||||
// the Rust side.
|
||||
String _pttBoundKeyLabel = '';
|
||||
final Set<LogicalKeyboardKey> _focusedPttHeldKeys = <LogicalKeyboardKey>{};
|
||||
final Set<String> _focusedPttHeldInputs = <String>{};
|
||||
|
||||
List<rust.BridgeBookmark> _bookmarks = const [];
|
||||
final List<ChatEntry> _chatMessages = [];
|
||||
@@ -426,24 +427,23 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
bool _handleFocusedPttKey(KeyEvent event) {
|
||||
final label = pttDisplayLabelForKey(event.logicalKey);
|
||||
final isBoundKey =
|
||||
_pttBoundKeyLabel.isNotEmpty && label == _pttBoundKeyLabel;
|
||||
if (event is KeyUpEvent && _focusedPttHeldKeys.contains(event.logicalKey)) {
|
||||
if (_focusedPttHeldKeys.remove(event.logicalKey)) {
|
||||
unawaited(_setPtt(false));
|
||||
final platformKey =
|
||||
pttMouseSideButtonPlatformKeyForLogicalKey(event.logicalKey) ??
|
||||
pttDisplayLabelForKey(event.logicalKey);
|
||||
if (platformKey == null) {
|
||||
return false;
|
||||
}
|
||||
if (event is KeyUpEvent && _focusedPttHeldInputs.contains(platformKey)) {
|
||||
if (_focusedPttHeldInputs.remove(platformKey)) {
|
||||
unawaited(_setPtt(false, reportError: false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_pttBackendId != 'focused' ||
|
||||
!_serverReachable ||
|
||||
!_inChannel ||
|
||||
_transmitMode != rust.BridgeTransmitMode.ptt ||
|
||||
!isBoundKey) {
|
||||
if (!_canHandleFocusedPttPlatformKey(platformKey)) {
|
||||
return false;
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
if (_focusedPttHeldKeys.add(event.logicalKey)) {
|
||||
if (_focusedPttHeldInputs.add(platformKey)) {
|
||||
unawaited(_setPtt(true));
|
||||
}
|
||||
return true;
|
||||
@@ -451,9 +451,38 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _canHandleFocusedPttPlatformKey(String platformKey) {
|
||||
return _pttBackendId == 'focused' &&
|
||||
_serverReachable &&
|
||||
_inChannel &&
|
||||
_transmitMode == rust.BridgeTransmitMode.ptt &&
|
||||
_pttBoundKeyLabel.isNotEmpty &&
|
||||
platformKey == _pttBoundKeyLabel;
|
||||
}
|
||||
|
||||
void _handleFocusedPttPointerDown(PointerDownEvent event) {
|
||||
final platformKey = pttMouseSideButtonPlatformKeyForButtons(event.buttons);
|
||||
if (platformKey == null || !_canHandleFocusedPttPlatformKey(platformKey)) {
|
||||
return;
|
||||
}
|
||||
if (_focusedPttHeldInputs.add(platformKey)) {
|
||||
unawaited(_setPtt(true));
|
||||
}
|
||||
}
|
||||
|
||||
void _handleFocusedPttPointerRelease(PointerEvent event) {
|
||||
final boundKey = _pttBoundKeyLabel;
|
||||
if (!boundKey.startsWith('mouse-side-button:')) {
|
||||
return;
|
||||
}
|
||||
if (_focusedPttHeldInputs.remove(boundKey)) {
|
||||
unawaited(_setPtt(false, reportError: false));
|
||||
}
|
||||
}
|
||||
|
||||
void _releaseFocusedPttIfHeld() {
|
||||
if (_focusedPttHeldKeys.isEmpty) return;
|
||||
_focusedPttHeldKeys.clear();
|
||||
if (_focusedPttHeldInputs.isEmpty) return;
|
||||
_focusedPttHeldInputs.clear();
|
||||
unawaited(_setPtt(false, reportError: false));
|
||||
}
|
||||
|
||||
@@ -623,12 +652,16 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
// Skip echo of self-sent messages (already added locally).
|
||||
if (senderId == _snapshot?.ownClientId) return;
|
||||
final isPoke = target is rust.BridgeMessageTarget_Poke;
|
||||
final resolvedSenderName = _resolvedChatSenderName(
|
||||
senderId,
|
||||
senderName,
|
||||
);
|
||||
final receivedAt = DateTime.now();
|
||||
setState(() {
|
||||
_appendChatEntryUnlocked(
|
||||
ChatEntry(
|
||||
senderId: senderId,
|
||||
senderName: senderName,
|
||||
senderName: resolvedSenderName,
|
||||
message: message,
|
||||
target: target,
|
||||
isSelf: senderId == _snapshot?.ownClientId,
|
||||
@@ -638,7 +671,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
});
|
||||
if (isPoke) {
|
||||
_showPokeSnackBar(
|
||||
senderName: senderName,
|
||||
senderName: resolvedSenderName,
|
||||
message: message,
|
||||
receivedAt: receivedAt,
|
||||
);
|
||||
@@ -649,7 +682,7 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
unawaited(() async {
|
||||
if (!mounted) return;
|
||||
_showChatMessageSnackBar(
|
||||
senderName: senderName,
|
||||
senderName: resolvedSenderName,
|
||||
message: message,
|
||||
target: target,
|
||||
);
|
||||
@@ -1435,6 +1468,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
};
|
||||
}
|
||||
|
||||
String _resolvedChatSenderName(BigInt senderId, String fallback) {
|
||||
final snapshot = _snapshot;
|
||||
if (snapshot != null) {
|
||||
for (final client in snapshot.clients) {
|
||||
if (client.id == senderId && client.name.isNotEmpty) {
|
||||
return client.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
void _applySnapshot(rust.BridgeSnapshot snap) {
|
||||
_snapshot = snap;
|
||||
final own = ownClientSnapshotState(snap);
|
||||
@@ -1473,6 +1518,9 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
Future<void> _onShowDiagnostics(BuildContext context) async {
|
||||
if (!_phase.canOpenDiagnostics) {
|
||||
return;
|
||||
}
|
||||
final l10n = AppL10n.of(context);
|
||||
final rustText = rust.exportDiagnostics();
|
||||
final uiText = _uiDiagnostics.isEmpty
|
||||
@@ -1614,10 +1662,12 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(ctx).pop();
|
||||
await _onShowDiagnostics(context);
|
||||
},
|
||||
onPressed: _phase.canOpenDiagnostics
|
||||
? () async {
|
||||
Navigator.of(ctx).pop();
|
||||
await _onShowDiagnostics(context);
|
||||
}
|
||||
: null,
|
||||
child: Text(l10n.diagnosticsAction),
|
||||
),
|
||||
TextButton(
|
||||
@@ -1806,223 +1856,230 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
reconnectDelay: _reconnectDelay,
|
||||
);
|
||||
|
||||
final bodyContent = LayoutBuilder(
|
||||
builder: (ctx, bodyConstraints) {
|
||||
final isWideSnapshot =
|
||||
bodyConstraints.maxWidth >= _wideBreakpoint &&
|
||||
_serverReachable &&
|
||||
_snapshot != null;
|
||||
final banner = Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
l10n.homeNotProductionReadyBanner,
|
||||
style: TextStyle(color: theme.colorScheme.onTertiaryContainer),
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)],
|
||||
if (!_serverReachable)
|
||||
Row(
|
||||
children: [
|
||||
Icon(connTokens.icon, size: 18, color: connTokens.color),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
statusText,
|
||||
softWrap: true,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_lostReason != null || _reconnectAttempt != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
connTokens.background ?? theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
final bodyContent = Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: _handleFocusedPttPointerDown,
|
||||
onPointerUp: _handleFocusedPttPointerRelease,
|
||||
onPointerCancel: _handleFocusedPttPointerRelease,
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, bodyConstraints) {
|
||||
final isWideSnapshot =
|
||||
bodyConstraints.maxWidth >= _wideBreakpoint &&
|
||||
_serverReachable &&
|
||||
_snapshot != null;
|
||||
final banner = Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.tertiaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
l10n.homeNotProductionReadyBanner,
|
||||
style: TextStyle(color: theme.colorScheme.onTertiaryContainer),
|
||||
),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (!isWideSnapshot) ...[banner, const SizedBox(height: 12)],
|
||||
if (!_serverReachable)
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Icon(connTokens.icon, size: 18, color: connTokens.color),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_reconnectAttempt != null
|
||||
? l10n.statusReconnecting(
|
||||
_reconnectAttempt!,
|
||||
_reconnectDelay ?? 0,
|
||||
)
|
||||
: l10n.statusConnectionLost(_lostReason ?? ''),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
statusText,
|
||||
softWrap: true,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
if (_phase == ConnectionPhase.idle ||
|
||||
_phase == ConnectionPhase.disconnected) ...[
|
||||
Expanded(
|
||||
child: AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewInsetsOf(ctx).bottom,
|
||||
if (_lostReason != null || _reconnectAttempt != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
connTokens.background ??
|
||||
theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConnectForm(
|
||||
hostCtl: _hostCtl,
|
||||
nickCtl: _nickCtl,
|
||||
passwordCtl: _passwordCtl,
|
||||
onConnect: () => _onConnect(),
|
||||
onAddBookmark: _onAddCurrentBookmark,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BookmarkList(
|
||||
bookmarks: _bookmarks,
|
||||
onConnect: _onUseBookmark,
|
||||
onDelete: _onDeleteBookmark,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_reconnectAttempt != null
|
||||
? l10n.statusReconnecting(
|
||||
_reconnectAttempt!,
|
||||
_reconnectDelay ?? 0,
|
||||
)
|
||||
: l10n.statusConnectionLost(_lostReason ?? ''),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
if (_phase == ConnectionPhase.idle ||
|
||||
_phase == ConnectionPhase.disconnected) ...[
|
||||
Expanded(
|
||||
child: AnimatedPadding(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeOut,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.viewInsetsOf(ctx).bottom,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
keyboardDismissBehavior:
|
||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
ConnectForm(
|
||||
hostCtl: _hostCtl,
|
||||
nickCtl: _nickCtl,
|
||||
passwordCtl: _passwordCtl,
|
||||
onConnect: () => _onConnect(),
|
||||
onAddBookmark: _onAddCurrentBookmark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BookmarkList(
|
||||
bookmarks: _bookmarks,
|
||||
onConnect: _onUseBookmark,
|
||||
onDelete: _onDeleteBookmark,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (_phase == ConnectionPhase.connecting) ...[
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
] else if (_phase == ConnectionPhase.connecting) ...[
|
||||
const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (_serverReachable && _snapshot != null) ...[
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final voiceBar = VoiceBar(
|
||||
inChannel: _inChannel,
|
||||
transmitMode: _transmitMode,
|
||||
hardMute: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
talkPowerBlocked: _hardMuteByTalkPower,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
channelName: snapshotChannelName(
|
||||
_snapshot,
|
||||
_currentVoiceChannelId,
|
||||
),
|
||||
audioStats: _audioStats,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
onConfigure: _onOpenVoiceSettings,
|
||||
onPttHeldChanged: _onOnscreenPttHeldChanged,
|
||||
);
|
||||
// SDD-106 §2/§3 + SRS-209 + SRS-164: listen-only
|
||||
// banner. Self-hides on granted / unknown.
|
||||
final permissionBanner =
|
||||
PermissionStateBanner.fromCallbacks(
|
||||
recordAudioState: _activeRecordAudioState,
|
||||
ensureRecordAudio: _ensureActiveRecordAudio,
|
||||
openAppSettings: _openActivePermissionSettings,
|
||||
] else if (_serverReachable && _snapshot != null) ...[
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final voiceBar = VoiceBar(
|
||||
inChannel: _inChannel,
|
||||
transmitMode: _transmitMode,
|
||||
hardMute: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
talkPowerBlocked: _hardMuteByTalkPower,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
channelName: snapshotChannelName(
|
||||
_snapshot,
|
||||
_currentVoiceChannelId,
|
||||
),
|
||||
audioStats: _audioStats,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
onConfigure: _onOpenVoiceSettings,
|
||||
onPttHeldChanged: _onOnscreenPttHeldChanged,
|
||||
);
|
||||
// SDD-106 §2/§3 + SRS-209 + SRS-164: listen-only
|
||||
// banner. Self-hides on granted / unknown.
|
||||
final permissionBanner =
|
||||
PermissionStateBanner.fromCallbacks(
|
||||
recordAudioState: _activeRecordAudioState,
|
||||
ensureRecordAudio: _ensureActiveRecordAudio,
|
||||
openAppSettings: _openActivePermissionSettings,
|
||||
);
|
||||
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),
|
||||
onJoinChannelWithPassword: (ch) =>
|
||||
_onJoinChannel(ch, askForPassword: true),
|
||||
onTs3ServerLink: _onTs3ServerLink,
|
||||
);
|
||||
final ownClientState = _ownClientState;
|
||||
const voiceBarWidthWide = 320.0;
|
||||
if (constraints.maxWidth >= _wideBreakpoint) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: voiceBarWidthWide,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
banner,
|
||||
const SizedBox(height: 12),
|
||||
permissionBanner,
|
||||
voiceBar,
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: snapshotView),
|
||||
],
|
||||
);
|
||||
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),
|
||||
onJoinChannelWithPassword: (ch) =>
|
||||
_onJoinChannel(ch, askForPassword: true),
|
||||
onTs3ServerLink: _onTs3ServerLink,
|
||||
);
|
||||
final ownClientState = _ownClientState;
|
||||
const voiceBarWidthWide = 320.0;
|
||||
if (constraints.maxWidth >= _wideBreakpoint) {
|
||||
return Row(
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: voiceBarWidthWide,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
banner,
|
||||
const SizedBox(height: 12),
|
||||
permissionBanner,
|
||||
voiceBar,
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: snapshotView),
|
||||
const SizedBox(height: 8),
|
||||
permissionBanner,
|
||||
VoiceStatusChip(
|
||||
transmitMode: _transmitMode,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
audioStats: _audioStats,
|
||||
isTouchOnly: isTouchOnlyPttHost,
|
||||
inputMuted: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
talkPower: ownClientState?.talkPower,
|
||||
neededTalkPower: ownClientState?.neededTalkPower,
|
||||
talkPowerGranted: ownClientState?.talkPowerGranted,
|
||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
||||
),
|
||||
if (_inChannel &&
|
||||
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
|
||||
const SizedBox(height: 8),
|
||||
VoicePttButton(
|
||||
active: _audioStats?.pttActive ?? false,
|
||||
onHeldChanged: _onOnscreenPttHeldChanged,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(child: snapshotView),
|
||||
const SizedBox(height: 8),
|
||||
permissionBanner,
|
||||
VoiceStatusChip(
|
||||
transmitMode: _transmitMode,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
audioStats: _audioStats,
|
||||
isTouchOnly: isTouchOnlyPttHost,
|
||||
inputMuted: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
talkPower: ownClientState?.talkPower,
|
||||
neededTalkPower: ownClientState?.neededTalkPower,
|
||||
talkPowerGranted: ownClientState?.talkPowerGranted,
|
||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
||||
),
|
||||
if (_inChannel &&
|
||||
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
|
||||
const SizedBox(height: 8),
|
||||
VoicePttButton(
|
||||
active: _audioStats?.pttActive ?? false,
|
||||
onHeldChanged: _onOnscreenPttHeldChanged,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (_isMacOS) {
|
||||
|
||||
Reference in New Issue
Block a user