110 lines
3.5 KiB
Dart
110 lines
3.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:media_kit/media_kit.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
import 'package:trainhub_flutter/core/router/app_router.dart';
|
|
import 'package:trainhub_flutter/core/theme/app_theme.dart';
|
|
import 'package:trainhub_flutter/domain/services/ai_process_manager.dart';
|
|
import 'package:trainhub_flutter/injection.dart' as di;
|
|
import 'package:trainhub_flutter/presentation/settings/ai_model_settings_controller.dart';
|
|
import 'package:trainhub_flutter/presentation/settings/ai_model_settings_state.dart';
|
|
|
|
void main() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
MediaKit.ensureInitialized();
|
|
await windowManager.ensureInitialized();
|
|
|
|
di.init();
|
|
|
|
const windowOptions = WindowOptions(
|
|
size: Size(1280, 800),
|
|
minimumSize: Size(800, 600),
|
|
center: true,
|
|
backgroundColor: Colors.transparent,
|
|
skipTaskbar: false,
|
|
titleBarStyle: TitleBarStyle.normal,
|
|
title: 'TrainHub',
|
|
);
|
|
|
|
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
|
await windowManager.show();
|
|
await windowManager.focus();
|
|
});
|
|
|
|
runApp(const ProviderScope(child: TrainHubApp()));
|
|
}
|
|
|
|
// =============================================================================
|
|
// Root application widget
|
|
// =============================================================================
|
|
class TrainHubApp extends ConsumerStatefulWidget {
|
|
const TrainHubApp({super.key});
|
|
|
|
@override
|
|
ConsumerState<TrainHubApp> createState() => _TrainHubAppState();
|
|
}
|
|
|
|
class _TrainHubAppState extends ConsumerState<TrainHubApp>
|
|
with WindowListener {
|
|
// Create the router once and reuse it across rebuilds.
|
|
final _appRouter = AppRouter();
|
|
|
|
// Guard flag so we never start the servers more than once per app session.
|
|
bool _serversStarted = false;
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Lifecycle
|
|
// -------------------------------------------------------------------------
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
windowManager.addListener(this);
|
|
// Intercept the OS close event so we can kill child processes first.
|
|
windowManager.setPreventClose(true);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
windowManager.removeListener(this);
|
|
super.dispose();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// WindowListener — critical: kill servers before the window is destroyed
|
|
// so they don't become zombie processes consuming 5 GB of RAM.
|
|
// -------------------------------------------------------------------------
|
|
|
|
@override
|
|
void onWindowClose() async {
|
|
await di.getIt<AiProcessManager>().stopServers();
|
|
await windowManager.destroy();
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Build
|
|
// -------------------------------------------------------------------------
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Watch the model-settings state and start servers exactly once, the
|
|
// first time models are confirmed to be present on disk.
|
|
ref.listen<AiModelSettingsState>(
|
|
aiModelSettingsControllerProvider,
|
|
(prev, next) {
|
|
if (!_serversStarted && next.areModelsValidated) {
|
|
_serversStarted = true;
|
|
di.getIt<AiProcessManager>().startServers();
|
|
}
|
|
},
|
|
);
|
|
|
|
return MaterialApp.router(
|
|
title: 'TrainHub',
|
|
theme: AppTheme.dark,
|
|
routerConfig: _appRouter.config(),
|
|
debugShowCheckedModeBanner: false,
|
|
);
|
|
}
|
|
}
|