import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'avatar_bridge.dart'; import 'control_panel_sheet.dart'; import 'voice_chat/voice_chat_controller.dart'; import 'voice_chat/voice_chat_sheet.dart'; int _serverPort = 8080; /// 探测端口是否可用 —— InAppLocalhostServer.start() 在端口被占时会死等不抛异常, /// 所以必须先用 ServerSocket 试探一下能不能 bind。 Future _isPortFree(int port) async { try { final s = await ServerSocket.bind('127.0.0.1', port); await s.close(); return true; } catch (_) { return false; } } Future main() async { WidgetsFlutterBinding.ensureInitialized(); // 同机多端(macOS + iOS sim 共享 localhost)端口可能冲突,循环找一个空闲端口 for (var port = 8080; port < 8090; port++) { if (await _isPortFree(port)) { await InAppLocalhostServer( documentRoot: 'assets/web', port: port, ).start(); _serverPort = port; debugPrint('[server] listening on localhost:$port'); break; } else { debugPrint('[server] port $port busy, trying next'); } } runApp(const AvatarApp()); } class AvatarApp extends StatelessWidget { const AvatarApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Avatar PoC', debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, brightness: Brightness.dark, scaffoldBackgroundColor: const Color(0xFF1A1A1F), colorScheme: const ColorScheme.dark( primary: Color(0xFF4488DD), surface: Color(0xFF222228), ), ), home: const AvatarHomePage(), ); } } class AvatarHomePage extends StatefulWidget { const AvatarHomePage({super.key}); @override State createState() => _AvatarHomePageState(); } class _AvatarHomePageState extends State with WidgetsBindingObserver { AvatarBridge? _bridge; bool _ready = false; VoiceChatController? _voiceController; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _voiceController?.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final controller = _voiceController; if (controller == null) return; if (state == AppLifecycleState.resumed) { // 回到前台:重新建立会话 controller.startSession(); } else if (state == AppLifecycleState.paused) { controller.stopSession(); } } void _ensureVoiceSessionStarted() { if (_bridge == null) return; _voiceController ??= VoiceChatController(bridge: _bridge!); _voiceController!.startSession(); } Widget _buildVoiceFab() { // 第一次进入页面时立刻启动会话;同时监听 controller 让 FAB 颜色实时反映状态 _ensureVoiceSessionStarted(); return ListenableBuilder( listenable: _voiceController!, builder: (context, _) { final c = _voiceController!; final (label, bg) = c.connected ? ('语音对话 · 已连接', const Color(0xFF1E9F4D)) // 绿色 : c.connecting ? ('语音对话 · 连接中', const Color(0xFFD08F2C)) // 黄色 : ('语音对话 · 未连接', const Color(0xFF6B6B73)); // 灰色 return FloatingActionButton.extended( heroTag: 'voice', onPressed: () { // 单击只展开抽屉看消息/日志;会话本身在后台一直跑 showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => VoiceChatSheet(controller: c), ); }, icon: Icon(c.connected ? Icons.graphic_eq : Icons.sync_problem), label: Text(label), backgroundColor: bg, foregroundColor: Colors.white, ); }, ); } @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Stack( children: [ InAppWebView( initialUrlRequest: URLRequest( url: WebUri('http://localhost:$_serverPort/index.html'), ), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, transparentBackground: false, allowsInlineMediaPlayback: true, mediaPlaybackRequiresUserGesture: false, // macOS / iOS:允许 localhost 加载 allowsBackForwardNavigationGestures: false, ), onWebViewCreated: (controller) { _bridge = AvatarBridge(controller); }, onLoadStop: (controller, _) async { // 隐藏 H5 内置调试面板(Flutter 接管控制权) await controller.evaluateJavascript(source: ''' const p = document.getElementById('debug-panel'); if (p) p.style.display = 'none'; '''); // 等 avatar 全局对象注册完成 await _bridge?.waitReady(); if (mounted) setState(() => _ready = true); }, onConsoleMessage: (_, msg) { debugPrint('[webview] ${msg.message}'); }, ), if (!_ready) const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 12), Text('Loading Live2D...'), ], ), ), ], ), ), floatingActionButton: _ready ? Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildVoiceFab(), const SizedBox(height: 12), FloatingActionButton.extended( heroTag: 'panel', onPressed: () { if (_bridge == null) return; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (_) => ControlPanelSheet(bridge: _bridge!), ); }, icon: const Icon(Icons.tune), label: const Text('控制面板'), backgroundColor: const Color(0xFF2766B6), foregroundColor: Colors.white, ), ], ) : null, ); } }