From 25b5bb3a6dbcb77a375d2238369f3816136cc06f Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sat, 16 May 2026 12:15:13 +0800 Subject: [PATCH] fix(ui): wide-mode banner placement + channel-pill overflow Two related VoiceBar / scaffold issues on wide windows: 1. Banner placement The 'not production ready' tertiaryContainer banner sat full-width above the body Column. In wide layouts (>=840 dp) where the connected view splits into Voice Bar (320 dp) + channel tree (Expanded), the banner spanned both columns and dwarfed the channel-tree pane. Rework: wrap the body in an outer LayoutBuilder so the placement decision can read bodyConstraints.maxWidth. When wide AND connected AND snapshot != null, render the banner inside the left 320 dp SizedBox above the VoiceBar. In every other state (narrow, idle, connecting) the banner stays pinned full-width at the top. 2. Channel-name pill overflow The Container holding the channel pill had no width constraint and Text(channelName) had no overflow handling. Long channel names made the pill extend past the column's 320 dp; mute icons slid under the adjacent channel tree. Rework: pill wrapped in Flexible(flex: 100, fit: FlexFit.loose); inner Text gets maxLines: 1, overflow: TextOverflow.ellipsis, softWrap: false. Spacer keeps default flex 1; the 100:1 ratio means short names hug their intrinsic width and long names take ~99% of the remaining space then ellipsize. Mute icons stay pinned right. flutter analyze: clean (6 pre-existing Radio.groupValue infos only). --- apps/chanora_flutter/lib/main.dart | 44 +++++++++++-- .../lib/widgets/voice_bar.dart | 65 ++++++++++++------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index a35c50e..b1a51b4 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -823,10 +823,22 @@ class _BetaHomeState extends State<_BetaHome> { ), body: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( + child: LayoutBuilder( + builder: (ctx, bodyConstraints) { + // Shared breakpoint with the inner snapshot LayoutBuilder + // below — when the UI is wide enough to split into two + // columns AND we are showing the snapshot, the + // not-production-ready banner is moved into the left + // column above the Voice Bar so it doesn't span the + // wider channel-tree area. In every other state (narrow, + // or idle / connecting at any width) the banner stays + // pinned to the top of the body. + const wideBreakpoint = 840.0; + final isWideSnapshot = + bodyConstraints.maxWidth >= wideBreakpoint && + _phase == _Phase.connected && + _snapshot != null; + final banner = Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: theme.colorScheme.tertiaryContainer, @@ -836,8 +848,14 @@ class _BetaHomeState extends State<_BetaHome> { l10n.homeNotProductionReadyBanner, style: TextStyle(color: theme.colorScheme.onTertiaryContainer), ), - ), - const SizedBox(height: 12), + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isWideSnapshot) ...[ + banner, + const SizedBox(height: 12), + ], Text(statusText(), style: theme.textTheme.titleMedium), if (_lostReason != null || _reconnectAttempt != null) ...[ const SizedBox(height: 8), @@ -943,7 +961,17 @@ class _BetaHomeState extends State<_BetaHome> { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(width: voiceBarWidthWide, child: voiceBar), + SizedBox( + width: voiceBarWidthWide, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + banner, + const SizedBox(height: 12), + voiceBar, + ], + ), + ), const SizedBox(width: 12), Expanded(child: snapshotView), ], @@ -962,6 +990,8 @@ class _BetaHomeState extends State<_BetaHome> { ), ], ], + ); + }, ), ), ); diff --git a/apps/chanora_flutter/lib/widgets/voice_bar.dart b/apps/chanora_flutter/lib/widgets/voice_bar.dart index 27e8f18..78f764a 100644 --- a/apps/chanora_flutter/lib/widgets/voice_bar.dart +++ b/apps/chanora_flutter/lib/widgets/voice_bar.dart @@ -110,39 +110,54 @@ class VoiceBar extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Row 1: channel pill + mute toggle + // Row 1: channel pill + mute toggle. The pill is + // wrapped in `Flexible` so very long channel names + // truncate with an ellipsis instead of overflowing the + // Voice Bar's column width (320 dp in the wide layout) + // and pushing the mute icons under the adjacent channel + // tree. Row( children: [ if (inChannel && channelName.isNotEmpty) ...[ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, - ), - decoration: BoxDecoration( - color: theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.tag, - size: 14, - color: theme.colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 4), - Text( - channelName, - style: TextStyle( + Flexible( + flex: 100, + fit: FlexFit.loose, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.tag, + size: 14, color: theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w600, ), - ), - ], + const SizedBox(width: 4), + Flexible( + child: Text( + channelName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), ), ), ], + const SizedBox(width: 8), const Spacer(), IconButton( tooltip: l10n.voiceOutputMuteLabel,