增加底栏插槽和沉浸式底栏效果

main
落雨楓 2 years ago
parent 484810d390
commit 8ee569bcef

@ -1,6 +1,15 @@
package cn.isekai.wiki.isekai_wiki package cn.isekai.wiki.isekai_wiki
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() { class MainActivity: FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
}
}
} }

@ -14,7 +14,7 @@ class MWApiErrorException implements Exception {
String? info; String? info;
@JsonKey(name: "*") @JsonKey(name: "docref")
String? detail; String? detail;
MWApiErrorException({required this.code, this.info, this.detail}); MWApiErrorException({required this.code, this.info, this.detail});

@ -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<MWResponse<MWParseInfo>> parse({
String? title,
int? pageId,
List<String>? 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<String, dynamic> 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);
}
}

@ -44,7 +44,7 @@ class MWParsePageLinkInfo with _$MWParsePageLinkInfo {
class MWParseSectionInfo with _$MWParseSectionInfo { class MWParseSectionInfo with _$MWParseSectionInfo {
factory MWParseSectionInfo({ factory MWParseSectionInfo({
required int toclevel, required int toclevel,
required int level, required String level,
required String line, required String line,
@Default("") String number, @Default("") String number,
@Default("") String index, @Default("") String index,
@ -76,8 +76,8 @@ class MWParseInfo with _$MWParseInfo {
@Default([]) List<String> modules, @Default([]) List<String> modules,
@Default([]) List<String> modulescripts, @Default([]) List<String> modulescripts,
@Default([]) List<String> modulestyles, @Default([]) List<String> modulestyles,
@Default([]) List<dynamic> iwlinks,
@Default({}) Map<String, dynamic> jsconfigvars, @Default({}) Map<String, dynamic> jsconfigvars,
@Default({}) Map<String, dynamic> iwlinks,
@Default({}) Map<String, dynamic> properties, @Default({}) Map<String, dynamic> properties,
}) = _MWParseInfo; }) = _MWParseInfo;

@ -4,7 +4,6 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:isekai_wiki/global.dart'; import 'package:isekai_wiki/global.dart';
import 'package:isekai_wiki/models/settings.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/models/user.dart';
import 'package:isekai_wiki/pages/welcome_page.dart'; import 'package:isekai_wiki/pages/welcome_page.dart';
import 'models/model.dart'; import 'models/model.dart';
@ -31,7 +30,7 @@ class IsekaiWikiApp extends StatelessWidget {
return Material( return Material(
child: GetCupertinoApp( child: GetCupertinoApp(
title: '异世界百科', title: '异世界百科',
theme: Styles.cupertinoLightTheme, theme: Styles.cupertinoTheme,
localizationsDelegates: const <LocalizationsDelegate<dynamic>>[ localizationsDelegates: const <LocalizationsDelegate<dynamic>>[
GlobalMaterialLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
@ -49,15 +48,13 @@ class IsekaiWikiApp extends StatelessWidget {
} else { } else {
Styles.textScaleFactor = MediaQuery.of(context).textScaleFactor; Styles.textScaleFactor = MediaQuery.of(context).textScaleFactor;
Styles.isXs = MediaQuery.of(context).size.width <= 340; Styles.isXs = MediaQuery.of(context).size.width <= 340;
return CupertinoTheme( var brightness = MediaQuery.of(context).platformBrightness;
data: MediaQuery.of(context).platformBrightness != Brightness.dark return Theme(
? Styles.cupertinoLightTheme data: brightness != Brightness.dark
: Styles.cupertinoDarkTheme,
child: Theme(
data: MediaQuery.of(context).platformBrightness != Brightness.dark
? Styles.materialLightTheme ? Styles.materialLightTheme
: Styles.materialDarkTheme, : Styles.materialDarkTheme,
child: child), child: CupertinoTheme(
data: Styles.cupertinoTheme.copyWith(brightness: brightness), child: child),
); );
} }
}, },

@ -33,8 +33,9 @@ class IsekaiPageScaffold extends StatefulWidget {
const IsekaiPageScaffold({ const IsekaiPageScaffold({
super.key, super.key,
this.navigationBar, this.navigationBar,
this.bottomNavigationBar,
this.backgroundColor, this.backgroundColor,
this.resizeToAvoidBottomInset = true, this.resizeToAvoidBottomInset = false,
required this.child, required this.child,
}) : assert(child != null), }) : assert(child != null),
assert(resizeToAvoidBottomInset != null); assert(resizeToAvoidBottomInset != null);
@ -47,17 +48,19 @@ class IsekaiPageScaffold extends StatefulWidget {
/// ///
/// The scaffold assumes the navigation bar will account for the [MediaQuery] /// The scaffold assumes the navigation bar will account for the [MediaQuery]
/// top padding, also consume it if the navigation bar is opaque. /// 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 // TODO(xster): document its page transition animation when ready
final ObstructingPreferredSizeWidget? navigationBar; 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. /// Widget to show in the main content area.
/// ///
/// Content can slide under the [navigationBar] when they're translucent. /// Content can slide under the [navigationBar] when they're translucent.
@ -103,61 +106,62 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
Widget paddedContent = widget.child; Widget paddedContent = widget.child;
final MediaQueryData existingMediaQuery = MediaQuery.of(context); final MediaQueryData existingMediaQuery = MediaQuery.of(context);
final double topPadding;
final bool topFullObstruction;
if (widget.navigationBar != null) { if (widget.navigationBar != null) {
// TODO(xster): Use real size after partial layout instead of preferred size. // Propagate top padding and include viewInsets if appropriate
// https://github.com/flutter/flutter/issues/12912 topPadding = widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top;
final double topPadding = widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top;
topFullObstruction = widget.navigationBar!.shouldFullyObstruct(context);
} else {
// Propagate bottom padding and include viewInsets if appropriate // Propagate bottom padding and include viewInsets if appropriate
final double bottomPadding = widget.resizeToAvoidBottomInset ? existingMediaQuery.viewInsets.bottom : 0.0; topPadding = existingMediaQuery.padding.top;
final EdgeInsets newViewInsets = widget.resizeToAvoidBottomInset topFullObstruction = false;
// 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); final double bottomPadding;
final bool bottomFullObstruction;
if (widget.bottomNavigationBar != null) {
// Propagate bottom padding and include viewInsets if appropriate
final double bottomBarHeight = widget.bottomNavigationBar?.preferredSize.height ?? 0;
bottomPadding = widget.resizeToAvoidBottomInset
? existingMediaQuery.padding.bottom + bottomBarHeight
: bottomBarHeight;
// If navigation bar is opaquely obstructing, directly shift the main content bottomFullObstruction = widget.bottomNavigationBar!.shouldFullyObstruct(context);
// 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 { } else {
// 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( paddedContent = MediaQuery(
data: existingMediaQuery.copyWith( data: fixedExistingMediaQuery.copyWith(
padding: existingMediaQuery.padding.copyWith( padding: fixedExistingMediaQuery.padding.copyWith(
top: topPadding, top: topFullObstruction ? null : topPadding,
bottom: bottomFullObstruction ? null : bottomPadding,
), ),
viewInsets: newViewInsets, viewInsets: newViewInsets,
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding), padding: EdgeInsets.only(
child: paddedContent, top: (widget.navigationBar == null) ? 0 : (topFullObstruction ? topPadding : 0),
bottom: (widget.bottomNavigationBar == null)
? 0
: (bottomFullObstruction ? bottomPadding : 0),
), ),
);
}
} 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, child: paddedContent,
),
); );
}
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
@ -175,6 +179,13 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
right: 0.0, right: 0.0,
child: widget.navigationBar!, 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 // Add a touch handler the size of the status bar on top of all contents
// to handle scroll to top by status bar taps. // to handle scroll to top by status bar taps.
Positioned( Positioned(

@ -123,13 +123,15 @@ class IsekaiText extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var strutStyleFixed = strutStyle; var styleFixed = style ?? const TextStyle();
if (style?.fontSize != null) {} if (styleFixed.color == null) {
styleFixed = styleFixed.copyWith(color: CupertinoTheme.of(context).textTheme.textStyle.color);
}
return Text( return Text(
data, data,
style: style, style: styleFixed,
strutStyle: strutStyleFixed, strutStyle: strutStyle,
textAlign: textAlign, textAlign: textAlign,
textDirection: textDirection, textDirection: textDirection,
locale: locale, locale: locale,

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:isekai_wiki/api/response/page_info.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/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/components/utils.dart';
import 'package:isekai_wiki/pages/article.dart'; import 'package:isekai_wiki/pages/article.dart';
import 'package:like_button/like_button.dart'; import 'package:like_button/like_button.dart';
@ -27,7 +28,6 @@ class PageCardStyles {
static const contentFontSize = 14.0; static const contentFontSize = 14.0;
static const TextStyle titleTextStyle = TextStyle( static const TextStyle titleTextStyle = TextStyle(
color: CupertinoDynamicColor.withBrightness(color: Colors.black, darkColor: Colors.white),
fontSize: titleFontSize, fontSize: titleFontSize,
fontStyle: FontStyle.normal, fontStyle: FontStyle.normal,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@ -156,7 +156,7 @@ class PageCard extends StatelessWidget {
style: SkeletonLineStyle( style: SkeletonLineStyle(
height: (PageCardStyles.titleFontSize * 1.1) * textScale, randomLength: true), height: (PageCardStyles.titleFontSize * 1.1) * textScale, randomLength: true),
), ),
child: Text( child: IsekaiText(
pageInfo?.mainTitle ?? "页面信息丢失", pageInfo?.mainTitle ?? "页面信息丢失",
style: PageCardStyles.titleTextStyle, style: PageCardStyles.titleTextStyle,
textScaleFactor: 1, textScaleFactor: 1,
@ -271,12 +271,12 @@ class PageCard extends StatelessWidget {
height: PageCardStyles.footerButtonSize, height: PageCardStyles.footerButtonSize,
width: PageCardStyles.footerButtonSize, width: PageCardStyles.footerButtonSize,
child: PullDownButton( child: PullDownButton(
routeTheme: PullDownMenuRouteTheme( routeTheme: const PullDownMenuRouteTheme(
endShadow: BoxShadow( endShadow: BoxShadow(
color: Colors.grey.withOpacity(0.6), color: Colors.black26,
spreadRadius: 1, spreadRadius: 1,
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, 2), offset: Offset(0, 2),
), ),
), ),
itemBuilder: _buildMenuItem, itemBuilder: _buildMenuItem,
@ -337,20 +337,18 @@ class PageCard extends StatelessWidget {
} }
Widget _buildCard(BuildContext context) { Widget _buildCard(BuildContext context) {
var textScale = MediaQuery.of(context).textScaleFactor;
return Container( return Container(
margin: Theme.of(context).cardTheme.margin, margin: Theme.of(context).cardTheme.margin,
clipBehavior: Theme.of(context).cardTheme.clipBehavior ?? Clip.antiAlias, clipBehavior: Theme.of(context).cardTheme.clipBehavior ?? Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).cardTheme.color, color: Theme.of(context).cardTheme.color,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Theme.of(context).cardTheme.shadowColor ?? Colors.transparent, color: Theme.of(context).cardTheme.shadowColor ?? Colors.transparent,
blurRadius: 16, blurRadius: 12,
offset: const Offset(0, 2), offset: const Offset(0, 2),
blurStyle: BlurStyle.outer), ),
], ],
), ),
child: SizedBox( child: SizedBox(

@ -1,15 +1,25 @@
import 'package:flutter/cupertino.dart'; 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_html/flutter_html.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:get/get.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/global.dart';
import 'package:isekai_wiki/models/settings.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/reactive/reactive.dart';
import 'package:isekai_wiki/styles.dart'; import 'package:isekai_wiki/utils/simpleTemplate.dart';
class WikiPageParserController extends GetxController { class WikiPageParserController extends GetxController {
static SimpleTemplate? renderTemplate;
InAppWebViewController? webviewCotroller; InAppWebViewController? webviewCotroller;
var pageInfo = Rx<PageInfo?>(null);
var parseInfo = Rx<MWParseInfo?>(null);
var contentHtml = "".obs; var contentHtml = "".obs;
var safeAreaPadding = const EdgeInsets.all(0).obs; var safeAreaPadding = const EdgeInsets.all(0).obs;
var textZoom = 100.obs; var textZoom = 100.obs;
@ -24,10 +34,7 @@ class WikiPageParserController extends GetxController {
loading.value = true; loading.value = true;
if (contentHtml.value.isNotEmpty) { if (contentHtml.value.isNotEmpty) {
webviewCotroller?.loadData( reloadHtml();
data: contentHtml.value,
baseUrl: Uri.parse(Global.siteConfig.baseUrl),
);
} }
}); });
@ -40,29 +47,100 @@ class WikiPageParserController extends GetxController {
}); });
} }
void onWebViewCreated(InAppWebViewController controller) { Future<SimpleTemplate> getRenderTemplate() async {
webviewCotroller = controller; 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<String> 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('<link rel="stylesheet" href="$uri" />');
}
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('<link rel="stylesheet" href="$uri" />');
}
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}");
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( webviewCotroller?.loadData(
data: contentHtml.value, data: parsedHtml,
baseUrl: Uri.parse(Global.siteConfig.baseUrl), 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) { void onPageCommitVisible(InAppWebViewController controller, Uri? uri) {
controller.injectCSSCode(source: """ controller.injectCSSCode(source: """
body { :root {
padding-top: ${safeAreaPadding.value.top}px; --safe-area-top: ${safeAreaPadding.value.top}px;
padding-bottom: ${safeAreaPadding.value.bottom}px; --safe-area-bottom: ${safeAreaPadding.value.bottom}px;
padding-left: ${safeAreaPadding.value.left}px; --safe-area-left: ${safeAreaPadding.value.left}px;
padding-right: ${safeAreaPadding.value.right}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) { if (contentHtml.value.isNotEmpty) {
@ -74,10 +152,11 @@ document.head.appendChild(metaEl);
} }
class WikiPageParser extends StatefulWidget { class WikiPageParser extends StatefulWidget {
final String? contentHtml; final PageInfo? pageInfo;
final MWParseInfo? parseInfo;
final EdgeInsets? padding; final EdgeInsets? padding;
const WikiPageParser({super.key, this.contentHtml, this.padding}); const WikiPageParser({super.key, this.pageInfo, this.parseInfo, this.padding});
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@ -96,7 +175,9 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
@override @override
void receiveProps() { 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.safeAreaPadding.value = widget.padding ?? const EdgeInsets.all(0);
c.textZoom.value = (MediaQuery.of(Get.context!).textScaleFactor * 100).round(); c.textZoom.value = (MediaQuery.of(Get.context!).textScaleFactor * 100).round();
} }
@ -105,10 +186,7 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
return ListView( return ListView(
children: <Widget>[ children: <Widget>[
Container( Container(
color: Styles.panelBackgroundColor, color: Theme.of(context).cardColor,
child: SafeArea(
top: false,
bottom: false,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Column( child: Column(
@ -121,7 +199,6 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
), ),
), ),
), ),
),
], ],
); );
} }

@ -14,10 +14,23 @@ import 'app.dart';
Future<void> init() async { Future<void> init() async {
// //
/*SystemChrome.setPreferredOrientations( SystemChrome.setPreferredOrientations([
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);*/ DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
SystemChrome.setSystemUIOverlayStyle( 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) { if (kIsWeb) {
// web origin // web origin

@ -15,6 +15,7 @@ class SiteConfig {
List<String> moduleStyles; List<String> moduleStyles;
List<String> moduleScripts; List<String> moduleScripts;
String renderTheme; String renderTheme;
String renderTemplateUrl;
String baseUrl; String baseUrl;
String indexUrl; String indexUrl;
@ -29,6 +30,7 @@ class SiteConfig {
this.moduleStyles = const [], this.moduleStyles = const [],
this.moduleScripts = const [], this.moduleScripts = const [],
this.renderTheme = Global.renderThemeFallback, this.renderTheme = Global.renderThemeFallback,
this.renderTemplateUrl = "",
this.baseUrl = "", this.baseUrl = "",
this.indexUrl = "", this.indexUrl = "",
this.apiUrl = "", this.apiUrl = "",

@ -1,6 +1,8 @@
import 'package:cupertino_lists/cupertino_lists.dart'; import 'package:cupertino_lists/cupertino_lists.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_text.dart';
import 'package:isekai_wiki/utils/dialog.dart'; import 'package:isekai_wiki/utils/dialog.dart';
import '../components/dummy_icon.dart'; import '../components/dummy_icon.dart';
@ -30,7 +32,7 @@ class AboutPage extends StatelessWidget {
children: [ children: [
const SizedBox(height: 18), const SizedBox(height: 18),
Container( Container(
color: Styles.panelBackgroundColor, color: Theme.of(context).cardTheme.color,
child: SafeArea( child: SafeArea(
top: false, top: false,
bottom: false, bottom: false,
@ -38,9 +40,9 @@ class AboutPage extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Column( child: Column(
children: const <Widget>[ children: const <Widget>[
Text("异世界百科APP", style: Styles.articleTitle), IsekaiText("异世界百科APP", style: Styles.articleTitle),
SizedBox(height: 18), SizedBox(height: 18),
Text("使用Flutter构建"), IsekaiText("使用Flutter构建"),
], ],
), ),
), ),
@ -48,10 +50,10 @@ class AboutPage extends StatelessWidget {
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
CupertinoListSection.insetGrouped( CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor, backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor,
children: <CupertinoListTile>[ children: <CupertinoListTile>[
CupertinoListTile.notched( CupertinoListTile.notched(
title: const Text('异世界百科', style: TextStyle(color: Styles.linkColor)), title: const IsekaiText('异世界百科', style: TextStyle(color: Styles.linkColor)),
leading: const DummyIcon( leading: const DummyIcon(
color: CupertinoColors.systemBlue, color: CupertinoColors.systemBlue,
icon: CupertinoIcons.globe, icon: CupertinoIcons.globe,

@ -1,12 +1,19 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_html/flutter_html.dart'; import 'package:flutter/foundation.dart';
import 'package:isekai_wiki/api/restbase/page.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/safearea_builder.dart';
import 'package:isekai_wiki/components/wikipage_parser.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_nav_bar.dart';
import '../components/isekai_page_scaffold.dart'; import '../components/isekai_page_scaffold.dart';
import '../styles.dart';
class MinimumArticleData { class MinimumArticleData {
final String title; final String title;
@ -24,56 +31,96 @@ class ArticleCategoryData {
ArticleCategoryData({required this.name, required this.identity}); 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<PageInfo?>(null);
var parseInfo = Rx<MWParseInfo?>(null);
var displayTitle = "".obs;
Future<void> loadPageContent() async {
if (pageTitle.isNotEmpty || pageId.value != 0) {
try {
//
loading.value = true;
MWResponse<List<PageInfo>> 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 { class ArticlePage extends StatefulWidget {
final MinimumArticleData? initialArticleData; final MinimumArticleData? initialArticleData;
final String? targetPage; 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 @override
State<StatefulWidget> createState() => _ArticlePageState(); State<StatefulWidget> createState() => _ArticlePageState();
} }
class _ArticlePageState extends State<ArticlePage> { class _ArticlePageState extends ReactiveState<ArticlePage> {
bool loading = true; var c = ArticlePageController();
String title = "";
String description = "";
String content = "";
String contentHtml = "";
List<String> categories = [];
Future<void> _loadPageContent() async {
if (widget.targetPage != null) {
var resContentHtml = await RestfulPageApi.getPageHtml(widget.targetPage!);
if (resContentHtml != null) {
setState(() {
contentHtml = resContentHtml;
});
}
}
}
@override @override
void initState() { void initState() {
title = widget.initialArticleData?.title ?? "";
description = widget.initialArticleData?.description ?? "";
if (widget.initialArticleData?.mainCategory != null) {
categories.add(widget.initialArticleData!.mainCategory!);
}
super.initState(); 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 @override
Widget build(BuildContext context) { Widget render(BuildContext context) {
return IsekaiPageScaffold( return IsekaiPageScaffold(
navigationBar: IsekaiNavigationBar( navigationBar: IsekaiNavigationBar(
middle: Text(title), middle: Obx(() => Text(c.displayTitle.value)),
), ),
child: SafeAreaBuilder( child: SafeAreaBuilder(
builder: (context, padding) => WikiPageParser( builder: (context, padding) => Obx(
contentHtml: contentHtml, () => WikiPageParser(
padding: padding, padding: padding,
pageInfo: c.pageInfo.value,
parseInfo: c.parseInfo.value,
),
), ),
), ),
); );

@ -108,7 +108,7 @@ class OwnProfileTab extends StatelessWidget {
return FollowTextScale( return FollowTextScale(
child: Obx( child: Obx(
() => CupertinoListSection.insetGrouped( () => CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor, backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor,
dividerMargin: uc.isLoggedIn ? 14 : double.infinity, dividerMargin: uc.isLoggedIn ? 14 : double.infinity,
children: <Widget>[ children: <Widget>[
Obx( Obx(
@ -159,7 +159,7 @@ class OwnProfileTab extends StatelessWidget {
Widget _buildArticleListsSection(BuildContext context, OwnProfileController c) { Widget _buildArticleListsSection(BuildContext context, OwnProfileController c) {
return FollowTextScale( return FollowTextScale(
child: CupertinoListSection.insetGrouped( child: CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor, backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor,
children: <CupertinoListTile>[ children: <CupertinoListTile>[
CupertinoListTile.notched( CupertinoListTile.notched(
title: const Text('收藏'), title: const Text('收藏'),
@ -195,7 +195,7 @@ class OwnProfileTab extends StatelessWidget {
Widget _buildSettingsSection(BuildContext context, OwnProfileController c) { Widget _buildSettingsSection(BuildContext context, OwnProfileController c) {
return FollowTextScale( return FollowTextScale(
child: CupertinoListSection.insetGrouped( child: CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor, backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor,
children: <CupertinoListTile>[ children: <CupertinoListTile>[
CupertinoListTile.notched( CupertinoListTile.notched(
title: const Text('设置'), title: const Text('设置'),

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_page_scaffold.dart'; import 'package:isekai_wiki/components/isekai_page_scaffold.dart';
import 'package:isekai_wiki/components/state_test.dart'; import 'package:isekai_wiki/components/state_test.dart';
@ -23,7 +24,7 @@ class SearchTab extends StatelessWidget {
child: ListView( child: ListView(
children: <Widget>[ children: <Widget>[
Container( Container(
color: Styles.panelBackgroundColor, color: Theme.of(context).cardColor,
child: SafeArea( child: SafeArea(
top: false, top: false,
bottom: false, bottom: false,

@ -7,7 +7,6 @@ import 'package:isekai_wiki/pages/discover.dart';
import 'package:isekai_wiki/pages/home.dart'; import 'package:isekai_wiki/pages/home.dart';
import 'package:isekai_wiki/pages/search.dart'; import 'package:isekai_wiki/pages/search.dart';
import 'package:isekai_wiki/pages/own_profile.dart'; import 'package:isekai_wiki/pages/own_profile.dart';
import 'package:isekai_wiki/styles.dart';
class TabsPageController extends GetxController { class TabsPageController extends GetxController {
final tabController = CupertinoTabController(); final tabController = CupertinoTabController();
@ -57,10 +56,11 @@ class IsekaiWikiTabsPage extends StatelessWidget {
return WillPopScope( return WillPopScope(
onWillPop: c.handleWillPop, onWillPop: c.handleWillPop,
child: CupertinoTabScaffold( child: CupertinoTabScaffold(
backgroundColor: CupertinoTheme.of(context).scaffoldBackgroundColor,
controller: c.tabController, controller: c.tabController,
resizeToAvoidBottomInset: false,
tabBar: CupertinoTabBar( tabBar: CupertinoTabBar(
backgroundColor: Styles.themeBottomColor, backgroundColor: CupertinoTheme.of(context).barBackgroundColor,
activeColor: Styles.themeMainColor,
border: const Border(top: BorderSide(color: CupertinoColors.systemGrey5, width: 2)), border: const Border(top: BorderSide(color: CupertinoColors.systemGrey5, width: 2)),
height: 56, height: 56,
onTap: c.handleTapTab, onTap: c.handleTapTab,

@ -23,45 +23,27 @@ abstract class Styles {
), ),
); );
static CupertinoThemeData cupertinoLightTheme = const CupertinoThemeData( static CupertinoThemeData cupertinoTheme = const CupertinoThemeData(
textTheme: Styles.defaultTextTheme, primaryColor: themeMainColor,
primaryContrastingColor: CupertinoColors.white,
scaffoldBackgroundColor: Styles.themePageBackgroundColor, scaffoldBackgroundColor: Styles.themePageBackgroundColor,
textTheme: Styles.defaultTextTheme,
); );
static CupertinoThemeData cupertinoDarkTheme = const CupertinoThemeData( static CardTheme cardTheme = const CardTheme(
brightness: Brightness.dark, color: Color.fromRGBO(255, 255, 255, 1),
textTheme: Styles.defaultTextTheme, shadowColor: Colors.black12,
scaffoldBackgroundColor: Styles.themePageBackgroundColor, margin: EdgeInsets.symmetric(horizontal: 6, vertical: 8),
); );
static ThemeData materialLightTheme = ThemeData.light().copyWith( static ThemeData materialLightTheme = ThemeData.light().copyWith(
cardTheme: cardTheme, cardTheme: cardTheme,
); );
static ThemeData materialDarkTheme = ThemeData.light().copyWith( static ThemeData materialDarkTheme = ThemeData.dark().copyWith(
cardTheme: cardTheme.copyWith(color: const Color.fromRGBO(28, 28, 28, 1)), 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 const TextStyle articleTitle = TextStyle( 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 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 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 themeBottomColor = CupertinoDynamicColor.withBrightness(
static const Color darkThemePageBackgroundColor = Color.fromRGBO(0, 0, 0, 1); 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 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 Color linkColor = CupertinoColors.link;
static const double largeTitleFontSize = 32; static const double largeTitleFontSize = 32;
static const double navTitleFontSize = 18; static const double navTitleFontSize = 18;

@ -0,0 +1,19 @@
class SimpleTemplate {
String? tpl;
SimpleTemplate({this.tpl});
String fetch(Map<String, String> params) {
if (tpl == null) return "";
var re = RegExp(r"{{(?<name>[^,;:'\n]+?)}}");
return tpl!.replaceAllMapped(re, (match) {
var key = match.group(1)!.trim();
if (params.containsKey(key)) {
return params[key]!;
}
return match.group(0)!;
});
}
}
Loading…
Cancel
Save