rtc_prd/airhub_app/lib/pages/webview_page.dart
seaislee1209 f9666d4aa3 feat: UI规范化 + 故事吸入动画 + 音乐页面优化
- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化
- 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失)
- 书架页: bookPop弹出+粒子效果(三段式动画完整链路)
- 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局
- 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化
- 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌
- 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面
- 阶段总结文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:34:53 +08:00

111 lines
4.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class WebViewPage extends StatefulWidget {
const WebViewPage({super.key});
@override
State<WebViewPage> createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
late final PlatformWebViewControllerCreationParams params;
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
params = WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
} else {
params = const PlatformWebViewControllerCreationParams();
}
final WebViewController controller =
WebViewController.fromPlatformCreationParams(params);
controller
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(0x00000000))
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onWebResourceError: (WebResourceError error) {
debugPrint('''
Page resource error:
code: ${error.errorCode}
description: ${error.description}
errorType: ${error.errorType}
isForMainFrame: ${error.isForMainFrame}
''');
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.contains('bluetooth.html')) {
// Intercept bluetooth.html and navigate to native BluetoothPage
debugPrint(
'Intercepting navigation to bluetooth.html -> Native Route',
);
// We need context to navigate, but initState doesn't have it easily available
// inside this callback unless we store a reference or use a GlobalKey.
// However, since we are in a State object, we can use 'context' if mounted?
// Actually, NavigationDelegate callbacks are not bound to context directly.
// We should probably move the controller creation or use a helper.
// BUT, since this is a callback, 'context' of the State is available in the closure!
// Warning: don't use 'context' across async gaps without checking mounted.
// Since this is synchronous, it should be fine to schedule a navigation.
// We must return NavigationDecision.prevent to stop WebView.
// And execute navigation asynchronously to avoid blocking.
Future.microtask(() {
if (mounted) {
context.go('/bluetooth');
}
});
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
),
)
..loadFlutterAsset(
'assets/www/index.html',
); // CHANGED: Load Home directly
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(true);
(controller.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}
_controller = controller;
}
@override
Widget build(BuildContext context) {
// We want the WebView to control the full screen, including status bar usually,
// but SafeArea might be needed if the Web content doesn't handle padding.
// Our CSS handles env(safe-area-inset-top), so we can disable SafeArea here
// or keep top:false.
return Scaffold(
backgroundColor: Colors.white,
body: WebViewWidget(controller: _controller),
);
}
}