fix(android): polish app chrome and join guard

This commit is contained in:
Edison Jwa
2026-05-20 14:52:34 +09:00
parent bc70da51df
commit 42d0ca0953
2 changed files with 35 additions and 26 deletions
@@ -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
+27 -26
View File
@@ -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<SystemUiOverlayStyle>(
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