import 'dart:async'; import 'dart:convert'; import 'package:desktop_webview_window/desktop_webview_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/network/app_dio.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; import 'dart:io' as io; export 'package:flutter_inappwebview/flutter_inappwebview.dart' show WebUri, URLRequest; extension WebviewExtension on InAppWebViewController { Future?> getCookies(String url) async { if (url.contains("https://")) { url.replaceAll("https://", ""); } if (url[url.length - 1] == '/') { url = url.substring(0, url.length - 1); } CookieManager cookieManager = CookieManager.instance(); final cookies = await cookieManager.getCookies(url: WebUri(url)); var res = []; for (var cookie in cookies) { var c = io.Cookie(cookie.name, cookie.value); c.domain = cookie.domain; res.add(c); } return res; } Future getUA() async { var res = await evaluateJavascript(source: "navigator.userAgent"); if (res is String) { if (res[0] == "'" || res[0] == "\"") { res = res.substring(1, res.length - 1); } } return res is String ? res : null; } } class AppWebview extends StatefulWidget { const AppWebview( {required this.initialUrl, this.onTitleChange, this.onNavigation, this.singlePage = false, this.onStarted, this.onLoadStop, super.key}); final String initialUrl; final void Function(String title, InAppWebViewController controller)? onTitleChange; final bool Function(String url, InAppWebViewController controller)? onNavigation; final void Function(InAppWebViewController controller)? onStarted; final void Function(InAppWebViewController controller)? onLoadStop; final bool singlePage; static WebViewEnvironment? webViewEnvironment; @override State createState() => _AppWebviewState(); } class _AppWebviewState extends State { InAppWebViewController? controller; String title = "Webview"; double _progress = 0; late var future = _createWebviewEnvironment(); Future _createWebviewEnvironment() async { var proxy = appdata.settings['proxy'].toString(); if (proxy != "system" && proxy != "direct") { var proxyAvailable = await WebViewFeature.isFeatureSupported( WebViewFeature.PROXY_OVERRIDE); if (proxyAvailable) { ProxyController proxyController = ProxyController.instance(); await proxyController.clearProxyOverride(); if (!proxy.contains("://")) { proxy = "http://$proxy"; } await proxyController.setProxyOverride( settings: ProxySettings( proxyRules: [ProxyRule(url: proxy)], ), ); } } return WebViewEnvironment.create( settings: WebViewEnvironmentSettings( userDataFolder: "${App.dataPath}\\webview", ), ); } @override Widget build(BuildContext context) { final actions = [ Tooltip( message: "More", child: IconButton( icon: const Icon(Icons.more_horiz), onPressed: () { showMenuX( context, Offset(context.width, context.padding.top), [ MenuEntry( icon: Icons.open_in_browser, text: "Open in browser".tl, onClick: () async => launchUrlString((await controller?.getUrl())!.toString()), ), MenuEntry( icon: Icons.copy, text: "Copy link".tl, onClick: () async => Clipboard.setData(ClipboardData( text: (await controller?.getUrl())!.toString())), ), MenuEntry( icon: Icons.refresh, text: "Reload".tl, onClick: () => controller?.reload(), ), ], ); }, ), ) ]; Widget body = (App.isWindows && AppWebview.webViewEnvironment == null) ? FutureBuilder( future: future, builder: (context, e) { if (e.error != null) { return Center(child: Text("Error: ${e.error}")); } if (e.data == null) { return const Center(child: CircularProgressIndicator()); } AppWebview.webViewEnvironment = e.data; return createWebviewWithEnvironment( AppWebview.webViewEnvironment); }, ) : createWebviewWithEnvironment(AppWebview.webViewEnvironment); body = Stack( children: [ Positioned.fill(child: body), if (_progress < 1.0) const Positioned.fill( child: Center(child: CircularProgressIndicator())) ], ); return Scaffold( appBar: Appbar( title: Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, ), actions: actions, ), body: body); } Widget createWebviewWithEnvironment(WebViewEnvironment? e) { return InAppWebView( webViewEnvironment: e, initialSettings: InAppWebViewSettings( isInspectable: true, ), initialUrlRequest: URLRequest(url: WebUri(widget.initialUrl)), onTitleChanged: (c, t) { if (mounted) { setState(() { title = t ?? "Webview"; }); } widget.onTitleChange?.call(title, controller!); }, shouldOverrideUrlLoading: (c, r) async { var res = widget.onNavigation?.call(r.request.url?.toString() ?? "", c) ?? false; if (res) { return NavigationActionPolicy.CANCEL; } else { return NavigationActionPolicy.ALLOW; } }, onWebViewCreated: (c) { controller = c; widget.onStarted?.call(c); }, onLoadStop: (c, r) { widget.onLoadStop?.call(c); }, onProgressChanged: (c, p) { if (mounted) { setState(() { _progress = p / 100; }); } }, ); } } class DesktopWebview { static Future isAvailable() => WebviewWindow.isWebviewAvailable(); final String initialUrl; final void Function(String title, DesktopWebview controller)? onTitleChange; final void Function(String url, DesktopWebview webview)? onNavigation; final void Function(DesktopWebview controller)? onStarted; final void Function()? onClose; DesktopWebview( {required this.initialUrl, this.onTitleChange, this.onNavigation, this.onStarted, this.onClose}); Webview? _webview; String? _ua; String? title; void onMessage(String message) { var json = jsonDecode(message); if (json is Map) { if (json["id"] == "document_created") { title = json["data"]["title"]; _ua = json["data"]["ua"]; onTitleChange?.call(title!, this); } } } String? get userAgent => _ua; Timer? timer; void _runTimer() { timer ??= Timer.periodic(const Duration(seconds: 2), (t) async { const js = ''' function collect() { if(document.readyState === 'loading') { return ''; } let data = { id: "document_created", data: { title: document.title, url: location.href, ua: navigator.userAgent } }; return data; } collect(); '''; if (_webview != null) { onMessage(await evaluateJavascript(js) ?? ''); } }); } void open() async { _webview = await WebviewWindow.create( configuration: CreateConfiguration( useWindowPositionAndSize: true, userDataFolderWindows: "${App.dataPath}\\webview", title: "webview", proxy: AppDio.proxy, )); _webview!.addOnWebMessageReceivedCallback(onMessage); _webview!.setOnNavigation((s) { s = s.substring(1, s.length - 1); return onNavigation?.call(s, this); }); _webview!.launch(initialUrl, triggerOnUrlRequestEvent: false); _runTimer(); _webview!.onClose.then((value) { _webview = null; timer?.cancel(); timer = null; onClose?.call(); }); Future.delayed(const Duration(milliseconds: 200), () { onStarted?.call(this); }); } Future evaluateJavascript(String source) { return _webview!.evaluateJavaScript(source); } Future> getCookies(String url) async { var allCookies = await _webview!.getAllCookies(); var res = {}; for (var c in allCookies) { if (_cookieMatch(url, c.domain)) { res[_removeCode0(c.name)] = _removeCode0(c.value); } } return res; } String _removeCode0(String s) { var codeUints = List.from(s.codeUnits); codeUints.removeWhere((e) => e == 0); return String.fromCharCodes(codeUints); } bool _cookieMatch(String url, String domain) { domain = _removeCode0(domain); var host = Uri.parse(url).host; var acceptedHost = _getAcceptedDomains(host); return acceptedHost.contains(domain.removeAllBlank); } List _getAcceptedDomains(String host) { var acceptedDomains = [host]; var hostParts = host.split("."); for (var i = 0; i < hostParts.length - 1; i++) { acceptedDomains.add(".${hostParts.sublist(i).join(".")}"); } return acceptedDomains; } void close() { _webview?.close(); _webview = null; } }