213 lines
5.6 KiB
Dart
213 lines
5.6 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as desktop;
|
|
import 'package:webview_flutter/webview_flutter.dart' as mobile;
|
|
|
|
bool get _useDesktopWebView =>
|
|
!kIsWeb && (Platform.isWindows || Platform.isLinux);
|
|
|
|
class AnnouncementDialog extends StatefulWidget {
|
|
final String title;
|
|
final String html;
|
|
final String baseUrl;
|
|
|
|
const AnnouncementDialog({
|
|
super.key,
|
|
required this.title,
|
|
required this.html,
|
|
required this.baseUrl,
|
|
});
|
|
|
|
static Future<void> show(
|
|
BuildContext context, {
|
|
required String title,
|
|
required String html,
|
|
required String baseUrl,
|
|
}) async {
|
|
if (html.trim().isEmpty) return;
|
|
|
|
await showDialog<void>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (_) =>
|
|
AnnouncementDialog(title: title, html: html, baseUrl: baseUrl),
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<AnnouncementDialog> createState() => _AnnouncementDialogState();
|
|
}
|
|
|
|
class _AnnouncementDialogState extends State<AnnouncementDialog> {
|
|
mobile.WebViewController? _mobileController;
|
|
desktop.InAppWebViewController? _desktopController;
|
|
late final String _html;
|
|
late final String _baseUrl;
|
|
bool _loading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
final origin = Uri.parse(widget.baseUrl).origin;
|
|
_html = _wrapHtml(widget.html);
|
|
_baseUrl = '$origin/';
|
|
|
|
if (!_useDesktopWebView) {
|
|
_mobileController = mobile.WebViewController()
|
|
..setJavaScriptMode(mobile.JavaScriptMode.unrestricted)
|
|
..setBackgroundColor(Colors.transparent)
|
|
..setNavigationDelegate(
|
|
mobile.NavigationDelegate(
|
|
onPageFinished: (_) {
|
|
if (mounted) setState(() => _loading = false);
|
|
},
|
|
),
|
|
)
|
|
..loadHtmlString(_html, baseUrl: _baseUrl);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_desktopController?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
String _wrapHtml(String body) {
|
|
final encodedTitle = const HtmlEscape().convert(widget.title);
|
|
|
|
return '''
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>$encodedTitle</title>
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: transparent;
|
|
color: #1f2937;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.65;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
body {
|
|
padding: 12px 14px 18px;
|
|
box-sizing: border-box;
|
|
}
|
|
img, video {
|
|
max-width: 100% !important;
|
|
height: auto !important;
|
|
border-radius: 12px;
|
|
}
|
|
a {
|
|
color: #2563eb;
|
|
text-decoration: none;
|
|
}
|
|
fieldset, section, div {
|
|
max-width: 100% !important;
|
|
box-sizing: border-box !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
$body
|
|
</body>
|
|
</html>
|
|
''';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Dialog(
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 28),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(22)),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(22),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
height: MediaQuery.of(context).size.height * 0.72,
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.fromLTRB(18, 14, 8, 10),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surface,
|
|
border: Border(
|
|
bottom: BorderSide(
|
|
color: theme.dividerColor.withValues(alpha: 0.45),
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.title,
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.w800,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
tooltip: '关闭',
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
icon: const Icon(Icons.close),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Stack(
|
|
children: [
|
|
_buildWebView(),
|
|
if (_loading)
|
|
const Center(child: CircularProgressIndicator()),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildWebView() {
|
|
if (_useDesktopWebView) {
|
|
return desktop.InAppWebView(
|
|
initialData: desktop.InAppWebViewInitialData(
|
|
data: _html,
|
|
baseUrl: desktop.WebUri(_baseUrl),
|
|
),
|
|
initialSettings: desktop.InAppWebViewSettings(
|
|
javaScriptEnabled: true,
|
|
domStorageEnabled: true,
|
|
transparentBackground: true,
|
|
supportZoom: false,
|
|
),
|
|
onWebViewCreated: (controller) {
|
|
_desktopController = controller;
|
|
},
|
|
onLoadStop: (_, url) {
|
|
if (mounted) setState(() => _loading = false);
|
|
},
|
|
onReceivedError: (_, request, error) {
|
|
if (mounted) setState(() => _loading = false);
|
|
},
|
|
);
|
|
}
|
|
|
|
return mobile.WebViewWidget(controller: _mobileController!);
|
|
}
|
|
}
|