From 8ee569bcef25d01ca9abd9b263abfcb055a3c387 Mon Sep 17 00:00:00 2001 From: Lex Lim Date: Thu, 19 Jan 2023 17:18:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BA=95=E6=A0=8F=E6=8F=92?= =?UTF-8?q?=E6=A7=BD=E5=92=8C=E6=B2=89=E6=B5=B8=E5=BC=8F=E5=BA=95=E6=A0=8F?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../isekai/wiki/isekai_wiki/MainActivity.kt | 9 ++ lib/api/mw/mw_api.dart | 2 +- lib/api/mw/parse.dart | 54 ++++++- lib/api/response/parse.dart | 4 +- lib/app.dart | 19 +-- lib/components/isekai_page_scaffold.dart | 129 ++++++++------- lib/components/isekai_text.dart | 10 +- lib/components/page_card.dart | 22 ++- lib/components/wikipage_parser.dart | 151 +++++++++++++----- lib/main.dart | 19 ++- lib/models/site_config.dart | 2 + lib/pages/about.dart | 12 +- lib/pages/article.dart | 113 +++++++++---- lib/pages/own_profile.dart | 6 +- lib/pages/search.dart | 3 +- lib/pages/tab_page.dart | 6 +- lib/styles.dart | 64 ++++---- lib/utils/simpleTemplate.dart | 19 +++ 18 files changed, 434 insertions(+), 210 deletions(-) create mode 100644 lib/utils/simpleTemplate.dart diff --git a/android/app/src/main/kotlin/cn/isekai/wiki/isekai_wiki/MainActivity.kt b/android/app/src/main/kotlin/cn/isekai/wiki/isekai_wiki/MainActivity.kt index 290d715..15d6066 100755 --- a/android/app/src/main/kotlin/cn/isekai/wiki/isekai_wiki/MainActivity.kt +++ b/android/app/src/main/kotlin/cn/isekai/wiki/isekai_wiki/MainActivity.kt @@ -1,6 +1,15 @@ package cn.isekai.wiki.isekai_wiki +import android.os.Build +import android.os.Bundle import io.flutter.embedding.android.FlutterActivity + class MainActivity: FlutterActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) + } + } } diff --git a/lib/api/mw/mw_api.dart b/lib/api/mw/mw_api.dart index c9df8fc..e4271d5 100755 --- a/lib/api/mw/mw_api.dart +++ b/lib/api/mw/mw_api.dart @@ -14,7 +14,7 @@ class MWApiErrorException implements Exception { String? info; - @JsonKey(name: "*") + @JsonKey(name: "docref") String? detail; MWApiErrorException({required this.code, this.info, this.detail}); diff --git a/lib/api/mw/parse.dart b/lib/api/mw/parse.dart index ac7340a..567ef5c 100644 --- a/lib/api/mw/parse.dart +++ b/lib/api/mw/parse.dart @@ -1 +1,53 @@ -class MWApiParse {} +import 'package:isekai_wiki/api/mw/mw_api.dart'; +import 'package:isekai_wiki/api/response/parse.dart'; +import 'package:isekai_wiki/global.dart'; + +class MWApiParse { + static Future> parse({ + String? title, + int? pageId, + List? prop, + String? useSkin, + String? section, + bool disableEditSection = true, + bool disableTOC = true, + bool mobileMode = true, + }) async { + prop ??= [ + "text", + "langlinks", + "categories", + "links", + "templates", + "images", + "externallinks", + "sections", + "revid", + "displaytitle", + "iwlinks", + "properties", + "parsewarnings", + "modules", + "jsconfigvars", + ]; + + useSkin ??= Global.siteConfig.renderTheme; + + Map query = { + "prop": prop.join("|"), + "useskin": useSkin, + }; + + if (title != null) query["page"] = title; + if (pageId != null) query["pageid"] = pageId; + if (disableEditSection) query["disableeditsection"] = 1; + if (section != null) query["section"] = section; + if (disableTOC) query["disabletoc"] = 1; + if (mobileMode) query["mobilenode"] = 1; + + var mwRes = await MWApi.get("parse", params: query); + var parseInfo = MWParseInfo.fromJson(mwRes.data); + + return mwRes.replaceData(parseInfo); + } +} diff --git a/lib/api/response/parse.dart b/lib/api/response/parse.dart index 45ce588..15df1ef 100644 --- a/lib/api/response/parse.dart +++ b/lib/api/response/parse.dart @@ -44,7 +44,7 @@ class MWParsePageLinkInfo with _$MWParsePageLinkInfo { class MWParseSectionInfo with _$MWParseSectionInfo { factory MWParseSectionInfo({ required int toclevel, - required int level, + required String level, required String line, @Default("") String number, @Default("") String index, @@ -76,8 +76,8 @@ class MWParseInfo with _$MWParseInfo { @Default([]) List modules, @Default([]) List modulescripts, @Default([]) List modulestyles, + @Default([]) List iwlinks, @Default({}) Map jsconfigvars, - @Default({}) Map iwlinks, @Default({}) Map properties, }) = _MWParseInfo; diff --git a/lib/app.dart b/lib/app.dart index 07a2b4a..402f916 100755 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,7 +4,6 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:isekai_wiki/global.dart'; import 'package:isekai_wiki/models/settings.dart'; -import 'package:isekai_wiki/models/site_config.dart'; import 'package:isekai_wiki/models/user.dart'; import 'package:isekai_wiki/pages/welcome_page.dart'; import 'models/model.dart'; @@ -31,7 +30,7 @@ class IsekaiWikiApp extends StatelessWidget { return Material( child: GetCupertinoApp( title: '异世界百科', - theme: Styles.cupertinoLightTheme, + theme: Styles.cupertinoTheme, localizationsDelegates: const >[ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, @@ -49,15 +48,13 @@ class IsekaiWikiApp extends StatelessWidget { } else { Styles.textScaleFactor = MediaQuery.of(context).textScaleFactor; Styles.isXs = MediaQuery.of(context).size.width <= 340; - return CupertinoTheme( - data: MediaQuery.of(context).platformBrightness != Brightness.dark - ? Styles.cupertinoLightTheme - : Styles.cupertinoDarkTheme, - child: Theme( - data: MediaQuery.of(context).platformBrightness != Brightness.dark - ? Styles.materialLightTheme - : Styles.materialDarkTheme, - child: child), + var brightness = MediaQuery.of(context).platformBrightness; + return Theme( + data: brightness != Brightness.dark + ? Styles.materialLightTheme + : Styles.materialDarkTheme, + child: CupertinoTheme( + data: Styles.cupertinoTheme.copyWith(brightness: brightness), child: child), ); } }, diff --git a/lib/components/isekai_page_scaffold.dart b/lib/components/isekai_page_scaffold.dart index da3275f..111c9ef 100755 --- a/lib/components/isekai_page_scaffold.dart +++ b/lib/components/isekai_page_scaffold.dart @@ -33,8 +33,9 @@ class IsekaiPageScaffold extends StatefulWidget { const IsekaiPageScaffold({ super.key, this.navigationBar, + this.bottomNavigationBar, this.backgroundColor, - this.resizeToAvoidBottomInset = true, + this.resizeToAvoidBottomInset = false, required this.child, }) : assert(child != null), assert(resizeToAvoidBottomInset != null); @@ -47,17 +48,19 @@ class IsekaiPageScaffold extends StatefulWidget { /// /// The scaffold assumes the navigation bar will account for the [MediaQuery] /// top padding, also consume it if the navigation bar is opaque. - /// - /// By default `navigationBar` has its text scale factor set to 1.0 and does - /// not respond to text scale factor changes from the operating system, to match - /// the native iOS behavior. To override such behavior, wrap each of the `navigationBar`'s - /// components inside a [MediaQuery] with the desired [MediaQueryData.textScaleFactor] - /// value. The text scale factor value from the operating system can be retrieved - /// in many ways, such as querying [MediaQuery.textScaleFactorOf] against - /// [CupertinoApp]'s [BuildContext]. // TODO(xster): document its page transition animation when ready final ObstructingPreferredSizeWidget? navigationBar; + /// The [bottomNavigationBar], typically a [CupertinoNavigationBar], is drawn at the + /// bottom of the screen. + /// + /// If translucent, the main content may slide behind it. + /// Otherwise, the main content's bottom margin will be offset by its height. + /// + /// The scaffold assumes the navigation bar will account for the [MediaQuery] + /// bottom padding, also consume it if the navigation bar is opaque. + final ObstructingPreferredSizeWidget? bottomNavigationBar; + /// Widget to show in the main content area. /// /// Content can slide under the [navigationBar] when they're translucent. @@ -103,62 +106,63 @@ class _IsekaiPageScaffoldState extends State { Widget paddedContent = widget.child; final MediaQueryData existingMediaQuery = MediaQuery.of(context); + + final double topPadding; + final bool topFullObstruction; if (widget.navigationBar != null) { - // TODO(xster): Use real size after partial layout instead of preferred size. - // https://github.com/flutter/flutter/issues/12912 - final double topPadding = widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top; + // Propagate top padding and include viewInsets if appropriate + topPadding = widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top; + + topFullObstruction = widget.navigationBar!.shouldFullyObstruct(context); + } else { + // Propagate bottom padding and include viewInsets if appropriate + topPadding = existingMediaQuery.padding.top; + + topFullObstruction = false; + } + final double bottomPadding; + final bool bottomFullObstruction; + if (widget.bottomNavigationBar != null) { // Propagate bottom padding and include viewInsets if appropriate - final double bottomPadding = widget.resizeToAvoidBottomInset ? existingMediaQuery.viewInsets.bottom : 0.0; - - final EdgeInsets newViewInsets = widget.resizeToAvoidBottomInset - // The insets are consumed by the scaffolds and no longer exposed to - // the descendant subtree. - ? existingMediaQuery.viewInsets.copyWith(bottom: 0.0) - : existingMediaQuery.viewInsets; - - final bool fullObstruction = widget.navigationBar!.shouldFullyObstruct(context); - - // If navigation bar is opaquely obstructing, directly shift the main content - // down. If translucent, let main content draw behind navigation bar but hint the - // obstructed area. - if (fullObstruction) { - paddedContent = MediaQuery( - data: existingMediaQuery - // If the navigation bar is opaque, the top media query padding is fully consumed by the navigation bar. - .removePadding(removeTop: true) - .copyWith( - viewInsets: newViewInsets, - ), - child: Padding( - padding: EdgeInsets.only(top: topPadding, bottom: bottomPadding), - child: paddedContent, - ), - ); - } else { - paddedContent = MediaQuery( - data: existingMediaQuery.copyWith( - padding: existingMediaQuery.padding.copyWith( - top: topPadding, - ), - viewInsets: newViewInsets, - ), - child: Padding( - padding: EdgeInsets.only(bottom: bottomPadding), - child: paddedContent, - ), - ); - } + final double bottomBarHeight = widget.bottomNavigationBar?.preferredSize.height ?? 0; + bottomPadding = widget.resizeToAvoidBottomInset + ? existingMediaQuery.padding.bottom + bottomBarHeight + : bottomBarHeight; + + bottomFullObstruction = widget.bottomNavigationBar!.shouldFullyObstruct(context); } else { - // If there is no navigation bar, still may need to add padding in order - // to support resizeToAvoidBottomInset. - final double bottomPadding = widget.resizeToAvoidBottomInset ? existingMediaQuery.viewInsets.bottom : 0.0; - paddedContent = Padding( - padding: EdgeInsets.only(bottom: bottomPadding), - child: paddedContent, - ); + // Propagate bottom padding and include viewInsets if appropriate + bottomPadding = existingMediaQuery.padding.bottom; + + bottomFullObstruction = false; } + final EdgeInsets newViewInsets = existingMediaQuery.viewInsets; + + var fixedExistingMediaQuery = existingMediaQuery.removePadding( + removeTop: widget.navigationBar != null && topFullObstruction, + removeBottom: widget.bottomNavigationBar != null && bottomFullObstruction, + ); + paddedContent = MediaQuery( + data: fixedExistingMediaQuery.copyWith( + padding: fixedExistingMediaQuery.padding.copyWith( + top: topFullObstruction ? null : topPadding, + bottom: bottomFullObstruction ? null : bottomPadding, + ), + viewInsets: newViewInsets, + ), + child: Padding( + padding: EdgeInsets.only( + top: (widget.navigationBar == null) ? 0 : (topFullObstruction ? topPadding : 0), + bottom: (widget.bottomNavigationBar == null) + ? 0 + : (bottomFullObstruction ? bottomPadding : 0), + ), + child: paddedContent, + ), + ); + return DecoratedBox( decoration: BoxDecoration( color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? @@ -175,6 +179,13 @@ class _IsekaiPageScaffoldState extends State { right: 0.0, child: widget.navigationBar!, ), + if (widget.bottomNavigationBar != null) + Positioned( + bottom: 0.0, + left: 0.0, + right: 0.0, + child: widget.bottomNavigationBar!, + ), // Add a touch handler the size of the status bar on top of all contents // to handle scroll to top by status bar taps. Positioned( diff --git a/lib/components/isekai_text.dart b/lib/components/isekai_text.dart index 710d523..faa46e9 100644 --- a/lib/components/isekai_text.dart +++ b/lib/components/isekai_text.dart @@ -123,13 +123,15 @@ class IsekaiText extends StatelessWidget { @override Widget build(BuildContext context) { - var strutStyleFixed = strutStyle; - if (style?.fontSize != null) {} + var styleFixed = style ?? const TextStyle(); + if (styleFixed.color == null) { + styleFixed = styleFixed.copyWith(color: CupertinoTheme.of(context).textTheme.textStyle.color); + } return Text( data, - style: style, - strutStyle: strutStyleFixed, + style: styleFixed, + strutStyle: strutStyle, textAlign: textAlign, textDirection: textDirection, locale: locale, diff --git a/lib/components/page_card.dart b/lib/components/page_card.dart index ff1bc74..53097d5 100755 --- a/lib/components/page_card.dart +++ b/lib/components/page_card.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:isekai_wiki/api/response/page_info.dart'; import 'package:isekai_wiki/components/flutter_scale_tap/flutter_scale_tap.dart'; +import 'package:isekai_wiki/components/isekai_text.dart'; import 'package:isekai_wiki/components/utils.dart'; import 'package:isekai_wiki/pages/article.dart'; import 'package:like_button/like_button.dart'; @@ -27,7 +28,6 @@ class PageCardStyles { static const contentFontSize = 14.0; static const TextStyle titleTextStyle = TextStyle( - color: CupertinoDynamicColor.withBrightness(color: Colors.black, darkColor: Colors.white), fontSize: titleFontSize, fontStyle: FontStyle.normal, fontWeight: FontWeight.w700, @@ -156,7 +156,7 @@ class PageCard extends StatelessWidget { style: SkeletonLineStyle( height: (PageCardStyles.titleFontSize * 1.1) * textScale, randomLength: true), ), - child: Text( + child: IsekaiText( pageInfo?.mainTitle ?? "页面信息丢失", style: PageCardStyles.titleTextStyle, textScaleFactor: 1, @@ -271,12 +271,12 @@ class PageCard extends StatelessWidget { height: PageCardStyles.footerButtonSize, width: PageCardStyles.footerButtonSize, child: PullDownButton( - routeTheme: PullDownMenuRouteTheme( + routeTheme: const PullDownMenuRouteTheme( endShadow: BoxShadow( - color: Colors.grey.withOpacity(0.6), + color: Colors.black26, spreadRadius: 1, blurRadius: 20, - offset: const Offset(0, 2), + offset: Offset(0, 2), ), ), itemBuilder: _buildMenuItem, @@ -337,20 +337,18 @@ class PageCard extends StatelessWidget { } Widget _buildCard(BuildContext context) { - var textScale = MediaQuery.of(context).textScaleFactor; - return Container( margin: Theme.of(context).cardTheme.margin, clipBehavior: Theme.of(context).cardTheme.clipBehavior ?? Clip.antiAlias, decoration: BoxDecoration( color: Theme.of(context).cardTheme.color, - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Theme.of(context).cardTheme.shadowColor ?? Colors.transparent, - blurRadius: 16, - offset: const Offset(0, 2), - blurStyle: BlurStyle.outer), + color: Theme.of(context).cardTheme.shadowColor ?? Colors.transparent, + blurRadius: 12, + offset: const Offset(0, 2), + ), ], ), child: SizedBox( diff --git a/lib/components/wikipage_parser.dart b/lib/components/wikipage_parser.dart index 1c4850d..a2b5179 100644 --- a/lib/components/wikipage_parser.dart +++ b/lib/components/wikipage_parser.dart @@ -1,15 +1,25 @@ import 'package:flutter/cupertino.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/styles.dart'; +import 'package:isekai_wiki/utils/simpleTemplate.dart'; class WikiPageParserController extends GetxController { + static SimpleTemplate? renderTemplate; + InAppWebViewController? webviewCotroller; + var pageInfo = Rx(null); + var parseInfo = Rx(null); var contentHtml = "".obs; var safeAreaPadding = const EdgeInsets.all(0).obs; var textZoom = 100.obs; @@ -24,10 +34,7 @@ class WikiPageParserController extends GetxController { loading.value = true; if (contentHtml.value.isNotEmpty) { - webviewCotroller?.loadData( - data: contentHtml.value, - baseUrl: Uri.parse(Global.siteConfig.baseUrl), - ); + reloadHtml(); } }); @@ -40,29 +47,100 @@ class WikiPageParserController extends GetxController { }); } - void onWebViewCreated(InAppWebViewController controller) { - webviewCotroller = controller; + 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 DefaultAssetBundle.of(Get.context!).loadString("assets/tpl/wikiPage.html"); + } + renderTemplate = SimpleTemplate(tpl: tplData); + } + return renderTemplate!; + } + + String getScriptTag() { + List tagList = []; + var resUri = Uri.parse(Global.siteConfig.resourceLoaderUrl); + + 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 uri = resUri.replace(queryParameters: { + "lang": pageInfo.value?.pagelanguage ?? "en", + "modules": parseInfo.value!.modulestyles.join("|"), + "only": "styles", + "skin": Global.siteConfig.renderTheme, + }); + 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}"); + + return classList.join(" "); + } + + void reloadHtml() async { + 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: contentHtml.value, - baseUrl: Uri.parse(Global.siteConfig.baseUrl), + data: parsedHtml, + baseUrl: Uri.parse(pageInfo.value?.fullurl ?? Global.siteConfig.baseUrl), ); } + void onWebViewCreated(InAppWebViewController controller) { + webviewCotroller = controller; + + if (contentHtml.value.isNotEmpty) { + reloadHtml(); + } + } + void onPageCommitVisible(InAppWebViewController controller, Uri? uri) { controller.injectCSSCode(source: """ -body { - padding-top: ${safeAreaPadding.value.top}px; - padding-bottom: ${safeAreaPadding.value.bottom}px; - padding-left: ${safeAreaPadding.value.left}px; - padding-right: ${safeAreaPadding.value.right}px; +:root { + --safe-area-top: ${safeAreaPadding.value.top}px; + --safe-area-bottom: ${safeAreaPadding.value.bottom}px; + --safe-area-left: ${safeAreaPadding.value.left}px; + --safe-area-right: ${safeAreaPadding.value.right}px; } -"""); - controller.evaluateJavascript(source: """ -var metaEl = document.createElement("meta"); -metaEl.name = "viewport"; -metaEl.content = "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0"; -document.head.appendChild(metaEl); """); if (contentHtml.value.isNotEmpty) { @@ -74,10 +152,11 @@ document.head.appendChild(metaEl); } class WikiPageParser extends StatefulWidget { - final String? contentHtml; + final PageInfo? pageInfo; + final MWParseInfo? parseInfo; final EdgeInsets? padding; - const WikiPageParser({super.key, this.contentHtml, this.padding}); + const WikiPageParser({super.key, this.pageInfo, this.parseInfo, this.padding}); @override State createState() { @@ -96,7 +175,9 @@ class _WikiParserState extends ReactiveState { @override void receiveProps() { - c.contentHtml.value = widget.contentHtml ?? ""; + 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(); } @@ -105,20 +186,16 @@ class _WikiParserState extends ReactiveState { return ListView( children: [ Container( - color: Styles.panelBackgroundColor, - child: SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Html( - data: c.contentHtml.value, - ), - ], - ), + 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, + ), + ], ), ), ), diff --git a/lib/main.dart b/lib/main.dart index 2379a20..119abcd 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,10 +14,23 @@ import 'app.dart'; Future init() async { // 仅允许竖屏 - /*SystemChrome.setPreferredOrientations( - [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);*/ + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle(statusBarColor: Colors.transparent)); + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + systemStatusBarContrastEnforced: false, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: false, + systemNavigationBarIconBrightness: Brightness.dark, + ), + ); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.edgeToEdge, + overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom], + ); if (kIsWeb) { // 设置web origin diff --git a/lib/models/site_config.dart b/lib/models/site_config.dart index 892a74c..28ee4e4 100644 --- a/lib/models/site_config.dart +++ b/lib/models/site_config.dart @@ -15,6 +15,7 @@ class SiteConfig { List moduleStyles; List moduleScripts; String renderTheme; + String renderTemplateUrl; String baseUrl; String indexUrl; @@ -29,6 +30,7 @@ class SiteConfig { this.moduleStyles = const [], this.moduleScripts = const [], this.renderTheme = Global.renderThemeFallback, + this.renderTemplateUrl = "", this.baseUrl = "", this.indexUrl = "", this.apiUrl = "", diff --git a/lib/pages/about.dart b/lib/pages/about.dart index b774ca5..4271c8b 100755 --- a/lib/pages/about.dart +++ b/lib/pages/about.dart @@ -1,6 +1,8 @@ import 'package:cupertino_lists/cupertino_lists.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:isekai_wiki/components/isekai_text.dart'; import 'package:isekai_wiki/utils/dialog.dart'; import '../components/dummy_icon.dart'; @@ -30,7 +32,7 @@ class AboutPage extends StatelessWidget { children: [ const SizedBox(height: 18), Container( - color: Styles.panelBackgroundColor, + color: Theme.of(context).cardTheme.color, child: SafeArea( top: false, bottom: false, @@ -38,9 +40,9 @@ class AboutPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), child: Column( children: const [ - Text("异世界百科APP", style: Styles.articleTitle), + IsekaiText("异世界百科APP", style: Styles.articleTitle), SizedBox(height: 18), - Text("使用Flutter构建"), + IsekaiText("使用Flutter构建"), ], ), ), @@ -48,10 +50,10 @@ class AboutPage extends StatelessWidget { ), const SizedBox(height: 18), CupertinoListSection.insetGrouped( - backgroundColor: Styles.themePageBackgroundColor, + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, children: [ CupertinoListTile.notched( - title: const Text('异世界百科', style: TextStyle(color: Styles.linkColor)), + title: const IsekaiText('异世界百科', style: TextStyle(color: Styles.linkColor)), leading: const DummyIcon( color: CupertinoColors.systemBlue, icon: CupertinoIcons.globe, diff --git a/lib/pages/article.dart b/lib/pages/article.dart index 8671982..8030c6a 100755 --- a/lib/pages/article.dart +++ b/lib/pages/article.dart @@ -1,12 +1,19 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter_html/flutter_html.dart'; -import 'package:isekai_wiki/api/restbase/page.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:isekai_wiki/api/mw/list.dart'; +import 'package:isekai_wiki/api/mw/mw_api.dart'; +import 'package:isekai_wiki/api/mw/parse.dart'; +import 'package:isekai_wiki/api/response/page_info.dart'; +import 'package:isekai_wiki/api/response/parse.dart'; import 'package:isekai_wiki/components/safearea_builder.dart'; import 'package:isekai_wiki/components/wikipage_parser.dart'; +import 'package:isekai_wiki/reactive/reactive.dart'; +import 'package:isekai_wiki/utils/dialog.dart'; +import 'package:isekai_wiki/utils/error.dart'; import '../components/isekai_nav_bar.dart'; import '../components/isekai_page_scaffold.dart'; -import '../styles.dart'; class MinimumArticleData { final String title; @@ -24,56 +31,96 @@ class ArticleCategoryData { ArticleCategoryData({required this.name, required this.identity}); } +class ArticlePageController extends GetxController { + var loading = true.obs; + + var pageTitle = "".obs; + var pageId = 0.obs; + + var pageInfo = Rx(null); + var parseInfo = Rx(null); + + var displayTitle = "".obs; + + Future loadPageContent() async { + if (pageTitle.isNotEmpty || pageId.value != 0) { + try { + // 加载页面信息 + loading.value = true; + MWResponse> pageInfoRes; + if (pageId.value != 0) { + pageInfoRes = await MWApiList.getPageInfoList(pageids: [pageId.value]); + } else { + pageInfoRes = await MWApiList.getPageInfoList(titles: [pageTitle.value]); + } + if (pageInfoRes.data.isEmpty) { + throw MWApiErrorException(code: 'no-page', info: "页面信息丢失"); + } + + pageInfo.value = pageInfoRes.data[0]; + displayTitle.value = pageInfo.value!.mainTitle; + pageId.value = pageInfo.value!.pageid; + pageTitle.value = pageInfo.value!.title; + + // 获取页面HTML + var parseRes = await MWApiParse.parse(pageId: pageId.value); + parseInfo.value = parseRes.data; + } catch (err, stack) { + alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误"); + if (kDebugMode) { + print("Exception in page: $err"); + stack.printError(); + } + } finally { + loading.value = false; + } + } + } +} + class ArticlePage extends StatefulWidget { final MinimumArticleData? initialArticleData; final String? targetPage; + final int? targetPageId; - const ArticlePage({super.key, this.targetPage, this.initialArticleData}); + const ArticlePage({super.key, this.targetPage, this.targetPageId, this.initialArticleData}); @override State createState() => _ArticlePageState(); } -class _ArticlePageState extends State { - bool loading = true; - String title = ""; - String description = ""; - String content = ""; - String contentHtml = ""; - List categories = []; - - Future _loadPageContent() async { - if (widget.targetPage != null) { - var resContentHtml = await RestfulPageApi.getPageHtml(widget.targetPage!); - if (resContentHtml != null) { - setState(() { - contentHtml = resContentHtml; - }); - } - } - } +class _ArticlePageState extends ReactiveState { + var c = ArticlePageController(); @override void initState() { - title = widget.initialArticleData?.title ?? ""; - description = widget.initialArticleData?.description ?? ""; - if (widget.initialArticleData?.mainCategory != null) { - categories.add(widget.initialArticleData!.mainCategory!); - } super.initState(); - _loadPageContent(); + + Get.put(c); + + c.loadPageContent(); + } + + @override + void receiveProps() { + c.pageTitle.value = widget.targetPage ?? ""; + c.pageId.value = widget.targetPageId ?? 0; + c.displayTitle.value = widget.initialArticleData?.title ?? "加载中"; } @override - Widget build(BuildContext context) { + Widget render(BuildContext context) { return IsekaiPageScaffold( navigationBar: IsekaiNavigationBar( - middle: Text(title), + middle: Obx(() => Text(c.displayTitle.value)), ), child: SafeAreaBuilder( - builder: (context, padding) => WikiPageParser( - contentHtml: contentHtml, - padding: padding, + builder: (context, padding) => Obx( + () => WikiPageParser( + padding: padding, + pageInfo: c.pageInfo.value, + parseInfo: c.parseInfo.value, + ), ), ), ); diff --git a/lib/pages/own_profile.dart b/lib/pages/own_profile.dart index 73e81db..334ee16 100755 --- a/lib/pages/own_profile.dart +++ b/lib/pages/own_profile.dart @@ -108,7 +108,7 @@ class OwnProfileTab extends StatelessWidget { return FollowTextScale( child: Obx( () => CupertinoListSection.insetGrouped( - backgroundColor: Styles.themePageBackgroundColor, + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, dividerMargin: uc.isLoggedIn ? 14 : double.infinity, children: [ Obx( @@ -159,7 +159,7 @@ class OwnProfileTab extends StatelessWidget { Widget _buildArticleListsSection(BuildContext context, OwnProfileController c) { return FollowTextScale( child: CupertinoListSection.insetGrouped( - backgroundColor: Styles.themePageBackgroundColor, + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, children: [ CupertinoListTile.notched( title: const Text('收藏'), @@ -195,7 +195,7 @@ class OwnProfileTab extends StatelessWidget { Widget _buildSettingsSection(BuildContext context, OwnProfileController c) { return FollowTextScale( child: CupertinoListSection.insetGrouped( - backgroundColor: Styles.themePageBackgroundColor, + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, children: [ CupertinoListTile.notched( title: const Text('设置'), diff --git a/lib/pages/search.dart b/lib/pages/search.dart index 067ea05..5505a53 100755 --- a/lib/pages/search.dart +++ b/lib/pages/search.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:isekai_wiki/components/isekai_page_scaffold.dart'; import 'package:isekai_wiki/components/state_test.dart'; @@ -23,7 +24,7 @@ class SearchTab extends StatelessWidget { child: ListView( children: [ Container( - color: Styles.panelBackgroundColor, + color: Theme.of(context).cardColor, child: SafeArea( top: false, bottom: false, diff --git a/lib/pages/tab_page.dart b/lib/pages/tab_page.dart index 7a02ac4..46c8cd7 100755 --- a/lib/pages/tab_page.dart +++ b/lib/pages/tab_page.dart @@ -7,7 +7,6 @@ import 'package:isekai_wiki/pages/discover.dart'; import 'package:isekai_wiki/pages/home.dart'; import 'package:isekai_wiki/pages/search.dart'; import 'package:isekai_wiki/pages/own_profile.dart'; -import 'package:isekai_wiki/styles.dart'; class TabsPageController extends GetxController { final tabController = CupertinoTabController(); @@ -57,10 +56,11 @@ class IsekaiWikiTabsPage extends StatelessWidget { return WillPopScope( onWillPop: c.handleWillPop, child: CupertinoTabScaffold( + backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor, controller: c.tabController, + resizeToAvoidBottomInset: false, tabBar: CupertinoTabBar( - backgroundColor: Styles.themeBottomColor, - activeColor: Styles.themeMainColor, + backgroundColor: CupertinoTheme.of(context).barBackgroundColor, border: const Border(top: BorderSide(color: CupertinoColors.systemGrey5, width: 2)), height: 56, onTap: c.handleTapTab, diff --git a/lib/styles.dart b/lib/styles.dart index 693cec8..076f683 100755 --- a/lib/styles.dart +++ b/lib/styles.dart @@ -23,45 +23,27 @@ abstract class Styles { ), ); - static CupertinoThemeData cupertinoLightTheme = const CupertinoThemeData( - textTheme: Styles.defaultTextTheme, + static CupertinoThemeData cupertinoTheme = const CupertinoThemeData( + primaryColor: themeMainColor, + primaryContrastingColor: CupertinoColors.white, scaffoldBackgroundColor: Styles.themePageBackgroundColor, + textTheme: Styles.defaultTextTheme, ); - static CupertinoThemeData cupertinoDarkTheme = const CupertinoThemeData( - brightness: Brightness.dark, - textTheme: Styles.defaultTextTheme, - scaffoldBackgroundColor: Styles.themePageBackgroundColor, + static CardTheme cardTheme = const CardTheme( + color: Color.fromRGBO(255, 255, 255, 1), + shadowColor: Colors.black12, + margin: EdgeInsets.symmetric(horizontal: 6, vertical: 8), ); static ThemeData materialLightTheme = ThemeData.light().copyWith( cardTheme: cardTheme, ); - static ThemeData materialDarkTheme = ThemeData.light().copyWith( - cardTheme: cardTheme.copyWith(color: const Color.fromRGBO(28, 28, 28, 1)), - ); - - static CardTheme cardTheme = CardTheme( - shadowColor: CupertinoColors.black.withOpacity(0.2), - margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), - ); - - static const TextStyle navLargeTitleTextStyle = TextStyle( - fontWeight: FontWeight.normal, fontSize: 32, color: CupertinoColors.label, inherit: false); - - static const TextStyle productRowItemName = TextStyle( - color: Color.fromRGBO(0, 0, 0, 0.8), - fontSize: 18, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.normal, - ); - - static const TextStyle productRowTotal = TextStyle( - color: Color.fromRGBO(0, 0, 0, 0.8), - fontSize: 20, - fontStyle: FontStyle.normal, - fontWeight: FontWeight.bold, + static ThemeData materialDarkTheme = ThemeData.dark().copyWith( + cardTheme: cardTheme.copyWith( + color: const Color.fromRGBO(28, 28, 28, 1), + ), ); static const TextStyle articleTitle = TextStyle( @@ -77,13 +59,25 @@ abstract class Styles { static const Color websiteNavbarColor = Color.fromRGBO(33, 37, 41, 1); - static const Color themeMainColor = Color.fromRGBO(65, 147, 135, 1); + static const Color themeMainColor = Color.fromARGB(255, 71, 172, 156); static const Color themeNavTitleColor = Color.fromRGBO(255, 255, 255, 1); - static const Color themeBottomColor = Color.fromRGBO(255, 255, 255, 1); - static const Color themePageBackgroundColor = Color.fromRGBO(240, 240, 240, 1); - static const Color darkThemePageBackgroundColor = Color.fromRGBO(0, 0, 0, 1); + + static const Color themeBottomColor = CupertinoDynamicColor.withBrightness( + color: Color.fromRGBO(255, 255, 255, 1), + darkColor: Color.fromRGBO(41, 41, 41, 1), + ); + + static const Color themePageBackgroundColor = CupertinoDynamicColor.withBrightness( + color: Color.fromRGBO(240, 240, 240, 1), + darkColor: Color.fromRGBO(0, 0, 0, 1), + ); + + static const Color themePureBackgroundColor = CupertinoDynamicColor.withBrightness( + color: Color.fromRGBO(255, 255, 255, 1), + darkColor: Color.fromRGBO(0, 0, 0, 1), + ); + static const Color themeNavSegmentTextColor = Color.fromRGBO(12, 12, 12, 1); - static const Color panelBackgroundColor = Color.fromRGBO(255, 255, 255, 1); static const Color linkColor = CupertinoColors.link; static const double largeTitleFontSize = 32; static const double navTitleFontSize = 18; diff --git a/lib/utils/simpleTemplate.dart b/lib/utils/simpleTemplate.dart new file mode 100644 index 0000000..3316da1 --- /dev/null +++ b/lib/utils/simpleTemplate.dart @@ -0,0 +1,19 @@ +class SimpleTemplate { + String? tpl; + + SimpleTemplate({this.tpl}); + + String fetch(Map params) { + if (tpl == null) return ""; + + var re = RegExp(r"{{(?[^,;:'\n]+?)}}"); + + return tpl!.replaceAllMapped(re, (match) { + var key = match.group(1)!.trim(); + if (params.containsKey(key)) { + return params[key]!; + } + return match.group(0)!; + }); + } +}