Files
chanora/apps/chanora_flutter/lib/main.dart
Edison Jwa 7ab50a4eb5 refactor(flutter): split main.dart into screens/ directory (TODO-023)
Extract BetaHome (2374 lines) to screens/home_screen.dart and
LiveDiagnosticsDialog (97 lines) to screens/diagnostics_dialog.dart.
main.dart reduced from 2910 to 255 lines. No behavioral changes.
2026-06-11 20:42:38 +09:00

257 lines
7.3 KiB
Dart

// Chanora Flutter application — External Beta build.
//
// Builds on Internal Beta v0.3.0-beta.1:
// * server password field
// * bookmark list with add / connect / delete actions
// * channel tap-to-join with optional channel password
// * self input + output mute toggles + master output gain slider
// * diagnostics dialog + reconnect banner + identity persistence
// (all carried over from v0.3.0-beta.1)
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'l10n/generated/app_localizations.dart';
import 'services/app_bootstrap.dart';
import 'services/audio_lifecycle_service.dart';
import 'services/ui_preferences_service.dart';
import 'screens/home_screen.dart';
import 'src/rust/api.dart' as rust;
import 'src/rust/frb_generated.dart';
import 'widgets/audio_debug_stats_panel.dart';
bool get isMacOS => !kIsWeb && Platform.isMacOS;
// Debug-only overlay. `kDebugMode` is a compile-time const in
// release builds (= false), so the overlay subtree is tree-shaken
// out of release/profile binaries entirely — release users never
// see internal audio stats and we don't pay the render cost.
bool get _showAudioDebugOverlay => kDebugMode && isMacOS;
const Color _appSurfaceColor = Color(0xFFFFFBFE);
/// Top padding for macOS to clear traffic-light buttons.
const double macOSTrafficLightPad = 56.0;
/// Public version string shown in the About dialog.
String kAppVersion = appSemverBaseline;
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
unawaited(() async {
await wireStorage();
await wireCache();
}());
unawaited(wireConnectivity());
wireAudioLifecycle();
await configureBundledVadModels();
runApp(const ChanoraApp());
unawaited(_finishDeferredStartup());
}
Future<void> _finishDeferredStartup() async {
try {
kAppVersion = await resolveAppVersion();
} catch (_) {}
}
class ChanoraApp extends StatefulWidget {
const ChanoraApp({super.key});
@override
State<ChanoraApp> createState() => _ChanoraAppState();
}
class _ChanoraAppState extends State<ChanoraApp> {
final UiPreferencesService _uiPreferences = const UiPreferencesService();
ThemeMode _themeMode = ThemeMode.system;
@override
void initState() {
super.initState();
unawaited(_loadThemeMode());
}
Future<void> _loadThemeMode() async {
try {
final settings = await _uiPreferences.loadSettings();
if (!mounted) return;
setState(() {
_themeMode = settings.themeMode.toFlutterThemeMode();
});
} catch (_) {}
}
@override
Widget build(BuildContext context) {
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,
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF3F51B5),
scaffoldBackgroundColor: _appSurfaceColor,
),
darkTheme: ThemeData(
useMaterial3: true,
colorSchemeSeed: const Color(0xFF3F51B5),
brightness: Brightness.dark,
),
themeMode: _themeMode,
localizationsDelegates: AppL10n.localizationsDelegates,
supportedLocales: AppL10n.supportedLocales,
home: Stack(
children: [
BetaHome(themeMode: _themeMode, onThemeModeChanged: _setThemeMode),
if (_showAudioDebugOverlay) const AudioDebugStatsPanel(),
],
),
),
);
}
Future<void> _setThemeMode(ThemeMode themeMode) async {
setState(() {
_themeMode = themeMode;
});
try {
await _uiPreferences.saveThemeMode(themeMode.toUiThemeMode());
} catch (_) {}
}
}
extension on UiThemeMode {
ThemeMode toFlutterThemeMode() {
return switch (this) {
UiThemeMode.system => ThemeMode.system,
UiThemeMode.light => ThemeMode.light,
UiThemeMode.dark => ThemeMode.dark,
};
}
}
extension on ThemeMode {
UiThemeMode toUiThemeMode() {
return switch (this) {
ThemeMode.system => UiThemeMode.system,
ThemeMode.light => UiThemeMode.light,
ThemeMode.dark => UiThemeMode.dark,
};
}
}
bool isPokeSenderActiveChat({
required bool chatOpen,
required rust.BridgeMessageTarget? inlineChatTarget,
required BigInt senderId,
}) {
if (!chatOpen) return false;
return switch (inlineChatTarget) {
rust.BridgeMessageTarget_Poke(:final field0) ||
rust.BridgeMessageTarget_Client(:final field0) => field0 == senderId,
_ => false,
};
}
class ChanoraThemeModeMenu extends StatelessWidget {
const ChanoraThemeModeMenu({
super.key,
required this.themeMode,
required this.onThemeModeChanged,
});
final ThemeMode themeMode;
final Future<void> Function(ThemeMode mode) onThemeModeChanged;
@override
Widget build(BuildContext context) {
return PopupMenuButton<ThemeMode>(
tooltip: 'Theme',
icon: const Icon(Icons.palette_outlined),
initialValue: themeMode,
onSelected: (mode) {
unawaited(onThemeModeChanged(mode));
},
itemBuilder: (context) => const [
PopupMenuItem(value: ThemeMode.system, child: Text('System')),
PopupMenuItem(value: ThemeMode.light, child: Text('Light')),
PopupMenuItem(value: ThemeMode.dark, child: Text('Dark')),
],
);
}
}
class ChanoraMobileScaffold extends StatelessWidget {
const ChanoraMobileScaffold({
super.key,
required this.compactIdleChrome,
required this.canDisconnect,
required this.title,
required this.actions,
required this.onDisconnect,
required this.compactHeader,
required this.body,
this.disconnectTooltip = 'Disconnect',
});
final bool compactIdleChrome;
final bool canDisconnect;
final Widget? title;
final List<Widget> actions;
final VoidCallback onDisconnect;
final Widget compactHeader;
final Widget body;
final String disconnectTooltip;
@override
Widget build(BuildContext context) {
final paddedBody = SafeArea(
top: compactIdleChrome,
child: Padding(
padding: const EdgeInsets.all(16),
child: compactIdleChrome
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
compactHeader,
const SizedBox(height: 8),
Expanded(child: body),
],
)
: body,
),
);
return Scaffold(
appBar: compactIdleChrome
? null
: AppBar(
leading: canDisconnect
? IconButton(
tooltip: disconnectTooltip,
icon: const Icon(Icons.arrow_back),
onPressed: onDisconnect,
)
: null,
leadingWidth: canDisconnect ? 44 : null,
titleSpacing: canDisconnect ? 4 : null,
title: title,
actions: actions,
),
body: paddedBody,
);
}
}