From 42d0ca0953045d31c130d8cd769cfebda3d3b88d Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Wed, 20 May 2026 09:51:48 +0900 Subject: [PATCH] fix(android): polish app chrome and join guard --- .../chanora/chanora_flutter/MainActivity.kt | 8 +++ apps/chanora_flutter/lib/main.dart | 53 ++++++++++--------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/MainActivity.kt b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/MainActivity.kt index ee3cb88..113d530 100644 --- a/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/MainActivity.kt +++ b/apps/chanora_flutter/android/app/src/main/kotlin/app/chanora/chanora_flutter/MainActivity.kt @@ -1,7 +1,10 @@ package app.chanora.chanora_flutter import android.content.Context +import android.graphics.Color +import android.os.Build import android.os.Bundle +import android.view.View import app.chanora.chanora_flutter.AndroidPermissionRequester import app.chanora.chanora_flutter.BackIntentBridge import app.chanora.chanora_flutter.MethodChannels @@ -76,6 +79,11 @@ class MainActivity : FlutterActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + window.navigationBarColor = Color.rgb(255, 251, 254) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + window.decorView.systemUiVisibility = + window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } // SDD-105: pass the Application context to the Rust side so the // audio engine can open device handles. Must run on the main thread // before any `chanora_audio` call from Dart. The native library is diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index c14ff2d..6136de2 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -31,6 +31,8 @@ import 'widgets/voice_settings.dart'; bool get _isMacOS => !kIsWeb && Platform.isMacOS; +const Color _appSurfaceColor = Color(0xFFFFFBFE); + /// Top padding for macOS to clear traffic-light buttons. const double _macOSTrafficLightPad = 56.0; @@ -205,19 +207,29 @@ class ChanoraApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - onGenerateTitle: (ctx) => AppL10n.of(ctx).appTitle, - // No "DEBUG" banner in the top-right corner. This is purely - // cosmetic for the developer-build experience; release builds - // never render it regardless of this flag. - debugShowCheckedModeBanner: false, - theme: ThemeData( - useMaterial3: true, - colorSchemeSeed: const Color(0xFF3F51B5), + return AnnotatedRegion( + value: const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + systemNavigationBarColor: _appSurfaceColor, + systemNavigationBarDividerColor: _appSurfaceColor, + systemNavigationBarIconBrightness: Brightness.dark, + ), + child: MaterialApp( + onGenerateTitle: (ctx) => AppL10n.of(ctx).appTitle, + // No "DEBUG" banner in the top-right corner. This is purely + // cosmetic for the developer-build experience; release builds + // never render it regardless of this flag. + debugShowCheckedModeBanner: false, + theme: ThemeData( + useMaterial3: true, + colorSchemeSeed: const Color(0xFF3F51B5), + scaffoldBackgroundColor: _appSurfaceColor, + ), + localizationsDelegates: AppL10n.localizationsDelegates, + supportedLocales: AppL10n.supportedLocales, + home: const _BetaHome(), ), - localizationsDelegates: AppL10n.localizationsDelegates, - supportedLocales: AppL10n.supportedLocales, - home: const _BetaHome(), ); } } @@ -703,6 +715,7 @@ class _BetaHomeState extends State<_BetaHome> { password = await _askChannelPassword(l10n); if (password == null) return; // cancelled } + setState(() => _pendingVoiceChannelId = ch.id); try { // SDD-106 §1, §6 + SRS-209: Android runtime permission gate. // Request RECORD_AUDIO at or before voice_join. On denial or @@ -756,6 +769,7 @@ class _BetaHomeState extends State<_BetaHome> { // is selected by TS3 error code per the canonical // catalogue at https://github.com/ReSpeak/tsdeclarations. final message = _channelJoinErrorMessage(l10n, e); + setState(() => _pendingVoiceChannelId = null); messenger.showSnackBar( SnackBar( content: Text(message), @@ -1274,7 +1288,7 @@ class _BetaHomeState extends State<_BetaHome> { ], ]; - const headerTitle = _AppBarTitle(); + const headerTitle = SizedBox.shrink(); final bodyContent = LayoutBuilder( builder: (ctx, bodyConstraints) { @@ -1491,19 +1505,6 @@ class _BetaHomeState extends State<_BetaHome> { } } -/// App header title. The current channel is highlighted only in the -/// channel tree and Voice Bar to avoid repeating the same badge in -/// multiple places. -class _AppBarTitle extends StatelessWidget { - const _AppBarTitle(); - - @override - Widget build(BuildContext context) { - final l10n = AppL10n.of(context); - return Text(l10n.appTitle); - } -} - /// 'Save bookmark' name-entry dialog. Owns its own /// TextEditingController via a StatefulWidget lifecycle so the /// dispose() runs cleanly at unmount time (after the route pop