feat: 构建flutter 基座
This commit is contained in:
314
lib/main.dart
Normal file
314
lib/main.dart
Normal file
@@ -0,0 +1,314 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.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';
|
||||
|
||||
const _homeUrl = 'https://h5-im.imharry.work/';
|
||||
const _stopWebMediaScript = r'''
|
||||
(() => {
|
||||
try {
|
||||
window.__stopOpenIMVoicePlayback?.();
|
||||
} catch (_) {}
|
||||
document.querySelectorAll('audio, video').forEach((media) => {
|
||||
try {
|
||||
media.pause();
|
||||
media.currentTime = 0;
|
||||
} catch (_) {}
|
||||
});
|
||||
})();
|
||||
''';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
systemNavigationBarColor: Colors.white,
|
||||
systemNavigationBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
runApp(const ImWebViewApp());
|
||||
}
|
||||
|
||||
class ImWebViewApp extends StatelessWidget {
|
||||
const ImWebViewApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: '集中营',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6FEB)),
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const H5ShellPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class H5ShellPage extends StatefulWidget {
|
||||
const H5ShellPage({super.key});
|
||||
|
||||
@override
|
||||
State<H5ShellPage> createState() => _H5ShellPageState();
|
||||
}
|
||||
|
||||
class _H5ShellPageState extends State<H5ShellPage> with WidgetsBindingObserver {
|
||||
late final WebViewController _controller;
|
||||
|
||||
int _progress = 0;
|
||||
String? _loadError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_controller = _buildController()..loadRequest(Uri.parse(_homeUrl));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.inactive ||
|
||||
state == AppLifecycleState.hidden ||
|
||||
state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached) {
|
||||
unawaited(_stopWebMedia());
|
||||
}
|
||||
}
|
||||
|
||||
WebViewController _buildController() {
|
||||
PlatformWebViewControllerCreationParams params =
|
||||
const PlatformWebViewControllerCreationParams();
|
||||
|
||||
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
|
||||
params = WebKitWebViewControllerCreationParams(
|
||||
allowsInlineMediaPlayback: true,
|
||||
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
|
||||
);
|
||||
} else if (WebViewPlatform.instance is AndroidWebViewPlatform) {
|
||||
params = AndroidWebViewControllerCreationParams
|
||||
.fromPlatformWebViewControllerCreationParams(params);
|
||||
}
|
||||
|
||||
final controller = WebViewController.fromPlatformCreationParams(
|
||||
params,
|
||||
onPermissionRequest: (request) {
|
||||
unawaited(_handleWebViewPermissionRequest(request));
|
||||
},
|
||||
)
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Colors.white)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (progress) {
|
||||
if (mounted) {
|
||||
setState(() => _progress = progress);
|
||||
}
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loadError = null;
|
||||
_progress = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
onPageFinished: (_) {
|
||||
if (mounted) {
|
||||
setState(() => _progress = 100);
|
||||
}
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
if (error.isForMainFrame ?? true) {
|
||||
if (mounted) {
|
||||
setState(() => _loadError = error.description);
|
||||
}
|
||||
}
|
||||
},
|
||||
onUrlChange: (_) {
|
||||
unawaited(_stopWebMedia());
|
||||
},
|
||||
onNavigationRequest: _handleNavigationRequest,
|
||||
),
|
||||
);
|
||||
|
||||
final platformController = controller.platform;
|
||||
if (platformController is AndroidWebViewController) {
|
||||
AndroidWebViewController.enableDebugging(false);
|
||||
unawaited(platformController.setMediaPlaybackRequiresUserGesture(false));
|
||||
unawaited(platformController.setGeolocationEnabled(true));
|
||||
unawaited(
|
||||
platformController.setGeolocationPermissionsPromptCallbacks(
|
||||
onShowPrompt: (_) async {
|
||||
final allowed =
|
||||
await _requestPermission(Permission.locationWhenInUse);
|
||||
return GeolocationPermissionsResponse(
|
||||
allow: allowed,
|
||||
retain: allowed,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
Future<void> _runJavaScriptSafely(String source) async {
|
||||
try {
|
||||
await _controller.runJavaScript(source);
|
||||
} catch (_) {
|
||||
// WebView can reject JavaScript while a page is still navigating.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _stopWebMedia() {
|
||||
return _runJavaScriptSafely(_stopWebMediaScript);
|
||||
}
|
||||
|
||||
Future<NavigationDecision> _handleNavigationRequest(
|
||||
NavigationRequest request,
|
||||
) async {
|
||||
unawaited(_stopWebMedia());
|
||||
|
||||
final uri = Uri.tryParse(request.url);
|
||||
if (uri == null) {
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
||||
const webSchemes = {'http', 'https', 'about', 'data'};
|
||||
if (webSchemes.contains(uri.scheme)) {
|
||||
return NavigationDecision.navigate;
|
||||
}
|
||||
|
||||
try {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
// Ignore unsupported custom schemes so the WebView does not navigate to
|
||||
// an error page.
|
||||
}
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
|
||||
Future<void> _handleWebViewPermissionRequest(
|
||||
WebViewPermissionRequest request,
|
||||
) async {
|
||||
final permissions = <Permission>[];
|
||||
if (request.types.contains(WebViewPermissionResourceType.camera)) {
|
||||
permissions.add(Permission.camera);
|
||||
}
|
||||
if (request.types.contains(WebViewPermissionResourceType.microphone)) {
|
||||
permissions.add(Permission.microphone);
|
||||
}
|
||||
|
||||
final allowed = permissions.isEmpty ||
|
||||
await Future.wait(permissions.map(_requestPermission))
|
||||
.then((results) => results.every((allowed) => allowed));
|
||||
|
||||
if (allowed) {
|
||||
await request.grant();
|
||||
} else {
|
||||
await request.deny();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _requestPermission(Permission permission) async {
|
||||
final status = await permission.request();
|
||||
return status.isGranted || status.isLimited;
|
||||
}
|
||||
|
||||
Future<void> _handleBackNavigation() async {
|
||||
await _stopWebMedia();
|
||||
if (await _controller.canGoBack()) {
|
||||
await _controller.goBack();
|
||||
} else {
|
||||
await SystemNavigator.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop) {
|
||||
unawaited(_handleBackNavigation());
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
if (_progress < 100)
|
||||
LinearProgressIndicator(
|
||||
value: _progress == 0 ? null : _progress / 100,
|
||||
minHeight: 2,
|
||||
),
|
||||
if (_loadError != null)
|
||||
_ErrorPanel(
|
||||
message: _loadError!,
|
||||
onRetry: () => _controller.loadRequest(Uri.parse(_homeUrl)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ErrorPanel extends StatelessWidget {
|
||||
const _ErrorPanel({required this.message, required this.onRetry});
|
||||
|
||||
final String message;
|
||||
final VoidCallback onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.wifi_off_rounded, size: 44),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'页面加载失败',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
message,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
onPressed: onRetry,
|
||||
child: const Text('重新加载'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user