import 'dart:convert'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:get/get.dart'; import 'package:isekai_wiki/api/base_api.dart'; import 'package:isekai_wiki/api/response/page_info.dart'; import 'package:isekai_wiki/api/response/parse.dart'; import 'package:isekai_wiki/global.dart'; import 'package:isekai_wiki/models/settings.dart'; import 'package:isekai_wiki/models/user.dart'; import 'package:isekai_wiki/reactive/reactive.dart'; import 'package:isekai_wiki/utils/simpleTemplate.dart'; import 'package:mime/mime.dart'; class WikiPageParserController extends GetxController { static SimpleTemplate? renderTemplate; static String jsBridge = ""; Function(String anchor)? onSectionChange; InAppWebViewController? webviewCotroller; ScrollController scrollController = ScrollController(); var pageInfo = Rx(null); var parseInfo = Rx(null); var contentHtml = "".obs; var safeAreaPadding = const EdgeInsets.all(0).obs; var textZoom = 100.obs; var loading = true.obs; var pageHeight = 0.obs; @override void onInit() { super.onInit(); ever(contentHtml, (_) { loading.value = true; if (contentHtml.value.isNotEmpty) { reloadHtml(); } }); ever(textZoom, (_) { webviewCotroller?.setOptions( options: InAppWebViewGroupOptions( android: AndroidInAppWebViewOptions(textZoom: textZoom.value), ), ); }); } Future initJsBridge() async { if (jsBridge.isEmpty || kDebugMode) { jsBridge = await rootBundle.loadString("assets/js/app_bridge.js"); } } Future getRenderTemplate() async { if (renderTemplate == null) { String tplData; if (Global.siteConfig.renderTemplateUrl.isNotEmpty) { // 加载远程模板 var uri = Uri.parse(Global.siteConfig.renderTemplateUrl); tplData = await BaseApi.get(uri); } else { tplData = await rootBundle.loadString("assets/tpl/wikiPage.html"); } renderTemplate = SimpleTemplate(tpl: tplData); } return renderTemplate!; } String getScriptTag() { List tagList = []; var resUri = Uri.parse(Global.siteConfig.resourceLoaderUrl); // CSS if (Global.siteConfig.moduleStyles.isNotEmpty) { var uri = resUri.replace(queryParameters: { "lang": pageInfo.value?.pagelanguage ?? "en", "modules": Global.siteConfig.moduleStyles.join("|"), "only": "styles", "skin": Global.siteConfig.renderTheme, }); tagList.add(''); } if (parseInfo.value?.modulestyles != null && parseInfo.value!.modulestyles.isNotEmpty) { var pageModules = parseInfo.value!.modulestyles .where((item) => !Global.siteConfig.moduleStyles.contains(item)); var uri = resUri.replace(queryParameters: { "lang": pageInfo.value?.pagelanguage ?? "en", "modules": pageModules.join("|"), "only": "styles", "skin": Global.siteConfig.renderTheme, }); tagList.add(''); } // JS Bridge tagList.add(''); return tagList.join(); } String getBodyClass() { List classList = []; var uc = Get.find(); if (uc.isLoggedIn) { classList.add("logged-in"); } if (pageInfo.value!.ns != 0) { classList.add("ns-${pageInfo.value!.ns}"); } classList.add("skin-${Global.siteConfig.renderTheme}"); if (GetPlatform.isAndroid) { classList.add("mugenapp-android"); } else if (GetPlatform.isIOS) { classList.add("mugenapp-ios"); } return classList.join(" "); } void reloadHtml() async { await initJsBridge(); var tpl = await getRenderTemplate(); var tplParams = { "lang": pageInfo.value?.pagelanguagehtmlcode ?? "en", "title": pageInfo.value?.mainTitle ?? "页面", "content": contentHtml.value, "scripts": getScriptTag(), "bodyClassName": getBodyClass(), }; var parsedHtml = tpl.fetch(tplParams); webviewCotroller?.loadData( data: parsedHtml, baseUrl: Uri.parse(pageInfo.value?.fullurl ?? Global.siteConfig.baseUrl), ); } void onWebViewCreated(InAppWebViewController controller) { webviewCotroller = controller; controller.addJavaScriptHandler( handlerName: "bridgeConnected", callback: (args) { debugPrint("JSBridge: bridgeConnected"); return {}; }, ); controller.addJavaScriptHandler( handlerName: "pageLoaded", callback: (args) { loading.value = false; debugPrint("JSBridge: pageLoaded"); return {}; }, ); controller.addJavaScriptHandler( handlerName: "sectionChange", callback: (args) { if (args.isEmpty && args[0] is! String) { return; } var anchor = args[0] as String; onSectionChange?.call(anchor); }, ); if (contentHtml.value.isNotEmpty) { reloadHtml(); } } void onPageCommitVisible(InAppWebViewController controller, Uri? uri) { controller.evaluateJavascript(source: """ document.documentElement.style.setProperty('--safe-area-top', '${safeAreaPadding.value.top}px'); document.documentElement.style.setProperty('--safe-area-right', '${safeAreaPadding.value.right}px'); document.documentElement.style.setProperty('--safe-area-bottom', '${safeAreaPadding.value.bottom}px'); document.documentElement.style.setProperty('--safe-area-left', '${safeAreaPadding.value.left}px'); navigator.safeArea = { top: ${safeAreaPadding.value.top}, right: ${safeAreaPadding.value.right}, bottom: ${safeAreaPadding.value.bottom}, left: ${safeAreaPadding.value.left} }; """); if (contentHtml.value.isNotEmpty) { Future.delayed(const Duration(milliseconds: 1000)).then((value) { loading.value = false; }); } } Future onLoadResourceCustomScheme( InAppWebViewController controller, Uri uri) async { if (uri.scheme == "mugenappres") { debugPrint("Webview load assets: \"$uri\""); try { var bytes = await rootBundle.load("assets/${uri.path}"); var data = bytes.buffer.asUint8List(); var mimeType = MimeTypeResolver().lookup(uri.path, headerBytes: data.sublist(0, 20)); CustomSchemeResponse( data: data, contentType: mimeType ?? "text/plain", contentEncoding: "utf-8", ); } catch (_) { debugPrint("Try to load assets \"${uri.path}\" which is not exists."); } } return null; } void onScrollChanged(InAppWebViewController controller, int x, int y) {} void scrollToAnchor(String anchor) { String encodedAnchor = jsonEncode(anchor); webviewCotroller?.evaluateJavascript( source: "window.MugenApp && window.MugenApp.scrollToTitle($encodedAnchor)"); } } class WikiPageParser extends StatefulWidget { final PageInfo? pageInfo; final MWParseInfo? parseInfo; final EdgeInsets? padding; final Function(String anchor)? onSectionChange; final Function(WikiPageParserController controller)? ref; const WikiPageParser({ super.key, this.pageInfo, this.parseInfo, this.padding, this.onSectionChange, this.ref, }); @override State createState() { return _WikiParserState(); } } class _WikiParserState extends ReactiveState { var c = WikiPageParserController(); @override void initState() { super.initState(); c = Get.put(c); } @override void receiveProps() { c.pageInfo.value = widget.pageInfo; c.parseInfo.value = widget.parseInfo; c.contentHtml.value = widget.parseInfo?.text ?? ""; c.safeAreaPadding.value = widget.padding ?? const EdgeInsets.all(0); c.textZoom.value = (MediaQuery.of(Get.context!).textScaleFactor * 100).round(); c.onSectionChange = widget.onSectionChange; widget.ref?.call(c); } Widget _buildRender() { return ListView( children: [ Container( color: Theme.of(context).cardColor, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Html( data: c.contentHtml.value, ), ], ), ), ), ], ); } Widget _buildWebview() { return Obx( () => Stack( children: [ Opacity( opacity: c.loading.value ? 0 : 1, child: InAppWebView( onWebViewCreated: c.onWebViewCreated, onPageCommitVisible: c.onPageCommitVisible, onLoadResourceCustomScheme: c.onLoadResourceCustomScheme, onScrollChanged: c.onScrollChanged, initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( disableHorizontalScroll: true, supportZoom: true, resourceCustomSchemes: ["mugenappres"], ), android: AndroidInAppWebViewOptions( textZoom: c.textZoom.value, forceDark: AndroidForceDark.FORCE_DARK_AUTO, ), ios: IOSInAppWebViewOptions( allowsLinkPreview: false, allowsBackForwardNavigationGestures: false, alwaysBounceVertical: true, ), ), ), ), if (c.loading.value) const Center( child: CupertinoActivityIndicator(radius: 20), ), ], ), ); } @override Widget build(BuildContext context) { var sc = Get.find(); return Obx(() => sc.betaPageRender.value ? _buildRender() : _buildWebview()); } }