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
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());
|
|
}
|
|
}
|