import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'avatar_bridge.dart'; import 'control_panel_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 { AvatarBridge? _bridge; bool _ready = false; @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 ? FloatingActionButton.extended( 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, ); } }