You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
10 KiB
Dart

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<PageInfo?>(null);
var parseInfo = Rx<MWParseInfo?>(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<void> initJsBridge() async {
if (jsBridge.isEmpty || kDebugMode) {
jsBridge = await rootBundle.loadString("assets/js/app_bridge.js");
}
}
Future<SimpleTemplate> 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<String> 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('<link rel="stylesheet" href="$uri" />');
}
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('<link rel="stylesheet" href="$uri" />');
}
// JS Bridge
tagList.add('<script type="text/javascript">$jsBridge</script>');
return tagList.join();
}
String getBodyClass() {
List<String> classList = [];
var uc = Get.find<UserController>();
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<CustomSchemeResponse?> 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<StatefulWidget> createState() {
return _WikiParserState();
}
}
class _WikiParserState extends ReactiveState<WikiPageParser> {
var c = WikiPageParserController();
@override
void initState() {
super.initState();
c = Get.put(c);
}
@override
void dispose() {
super.dispose();
c.loading.value = true;
}
@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: <Widget>[
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<AppSettingsController>();
return Obx(() => sc.betaPageRender.value ? _buildRender() : _buildWebview());
}
}