修复处理内容包括公告 WebView、文件预览页、分享页兜底获取下载链接的隐藏 WebView。
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart' as desktop;
|
||||
import 'package:webview_flutter/webview_flutter.dart' as mobile;
|
||||
|
||||
import '../../../data/models/file_model.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
|
||||
bool get _useDesktopWebView =>
|
||||
!kIsWeb && (Platform.isWindows || Platform.isLinux);
|
||||
|
||||
/// 完全交给 Cloudreve 官方 Web 前端处理文件打开。
|
||||
///
|
||||
/// 这里不再自己读取 file_viewers,也不再自己创建 viewerSession。
|
||||
@@ -26,7 +32,8 @@ class CloudreveFileAppPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
late final WebViewController _controller;
|
||||
mobile.WebViewController? _mobileController;
|
||||
desktop.InAppWebViewController? _desktopController;
|
||||
|
||||
bool _isPreparing = true;
|
||||
bool _sessionInjected = false;
|
||||
@@ -40,36 +47,47 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Colors.transparent)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (progress) {
|
||||
if (!mounted) return;
|
||||
setState(() => _progress = progress);
|
||||
},
|
||||
onPageFinished: (_) => _injectSessionAndOpenIfNeeded(),
|
||||
onWebResourceError: (error) {
|
||||
if (!mounted) return;
|
||||
if (error.isForMainFrame == true) {
|
||||
setState(() {
|
||||
_error = '${error.errorCode}: ${error.description}';
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
if (!_useDesktopWebView) {
|
||||
_mobileController = mobile.WebViewController()
|
||||
..setJavaScriptMode(mobile.JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Colors.transparent)
|
||||
..setNavigationDelegate(
|
||||
mobile.NavigationDelegate(
|
||||
onProgress: (progress) {
|
||||
if (!mounted) return;
|
||||
setState(() => _progress = progress);
|
||||
},
|
||||
onPageFinished: (_) => _injectSessionAndOpenIfNeeded(),
|
||||
onWebResourceError: (error) {
|
||||
if (!mounted) return;
|
||||
if (error.isForMainFrame == true) {
|
||||
setState(() {
|
||||
_error = '${error.errorCode}: ${error.description}';
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_prepareAndOpen();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_desktopController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _prepareAndOpen() async {
|
||||
setState(() {
|
||||
_isPreparing = true;
|
||||
_error = null;
|
||||
_progress = 0;
|
||||
_sessionInjected = false;
|
||||
if (_useDesktopWebView) {
|
||||
_desktopController = null;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -80,7 +98,7 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
|
||||
// 先打开同源页面,再注入 localStorage。
|
||||
// 这样 Cloudreve 官方前端可以直接复用 App 的 access_token。
|
||||
await _controller.loadRequest(_originUri);
|
||||
await _loadOrigin();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() => _isPreparing = false);
|
||||
@@ -105,7 +123,9 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
|
||||
Uri _buildCloudreveHomeUri() {
|
||||
final parent = _parentUri(widget.file.relativePath);
|
||||
final openTarget = widget.file.id.isNotEmpty ? widget.file.id : widget.file.relativePath;
|
||||
final openTarget = widget.file.id.isNotEmpty
|
||||
? widget.file.id
|
||||
: widget.file.relativePath;
|
||||
|
||||
return Uri(
|
||||
scheme: _originUri.scheme,
|
||||
@@ -121,7 +141,9 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
}
|
||||
|
||||
String _parentUri(String uri) {
|
||||
final normalized = uri.endsWith('/') ? uri.substring(0, uri.length - 1) : uri;
|
||||
final normalized = uri.endsWith('/')
|
||||
? uri.substring(0, uri.length - 1)
|
||||
: uri;
|
||||
final index = normalized.lastIndexOf('/');
|
||||
if (index <= 'cloudreve://my'.length) {
|
||||
return 'cloudreve://my';
|
||||
@@ -138,7 +160,9 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
|
||||
Map<String, dynamic>? user;
|
||||
try {
|
||||
final response = await ApiService.instance.get<Map<String, dynamic>>('/user/me');
|
||||
final response = await ApiService.instance.get<Map<String, dynamic>>(
|
||||
'/user/me',
|
||||
);
|
||||
user = Map<String, dynamic>.from(response);
|
||||
} catch (_) {
|
||||
user = null;
|
||||
@@ -181,15 +205,16 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
|
||||
try {
|
||||
if (sessionJson != null) {
|
||||
final script = '''
|
||||
final script =
|
||||
'''
|
||||
try {
|
||||
localStorage.setItem('cloudreve_session', ${jsonEncode(sessionJson)});
|
||||
} catch (e) {}
|
||||
window.location.replace(${jsonEncode(target)});
|
||||
''';
|
||||
await _controller.runJavaScript(script);
|
||||
await _runJavaScript(script);
|
||||
} else {
|
||||
await _controller.loadRequest(_targetUri);
|
||||
await _loadTarget();
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
@@ -199,17 +224,52 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
|
||||
Future<void> _openTargetAgain() async {
|
||||
_sessionInjected = true;
|
||||
await _controller.loadRequest(_targetUri);
|
||||
await _loadTarget();
|
||||
}
|
||||
|
||||
Future<void> _reloadWebView() async {
|
||||
if (_useDesktopWebView) {
|
||||
await _desktopController?.reload();
|
||||
} else {
|
||||
await _mobileController?.reload();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadOrigin() async {
|
||||
if (_useDesktopWebView) {
|
||||
final controller = _desktopController;
|
||||
if (controller == null) return;
|
||||
await _desktopController?.loadUrl(
|
||||
urlRequest: desktop.URLRequest(url: desktop.WebUri.uri(_originUri)),
|
||||
);
|
||||
} else {
|
||||
await _mobileController!.loadRequest(_originUri);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadTarget() async {
|
||||
if (_useDesktopWebView) {
|
||||
await _desktopController?.loadUrl(
|
||||
urlRequest: desktop.URLRequest(url: desktop.WebUri.uri(_targetUri)),
|
||||
);
|
||||
} else {
|
||||
await _mobileController!.loadRequest(_targetUri);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runJavaScript(String source) async {
|
||||
if (_useDesktopWebView) {
|
||||
await _desktopController?.evaluateJavascript(source: source);
|
||||
} else {
|
||||
await _mobileController!.runJavaScript(source);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.file.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
title: Text(widget.file.name, overflow: TextOverflow.ellipsis),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: '重新打开',
|
||||
@@ -219,7 +279,7 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
IconButton(
|
||||
tooltip: '刷新',
|
||||
icon: const Icon(Icons.refresh),
|
||||
onPressed: () => _controller.reload(),
|
||||
onPressed: _reloadWebView,
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
@@ -260,6 +320,32 @@ class _CloudreveFileAppPageState extends State<CloudreveFileAppPage> {
|
||||
);
|
||||
}
|
||||
|
||||
return WebViewWidget(controller: _controller);
|
||||
if (_useDesktopWebView) {
|
||||
return desktop.InAppWebView(
|
||||
initialSettings: desktop.InAppWebViewSettings(
|
||||
javaScriptEnabled: true,
|
||||
domStorageEnabled: true,
|
||||
transparentBackground: true,
|
||||
supportZoom: false,
|
||||
),
|
||||
onWebViewCreated: (controller) async {
|
||||
_desktopController = controller;
|
||||
if (!_isPreparing) {
|
||||
await _loadOrigin();
|
||||
}
|
||||
},
|
||||
onProgressChanged: (_, progress) {
|
||||
if (!mounted) return;
|
||||
setState(() => _progress = progress);
|
||||
},
|
||||
onLoadStop: (_, url) => _injectSessionAndOpenIfNeeded(),
|
||||
onReceivedError: (_, request, error) {
|
||||
if (!mounted || request.isForMainFrame != true) return;
|
||||
setState(() => _error = '${error.type}: ${error.description}');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return mobile.WebViewWidget(controller: _mobileController!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import 'dart:async';
|
||||
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:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart' as mobile;
|
||||
|
||||
import '../../../core/utils/app_logger.dart';
|
||||
import '../../../services/api_service.dart';
|
||||
@@ -14,13 +17,13 @@ import '../../providers/download_manager_provider.dart';
|
||||
import '../../widgets/folder_picker.dart';
|
||||
import '../../widgets/user_avatar.dart';
|
||||
|
||||
bool get _useDesktopWebView =>
|
||||
!kIsWeb && (Platform.isWindows || Platform.isLinux);
|
||||
|
||||
class ShareLinkPage extends StatefulWidget {
|
||||
final ShareLinkCandidate candidate;
|
||||
|
||||
const ShareLinkPage({
|
||||
super.key,
|
||||
required this.candidate,
|
||||
});
|
||||
const ShareLinkPage({super.key, required this.candidate});
|
||||
|
||||
@override
|
||||
State<ShareLinkPage> createState() => _ShareLinkPageState();
|
||||
@@ -49,7 +52,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
final baseUri = Uri.tryParse(ApiService.instance.dio.options.baseUrl);
|
||||
if (shareUri == null || baseUri == null) return false;
|
||||
final same = shareUri.host == baseUri.host;
|
||||
AppLogger.d('ShareLinkPage _isSameOrigin: shareHost=${shareUri.host}, baseHost=${baseUri.host}, result=$same');
|
||||
AppLogger.d(
|
||||
'ShareLinkPage _isSameOrigin: shareHost=${shareUri.host}, baseHost=${baseUri.host}, result=$same',
|
||||
);
|
||||
return same;
|
||||
}
|
||||
|
||||
@@ -165,8 +170,11 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
final info = _info;
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_singleFile = info == null ? null : ShareLinkService.instance.fileFromShareInfo(info);
|
||||
_contextHint = _singleFile?.contextHint ?? _contextHint ?? info?.contextHint;
|
||||
_singleFile = info == null
|
||||
? null
|
||||
: ShareLinkService.instance.fileFromShareInfo(info);
|
||||
_contextHint =
|
||||
_singleFile?.contextHint ?? _contextHint ?? info?.contextHint;
|
||||
_fileError = null;
|
||||
_loadingFiles = false;
|
||||
});
|
||||
@@ -280,9 +288,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
return;
|
||||
} catch (downloadError) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('创建下载任务失败:$downloadError')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('创建下载任务失败:$downloadError')));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -311,20 +319,17 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
}
|
||||
|
||||
final task = await context.read<DownloadManagerProvider>().addDownloadTask(
|
||||
fileName: fileName,
|
||||
fileUri: fileUri,
|
||||
fileSize: fileSize,
|
||||
);
|
||||
fileName: fileName,
|
||||
fileUri: fileUri,
|
||||
fileSize: fileSize,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(task == null ? '该文件已在下载队列中' : '已添加到下载队列,正在下载'),
|
||||
),
|
||||
SnackBar(content: Text(task == null ? '该文件已在下载队列中' : '已添加到下载队列,正在下载')),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Future<String?> _obtainDownloadUrlFromOfficialPage({
|
||||
required String fileName,
|
||||
}) async {
|
||||
@@ -332,7 +337,7 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
if (shareUri == null) return null;
|
||||
|
||||
final completer = Completer<String?>();
|
||||
late final WebViewController controller;
|
||||
mobile.WebViewController? mobileController;
|
||||
BuildContext? dialogContext;
|
||||
Timer? timeoutTimer;
|
||||
|
||||
@@ -341,42 +346,45 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
completer.complete(url);
|
||||
}
|
||||
|
||||
controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..addJavaScriptChannel(
|
||||
'CloudreveDownloadBridge',
|
||||
onMessageReceived: (message) {
|
||||
final url = _extractOfficialDownloadUrl(
|
||||
message.message,
|
||||
shareUri: shareUri,
|
||||
);
|
||||
if (url != null && url.isNotEmpty) {
|
||||
complete(url);
|
||||
}
|
||||
},
|
||||
)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageFinished: (_) async {
|
||||
await _installOfficialDownloadHook(controller);
|
||||
},
|
||||
onNavigationRequest: (request) {
|
||||
final url = request.url;
|
||||
if (_looksLikeDirectDownloadUrl(url, shareUri: shareUri)) {
|
||||
complete(url);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onUrlChange: (change) {
|
||||
final url = change.url;
|
||||
if (url != null && _looksLikeDirectDownloadUrl(url, shareUri: shareUri)) {
|
||||
if (!_useDesktopWebView) {
|
||||
mobileController = mobile.WebViewController()
|
||||
..setJavaScriptMode(mobile.JavaScriptMode.unrestricted)
|
||||
..addJavaScriptChannel(
|
||||
'CloudreveDownloadBridge',
|
||||
onMessageReceived: (message) {
|
||||
final url = _extractOfficialDownloadUrl(
|
||||
message.message,
|
||||
shareUri: shareUri,
|
||||
);
|
||||
if (url != null && url.isNotEmpty) {
|
||||
complete(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(shareUri);
|
||||
)
|
||||
..setNavigationDelegate(
|
||||
mobile.NavigationDelegate(
|
||||
onPageFinished: (_) async {
|
||||
await _installOfficialDownloadHookForMobile(mobileController!);
|
||||
},
|
||||
onNavigationRequest: (request) {
|
||||
final url = request.url;
|
||||
if (_looksLikeDirectDownloadUrl(url, shareUri: shareUri)) {
|
||||
complete(url);
|
||||
return mobile.NavigationDecision.prevent;
|
||||
}
|
||||
return mobile.NavigationDecision.navigate;
|
||||
},
|
||||
onUrlChange: (change) {
|
||||
final url = change.url;
|
||||
if (url != null &&
|
||||
_looksLikeDirectDownloadUrl(url, shareUri: shareUri)) {
|
||||
complete(url);
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(shareUri);
|
||||
}
|
||||
|
||||
timeoutTimer = Timer(const Duration(seconds: 24), () => complete(null));
|
||||
|
||||
@@ -409,7 +417,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'不会自动跳转浏览器;浏览器入口只作为手动备用方案。',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
SizedBox(
|
||||
@@ -417,7 +427,32 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
height: 1,
|
||||
child: Opacity(
|
||||
opacity: 0.01,
|
||||
child: WebViewWidget(controller: controller),
|
||||
child: _useDesktopWebView
|
||||
? _OfficialDownloadDesktopWebView(
|
||||
shareUri: shareUri,
|
||||
onBridgeMessage: (message) {
|
||||
final url = _extractOfficialDownloadUrl(
|
||||
message,
|
||||
shareUri: shareUri,
|
||||
);
|
||||
if (url != null && url.isNotEmpty) {
|
||||
complete(url);
|
||||
}
|
||||
},
|
||||
onNavigationUrl: (url) {
|
||||
if (_looksLikeDirectDownloadUrl(
|
||||
url,
|
||||
shareUri: shareUri,
|
||||
)) {
|
||||
complete(url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
buildHookScript: _officialDownloadHookScript,
|
||||
buildClickScript: _officialDownloadClickScript,
|
||||
)
|
||||
: mobile.WebViewWidget(controller: mobileController!),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -427,10 +462,7 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
onPressed: () => complete(null),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _openExternal,
|
||||
child: const Text('浏览器打开'),
|
||||
),
|
||||
TextButton(onPressed: _openExternal, child: const Text('浏览器打开')),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -440,8 +472,18 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _installOfficialDownloadHook(WebViewController controller) async {
|
||||
const hookScript = r"""
|
||||
Future<void> _installOfficialDownloadHookForMobile(
|
||||
mobile.WebViewController controller,
|
||||
) async {
|
||||
try {
|
||||
await controller.runJavaScript(_officialDownloadHookScript);
|
||||
await controller.runJavaScript(_officialDownloadClickScript);
|
||||
} catch (_) {
|
||||
// The outer flow times out and leaves the browser fallback available.
|
||||
}
|
||||
}
|
||||
|
||||
static const String _officialDownloadHookScript = r"""
|
||||
(function () {
|
||||
if (window.__cloudreveAppDownloadHookInstalled) return;
|
||||
window.__cloudreveAppDownloadHookInstalled = true;
|
||||
@@ -449,7 +491,11 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
function post(value) {
|
||||
try {
|
||||
if (typeof value !== 'string') value = JSON.stringify(value);
|
||||
CloudreveDownloadBridge.postMessage(value);
|
||||
if (typeof CloudreveDownloadBridge !== 'undefined' && CloudreveDownloadBridge.postMessage) {
|
||||
CloudreveDownloadBridge.postMessage(value);
|
||||
} else if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
|
||||
window.flutter_inappwebview.callHandler('CloudreveDownloadBridge', value);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
@@ -521,7 +567,7 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
})();
|
||||
""";
|
||||
|
||||
const clickScript = r"""
|
||||
static const String _officialDownloadClickScript = r"""
|
||||
(function () {
|
||||
function visible(el) {
|
||||
if (!el) return false;
|
||||
@@ -571,20 +617,13 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
})();
|
||||
""";
|
||||
|
||||
try {
|
||||
await controller.runJavaScript(hookScript);
|
||||
await controller.runJavaScript(clickScript);
|
||||
} catch (_) {
|
||||
// 官方页面脚本注入失败时,外层会超时并保留“浏览器打开”。
|
||||
}
|
||||
}
|
||||
|
||||
String? _extractOfficialDownloadUrl(String message, {required Uri shareUri}) {
|
||||
final text = message.trim();
|
||||
if (text.isEmpty) return null;
|
||||
|
||||
final direct = _normalizeOfficialUrl(text, shareUri: shareUri);
|
||||
if (direct != null && _looksLikeDirectDownloadUrl(direct, shareUri: shareUri)) {
|
||||
if (direct != null &&
|
||||
_looksLikeDirectDownloadUrl(direct, shareUri: shareUri)) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
@@ -601,7 +640,8 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
|
||||
if (value is String) {
|
||||
final normalized = _normalizeOfficialUrl(value, shareUri: shareUri);
|
||||
if (normalized != null && _looksLikeDirectDownloadUrl(normalized, shareUri: shareUri)) {
|
||||
if (normalized != null &&
|
||||
_looksLikeDirectDownloadUrl(normalized, shareUri: shareUri)) {
|
||||
return normalized;
|
||||
}
|
||||
return null;
|
||||
@@ -618,8 +658,12 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
if (value is Map) {
|
||||
final navigationUrl = value['__navigation_url'];
|
||||
if (navigationUrl is String) {
|
||||
final normalized = _normalizeOfficialUrl(navigationUrl, shareUri: shareUri);
|
||||
if (normalized != null && _looksLikeDirectDownloadUrl(normalized, shareUri: shareUri)) {
|
||||
final normalized = _normalizeOfficialUrl(
|
||||
navigationUrl,
|
||||
shareUri: shareUri,
|
||||
);
|
||||
if (normalized != null &&
|
||||
_looksLikeDirectDownloadUrl(normalized, shareUri: shareUri)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -678,7 +722,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
final path = uri.path.toLowerCase();
|
||||
|
||||
if (sameHost) {
|
||||
if (path.startsWith('/s/') || path == '/home' || path.startsWith('/home/')) {
|
||||
if (path.startsWith('/s/') ||
|
||||
path == '/home' ||
|
||||
path.startsWith('/home/')) {
|
||||
return false;
|
||||
}
|
||||
if (path.contains('/api/v4/file/url')) return false;
|
||||
@@ -706,16 +752,14 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final isRootShareFolder = ShareLinkService.isShareRootUri(
|
||||
uri,
|
||||
shareId: widget.candidate.id,
|
||||
) &&
|
||||
final isRootShareFolder =
|
||||
ShareLinkService.isShareRootUri(uri, shareId: widget.candidate.id) &&
|
||||
_files.isNotEmpty;
|
||||
final urisToSave = isRootShareFolder
|
||||
? _files
|
||||
.map((file) => file.path)
|
||||
.where((path) => path.trim().isNotEmpty)
|
||||
.toList()
|
||||
.map((file) => file.path)
|
||||
.where((path) => path.trim().isNotEmpty)
|
||||
.toList()
|
||||
: <String>[uri];
|
||||
|
||||
await ShareLinkService.instance.saveSharedFiles(
|
||||
@@ -728,14 +772,14 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('已转存「$name」到 $destination')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('已转存「$name」到 $destination')));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('转存失败:$e')),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text('转存失败:$e')));
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
@@ -760,9 +804,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
children: [
|
||||
Text(
|
||||
'选择转存位置',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FolderPicker(
|
||||
@@ -880,9 +924,8 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
isDense: true,
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (_) => _loadShare(
|
||||
password: _passwordController.text.trim(),
|
||||
),
|
||||
onSubmitted: (_) =>
|
||||
_loadShare(password: _passwordController.text.trim()),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
@@ -890,8 +933,8 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
onPressed: _loadingInfo
|
||||
? null
|
||||
: () => _loadShare(
|
||||
password: _passwordController.text.trim(),
|
||||
),
|
||||
password: _passwordController.text.trim(),
|
||||
),
|
||||
child: const Text('解锁'),
|
||||
),
|
||||
],
|
||||
@@ -910,13 +953,18 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
CircleAvatar(
|
||||
radius: 28,
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
child: Icon(Icons.ios_share, color: theme.colorScheme.onPrimaryContainer),
|
||||
child: Icon(
|
||||
Icons.ios_share,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'分享链接',
|
||||
style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -933,11 +981,7 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
return Row(
|
||||
children: [
|
||||
_isSameOrigin
|
||||
? UserAvatar(
|
||||
userId: ownerId,
|
||||
displayName: ownerName,
|
||||
radius: 29,
|
||||
)
|
||||
? UserAvatar(userId: ownerId, displayName: ownerName, radius: 29)
|
||||
: CircleAvatar(
|
||||
radius: 29,
|
||||
backgroundColor: theme.colorScheme.primaryContainer,
|
||||
@@ -966,7 +1010,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'向您分享了 ${info.isFolder ? '一个文件夹' : '一个文件'}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -974,7 +1020,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
const SizedBox(width: 8),
|
||||
_StatusBadge(
|
||||
text: info.expired ? '已过期' : '有效',
|
||||
color: info.expired ? theme.colorScheme.error : theme.colorScheme.primary,
|
||||
color: info.expired
|
||||
? theme.colorScheme.error
|
||||
: theme.colorScheme.primary,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -1009,7 +1057,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
info.name,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w900),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1020,12 +1070,20 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_MetaChip(icon: LucideIcons.eye, text: '${info.visited} 次访问'),
|
||||
if (sizeText != null) _MetaChip(icon: Icons.sd_storage_outlined, text: sizeText),
|
||||
if (sizeText != null)
|
||||
_MetaChip(icon: Icons.sd_storage_outlined, text: sizeText),
|
||||
if (info.createdAt != null)
|
||||
_MetaChip(icon: LucideIcons.calendar, text: '${_formatDate(info.createdAt!)} 创建'),
|
||||
_MetaChip(
|
||||
icon: LucideIcons.calendar,
|
||||
text: '${_formatDate(info.createdAt!)} 创建',
|
||||
),
|
||||
if (info.expires != null)
|
||||
_MetaChip(icon: LucideIcons.clock, text: '${_formatDate(info.expires!)} 过期'),
|
||||
if (info.isPrivate) const _MetaChip(icon: LucideIcons.lock, text: '私密分享'),
|
||||
_MetaChip(
|
||||
icon: LucideIcons.clock,
|
||||
text: '${_formatDate(info.expires!)} 过期',
|
||||
),
|
||||
if (info.isPrivate)
|
||||
const _MetaChip(icon: LucideIcons.lock, text: '私密分享'),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -1082,7 +1140,8 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
}
|
||||
|
||||
Widget _buildSingleFileCard(BuildContext context, ShareLinkInfo info) {
|
||||
final file = _singleFile ?? ShareLinkService.instance.fileFromShareInfo(info);
|
||||
final file =
|
||||
_singleFile ?? ShareLinkService.instance.fileFromShareInfo(info);
|
||||
final sourceUri = (info.sourceUri?.trim().isNotEmpty == true)
|
||||
? info.sourceUri
|
||||
: (file.path.trim().isNotEmpty ? file.path : null);
|
||||
@@ -1108,19 +1167,17 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
info.name,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
),
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w900),
|
||||
),
|
||||
...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${file.size > 0 ? _ShareFileTile.formatSize(file.size) : '分享文件'}${file.updatedAt == null ? '' : ' · ${_formatDate(file.updatedAt!)}'}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${file.size > 0 ? _ShareFileTile.formatSize(file.size) : '分享文件'}${file.updatedAt == null ? '' : ' · ${_formatDate(file.updatedAt!)}'}',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: Theme.of(context).hintColor),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1135,7 +1192,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
_ErrorBox(
|
||||
text: '文件详情读取失败,仍可尝试下载或转存:$_fileError',
|
||||
actionText: sourceUri == null ? null : '重试',
|
||||
onAction: sourceUri == null ? null : () => _loadSingleFile(sourceUri),
|
||||
onAction: sourceUri == null
|
||||
? null
|
||||
: () => _loadSingleFile(sourceUri),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 18),
|
||||
@@ -1182,12 +1241,12 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
onPressed: uri == null || uri.isEmpty || _openingDownload
|
||||
? null
|
||||
: () => _openDownloadUrl(
|
||||
uri,
|
||||
fileName: name,
|
||||
fileSize: fileSize,
|
||||
entity: entity,
|
||||
archive: archive,
|
||||
),
|
||||
uri,
|
||||
fileName: name,
|
||||
fileSize: fileSize,
|
||||
entity: entity,
|
||||
archive: archive,
|
||||
),
|
||||
icon: _openingDownload
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
@@ -1248,19 +1307,23 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
subtitle: '这个分享目录下没有文件。',
|
||||
)
|
||||
else
|
||||
..._files.map((file) => _ShareFileTile(
|
||||
file: file,
|
||||
onTap: file.isFolder ? () => _enterFolder(file) : null,
|
||||
onDownload: file.isFile
|
||||
? () => _openDownloadUrl(
|
||||
file.path,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
entity: file.primaryEntity,
|
||||
)
|
||||
: null,
|
||||
onSave: _isSameOrigin ? () => _saveSharedUri(file.path, name: file.name) : null,
|
||||
)),
|
||||
..._files.map(
|
||||
(file) => _ShareFileTile(
|
||||
file: file,
|
||||
onTap: file.isFolder ? () => _enterFolder(file) : null,
|
||||
onDownload: file.isFile
|
||||
? () => _openDownloadUrl(
|
||||
file.path,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
entity: file.primaryEntity,
|
||||
)
|
||||
: null,
|
||||
onSave: _isSameOrigin
|
||||
? () => _saveSharedUri(file.path, name: file.name)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1271,7 +1334,9 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.55),
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(
|
||||
alpha: 0.55,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
@@ -1287,12 +1352,17 @@ class _ShareLinkPageState extends State<ShareLinkPage> {
|
||||
onTap: isLast ? null : () => _jumpToBreadcrumb(index),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 5,
|
||||
),
|
||||
child: Text(
|
||||
item.title,
|
||||
style: TextStyle(
|
||||
fontWeight: isLast ? FontWeight.w800 : FontWeight.w500,
|
||||
color: isLast ? theme.colorScheme.primary : theme.hintColor,
|
||||
color: isLast
|
||||
? theme.colorScheme.primary
|
||||
: theme.hintColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1320,6 +1390,93 @@ class _ShareBreadcrumb {
|
||||
const _ShareBreadcrumb({required this.title, required this.uri});
|
||||
}
|
||||
|
||||
class _OfficialDownloadDesktopWebView extends StatefulWidget {
|
||||
final Uri shareUri;
|
||||
final ValueChanged<String> onBridgeMessage;
|
||||
final bool Function(String url) onNavigationUrl;
|
||||
final String buildHookScript;
|
||||
final String buildClickScript;
|
||||
|
||||
const _OfficialDownloadDesktopWebView({
|
||||
required this.shareUri,
|
||||
required this.onBridgeMessage,
|
||||
required this.onNavigationUrl,
|
||||
required this.buildHookScript,
|
||||
required this.buildClickScript,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_OfficialDownloadDesktopWebView> createState() =>
|
||||
_OfficialDownloadDesktopWebViewState();
|
||||
}
|
||||
|
||||
class _OfficialDownloadDesktopWebViewState
|
||||
extends State<_OfficialDownloadDesktopWebView> {
|
||||
desktop.InAppWebViewController? _controller;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _installHook() async {
|
||||
final controller = _controller;
|
||||
if (controller == null) return;
|
||||
try {
|
||||
await controller.evaluateJavascript(source: widget.buildHookScript);
|
||||
await controller.evaluateJavascript(source: widget.buildClickScript);
|
||||
} catch (_) {
|
||||
// The parent dialog timeout keeps the manual browser fallback available.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return desktop.InAppWebView(
|
||||
initialUrlRequest: desktop.URLRequest(
|
||||
url: desktop.WebUri.uri(widget.shareUri),
|
||||
),
|
||||
initialSettings: desktop.InAppWebViewSettings(
|
||||
javaScriptEnabled: true,
|
||||
domStorageEnabled: true,
|
||||
supportZoom: false,
|
||||
transparentBackground: true,
|
||||
useShouldOverrideUrlLoading: true,
|
||||
useOnDownloadStart: true,
|
||||
),
|
||||
onWebViewCreated: (controller) {
|
||||
_controller = controller;
|
||||
controller.addJavaScriptHandler(
|
||||
handlerName: 'CloudreveDownloadBridge',
|
||||
callback: (args) {
|
||||
if (args.isNotEmpty) {
|
||||
widget.onBridgeMessage(args.first.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
onLoadStop: (_, url) => _installHook(),
|
||||
shouldOverrideUrlLoading: (_, action) {
|
||||
final url = action.request.url?.toString();
|
||||
if (url != null && widget.onNavigationUrl(url)) {
|
||||
return desktop.NavigationActionPolicy.CANCEL;
|
||||
}
|
||||
return desktop.NavigationActionPolicy.ALLOW;
|
||||
},
|
||||
onUpdateVisitedHistory: (_, url, isReload) {
|
||||
if (url != null) {
|
||||
widget.onNavigationUrl(url.toString());
|
||||
}
|
||||
},
|
||||
onDownloadStarting: (_, request) {
|
||||
widget.onNavigationUrl(request.url.toString());
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareFileTile extends StatelessWidget {
|
||||
final ShareLinkFile file;
|
||||
final VoidCallback? onTap;
|
||||
@@ -1426,7 +1583,9 @@ class _ShareFileTile extends StatelessWidget {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
final text = unitIndex == 0 ? size.toStringAsFixed(0) : size.toStringAsFixed(1);
|
||||
final text = unitIndex == 0
|
||||
? size.toStringAsFixed(0)
|
||||
: size.toStringAsFixed(1);
|
||||
return '$text ${units[unitIndex]}';
|
||||
}
|
||||
|
||||
@@ -1440,10 +1599,7 @@ class _MetaChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
|
||||
const _MetaChip({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
});
|
||||
const _MetaChip({required this.icon, required this.text});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1498,11 +1654,7 @@ class _ErrorBox extends StatelessWidget {
|
||||
final String? actionText;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
const _ErrorBox({
|
||||
required this.text,
|
||||
this.actionText,
|
||||
this.onAction,
|
||||
});
|
||||
const _ErrorBox({required this.text, this.actionText, this.onAction});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -1525,10 +1677,7 @@ class _ErrorBox extends StatelessWidget {
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionText!),
|
||||
),
|
||||
child: TextButton(onPressed: onAction, child: Text(actionText!)),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -1573,16 +1722,11 @@ class _EmptyState extends StatelessWidget {
|
||||
Text(
|
||||
subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
),
|
||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor),
|
||||
),
|
||||
if (actionText != null && onAction != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
OutlinedButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionText!),
|
||||
),
|
||||
OutlinedButton(onPressed: onAction, child: Text(actionText!)),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.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;
|
||||
@@ -26,11 +32,8 @@ class AnnouncementDialog extends StatefulWidget {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (_) => AnnouncementDialog(
|
||||
title: title,
|
||||
html: html,
|
||||
baseUrl: baseUrl,
|
||||
),
|
||||
builder: (_) =>
|
||||
AnnouncementDialog(title: title, html: html, baseUrl: baseUrl),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,35 +42,39 @@ class AnnouncementDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AnnouncementDialogState extends State<AnnouncementDialog> {
|
||||
late final WebViewController _controller;
|
||||
mobile.WebViewController? _mobileController;
|
||||
desktop.InAppWebViewController? _desktopController;
|
||||
late final String _html;
|
||||
late final String _baseUrl;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Colors.transparent)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageFinished: (_) {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
},
|
||||
),
|
||||
);
|
||||
final origin = Uri.parse(widget.baseUrl).origin;
|
||||
_html = _wrapHtml(widget.html);
|
||||
_baseUrl = '$origin/';
|
||||
|
||||
_load();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final origin = Uri.parse(widget.baseUrl).origin;
|
||||
final html = _wrapHtml(widget.html);
|
||||
|
||||
await _controller.loadHtmlString(
|
||||
html,
|
||||
baseUrl: '$origin/',
|
||||
);
|
||||
@override
|
||||
void dispose() {
|
||||
_desktopController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _wrapHtml(String body) {
|
||||
@@ -162,7 +169,7 @@ $body
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
_buildWebView(),
|
||||
if (_loading)
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
],
|
||||
@@ -174,4 +181,29 @@ $body
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return mobile.WebViewWidget(controller: _mobileController!);
|
||||
}
|
||||
}
|
||||
|
||||
+53
-12
@@ -32,21 +32,56 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
# Use Unicode for all projects.
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
|
||||
# flutter_inappwebview_windows requires NuGet; help CMake find it
|
||||
if(NOT DEFINED NUGET OR NUGET STREQUAL "NUGET-NOTFOUND")
|
||||
find_program(NUGET nuget PATHS
|
||||
"$ENV{USERPROFILE}\\.local\\bin"
|
||||
"$ENV{LOCALAPPDATA}\\Microsoft\\WinGet\\Links"
|
||||
"C:/ProgramData/chocolatey/bin"
|
||||
NO_DEFAULT_PATH
|
||||
)
|
||||
find_program(NUGET nuget)
|
||||
if(NUGET AND NOT NUGET STREQUAL "NUGET-NOTFOUND")
|
||||
message(STATUS "Found NuGet: ${NUGET}")
|
||||
set(NUGET "${NUGET}" CACHE FILEPATH "nuget executable" FORCE)
|
||||
# flutter_inappwebview_windows requires NuGet. The plugin's own CMake only
|
||||
# calls find_program(NUGET nuget), so seed the cache with an absolute path.
|
||||
if(DEFINED NUGET AND NOT NUGET STREQUAL "" AND NOT NUGET STREQUAL "NUGET-NOTFOUND")
|
||||
if(NOT EXISTS "${NUGET}")
|
||||
unset(NUGET CACHE)
|
||||
unset(NUGET)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED NUGET OR NUGET STREQUAL "" OR NUGET STREQUAL "NUGET-NOTFOUND")
|
||||
set(_NUGET_CANDIDATES
|
||||
"$ENV{SystemRoot}/System32/nuget.exe"
|
||||
"$ENV{windir}/System32/nuget.exe"
|
||||
"C:/Windows/System32/nuget.exe"
|
||||
"$ENV{SystemRoot}/Sysnative/nuget.exe"
|
||||
"$ENV{windir}/Sysnative/nuget.exe"
|
||||
"C:/Windows/Sysnative/nuget.exe"
|
||||
"$ENV{USERPROFILE}/.local/bin/nuget.exe"
|
||||
"$ENV{LOCALAPPDATA}/Microsoft/WinGet/Links/nuget.exe"
|
||||
"C:/ProgramData/chocolatey/bin/nuget.exe"
|
||||
)
|
||||
foreach(_NUGET_CANDIDATE IN LISTS _NUGET_CANDIDATES)
|
||||
if(EXISTS "${_NUGET_CANDIDATE}")
|
||||
set(NUGET "${_NUGET_CANDIDATE}")
|
||||
break()
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
if(NOT DEFINED NUGET OR NUGET STREQUAL "" OR NUGET STREQUAL "NUGET-NOTFOUND")
|
||||
find_program(NUGET NAMES nuget.exe nuget)
|
||||
endif()
|
||||
|
||||
if(NUGET AND NOT NUGET STREQUAL "NUGET-NOTFOUND")
|
||||
set(_PROJECT_NUGET "${CMAKE_BINARY_DIR}/nuget.exe")
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${NUGET}" "${_PROJECT_NUGET}"
|
||||
RESULT_VARIABLE _NUGET_COPY_RESULT
|
||||
)
|
||||
if(_NUGET_COPY_RESULT EQUAL 0 AND EXISTS "${_PROJECT_NUGET}")
|
||||
set(NUGET "${_PROJECT_NUGET}")
|
||||
else()
|
||||
message(WARNING "Failed to copy NuGet to ${_PROJECT_NUGET}; using ${NUGET}")
|
||||
endif()
|
||||
message(STATUS "Found NuGet: ${NUGET}")
|
||||
set(NUGET "${NUGET}" CACHE FILEPATH "nuget executable" FORCE)
|
||||
else()
|
||||
message(WARNING "NuGet executable was not found. Windows WebView builds may fail.")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
@@ -125,6 +160,12 @@ ENSURE_WINDOWS_ARCHIVE(
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
if(POLICY CMP0175)
|
||||
# flutter_inappwebview_windows 0.7.0-beta.3 passes DEPENDS to the TARGET
|
||||
# form of add_custom_command. Keep older CMake behavior for plugins that do
|
||||
# not set this policy themselves.
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0175 OLD)
|
||||
endif()
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
if(MSVC)
|
||||
|
||||
Reference in New Issue
Block a user