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.
257 lines
7.3 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|