增加hover检测组件

main
落雨楓 2 years ago
parent d2430d7602
commit 3ec076e176

@ -0,0 +1,87 @@
var _InAppWebViewReady = false;
var titleList = [];
var currentTitle = '_top';
function onReadyStateChange() {
if (_InAppWebViewReady && document.readyState === "complete") {
titleList = document.querySelectorAll('.mw-parser-output h1, .mw-parser-output h2, .mw-parser-output h3, .mw-parser-output h4, .mw-parser-output h5, .mw-parser-output h6');
window.flutter_inappwebview.callHandler('pageLoaded', true);
}
}
window.addEventListener("flutterInAppWebViewPlatformReady", function(event) {
_InAppWebViewReady = true;
window.flutter_inappwebview.callHandler('bridgeConnected', true);
onReadyStateChange();
});
document.addEventListener("readystatechange", onReadyStateChange);
var _debouceTimer = null;
function debouce(callback, ms) {
return function() {
var _args = arguments;
if (!_debouceTimer) {
_debouceTimer = setTimeout(function() {
callback.apply(this, _args);
_debouceTimer = null;
}, ms);
}
}
}
document.addEventListener('scroll', debouce(function() {
if (!titleList || titleList.length === 0) return;
var offsetTop = (navigator.safeArea ? navigator.safeArea.top : 0) + 10;
var currentTitleEl = null;
var isFirstSection = false;
for (var i = 0; i < titleList.length; i ++) {
var el = titleList[i];
var pos = el.getBoundingClientRect();
if (pos.top - offsetTop > 0) {
if (i === 0) {
isFirstSection = true;
} else {
currentTitleEl = titleList[i - 1];
}
break;
}
}
if (!currentTitleEl && !isFirstSection) {
currentTitleEl = titleList[titleList.length - 1];
}
var newCurrentTitle;
if (isFirstSection) {
newCurrentTitle = '_firstSection';
} else {
var currentTitleAnchorEl = currentTitleEl.querySelector('.mw-headline');
if (currentTitleAnchorEl) {
newCurrentTitle = currentTitleAnchorEl.id;
}
}
if (newCurrentTitle && newCurrentTitle !== currentTitle) {
currentTitle = newCurrentTitle;
window.flutter_inappwebview.callHandler('sectionChange', newCurrentTitle);
}
}, 200), { passive: true });
var MugenApp = {
scrollToTitle: function (anchor) {
var el = document.getElementById(anchor);
if (el) {
var scrollTop = el.offsetTop;
if (navigator.safeArea) {
scrollTop += navigator.safeArea.top;
}
window.scrollTo({
top: scrollTop,
behavior: "smooth",
});
}
},
};

@ -9,6 +9,9 @@ PODS:
- OrderedSet (~> 5.0)
- flutter_web_browser (0.17.1):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
@ -20,6 +23,7 @@ PODS:
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
@ -33,6 +37,7 @@ DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
@ -45,6 +50,7 @@ SPEC REPOS:
trunk:
- FMDB
- OrderedSet
- Toast
EXTERNAL SOURCES:
Flutter:
@ -53,6 +59,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_web_browser:
:path: ".symlinks/plugins/flutter_web_browser/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
@ -72,11 +80,13 @@ SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_web_browser: 7bccaafbb0c5b8862afe7bcd158f15557109f61f
fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/global.dart';
@ -10,6 +11,47 @@ import 'models/model.dart';
import 'pages/tab_page.dart';
import 'styles.dart';
class IsekaiWikiAppWrapper extends StatefulWidget {
final Widget child;
const IsekaiWikiAppWrapper({super.key, required this.child});
@override
State<StatefulWidget> createState() => _IsekaiWikiAppWrapperState();
}
class _IsekaiWikiAppWrapperState extends State<IsekaiWikiAppWrapper> {
@override
void initState() {
super.initState();
if (GetPlatform.isAndroid) {
//
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
if (GetPlatform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarBrightness: Brightness.dark,
statusBarIconBrightness: Brightness.light,
systemStatusBarContrastEnforced: false,
systemNavigationBarColor: Colors.transparent.withAlpha(1),
systemNavigationBarContrastEnforced: false,
systemNavigationBarIconBrightness: Brightness.dark,
),
);
}
}
}
@override
Widget build(BuildContext context) => widget.child;
}
class IsekaiWikiApp extends StatelessWidget {
const IsekaiWikiApp({super.key});
@ -49,12 +91,16 @@ class IsekaiWikiApp extends StatelessWidget {
Styles.textScaleFactor = MediaQuery.of(context).textScaleFactor;
Styles.isXs = MediaQuery.of(context).size.width <= 340;
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),
return IsekaiWikiAppWrapper(
child: Theme(
data: brightness != Brightness.dark
? Styles.materialLightTheme
: Styles.materialDarkTheme,
child: CupertinoTheme(
data:
Styles.cupertinoTheme.copyWith(brightness: brightness),
child: child),
),
);
}
},

@ -33,7 +33,8 @@ Widget _wrapWithBackground({
Widget result = child;
if (updateSystemUiOverlay) {
final bool isDark = backgroundColor.computeLuminance() < 0.179;
final Brightness newBrightness = brightness ?? (isDark ? Brightness.dark : Brightness.light);
final Brightness newBrightness =
brightness ?? (isDark ? Brightness.dark : Brightness.light);
final SystemUiOverlayStyle overlayStyle;
switch (newBrightness) {
case Brightness.dark:
@ -43,10 +44,10 @@ Widget _wrapWithBackground({
overlayStyle = SystemUiOverlayStyle.dark;
break;
}
result = AnnotatedRegion<SystemUiOverlayStyle>(
/*result = AnnotatedRegion<SystemUiOverlayStyle>(
value: overlayStyle,
child: result,
);
);*/
}
final DecoratedBox childWithBackground = DecoratedBox(
decoration: BoxDecoration(
@ -68,7 +69,8 @@ Widget _wrapWithBackground({
);
}
class BottomNavigationBar extends StatefulWidget implements ObstructingPreferredSizeWidget {
class BottomNavigationBar extends StatefulWidget
implements ObstructingPreferredSizeWidget {
final Widget child;
final Color? backgroundColor;
final Brightness? brightness;
@ -116,7 +118,8 @@ class _BottomNavigationBarState extends State<BottomNavigationBar> {
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: SizedBox(
height: (_kNavBarPersistentHeight * scaleFactor) + MediaQuery.of(context).padding.bottom,
height: (_kNavBarPersistentHeight * scaleFactor) +
MediaQuery.of(context).padding.bottom,
child: SafeArea(
top: false,
child: widget.child,

@ -1,3 +1,4 @@
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../styles.dart';
@ -22,11 +23,92 @@ class _OpacityGestureDetectorState extends State<OpacityGestureDetector> {
@override
Widget build(BuildContext context) {
return Text(widget.text,
return Text(
widget.text,
style: const TextStyle(
color: Styles.themeNavTitleColor,
fontWeight: FontWeight.normal
color: Styles.themeNavTitleColor, fontWeight: FontWeight.normal),
);
}
}
enum PointerActiveMode { none, hover, active }
class ClickableBuilder extends StatefulWidget {
final Widget Function(
BuildContext context, PointerActiveMode mode, Widget child) builder;
final Widget? child;
const ClickableBuilder({super.key, required this.builder, this.child});
@override
State<StatefulWidget> createState() => _ClickableBuilder();
}
class _ClickableBuilder extends State<ClickableBuilder> {
bool isHover = false;
bool isActive = false;
bool isPointerDown = false;
bool isPersistActive = false;
void onMouseEnter(PointerEnterEvent event) {
setState(() {
isHover = true;
});
}
void onMouseExit(PointerExitEvent event) {
setState(() {
isHover = false;
});
}
void onPointerDown(dynamic _) {
isPointerDown = true;
setState(() {
isActive = true;
});
Future.delayed(const Duration(milliseconds: 300)).then((value) {
if (isPointerDown) {
isPersistActive = true;
} else {
setState(() {
isActive = false;
});
}
});
}
void onPointerUp(dynamic _) {
isPointerDown = false;
if (isPersistActive) {
isPersistActive = false;
setState(() {
isActive = false;
});
}
}
@override
Widget build(BuildContext context) {
var child = widget.child ?? const SizedBox();
Widget innerItem;
if (isActive) {
innerItem = widget.builder(context, PointerActiveMode.active, child);
} else if (isHover) {
innerItem = widget.builder(context, PointerActiveMode.hover, child);
}
innerItem = widget.builder(context, PointerActiveMode.none, child);
return Listener(
onPointerDown: (event) {},
child: MouseRegion(
onEnter: onMouseEnter,
onExit: onMouseExit,
child: innerItem,
),
);
}
}
}

@ -52,7 +52,8 @@ class _HeroTag {
// Let the Hero tag be described in tree dumps.
@override
String toString() => 'Default Hero tag for Cupertino navigation bars with navigator $navigator';
String toString() =>
'Default Hero tag for Cupertino navigation bars with navigator $navigator';
@override
bool operator ==(Object other) {
@ -129,7 +130,8 @@ Widget _wrapWithBackground({
Widget result = child;
if (updateSystemUiOverlay) {
final bool isDark = backgroundColor.computeLuminance() < 0.179;
final Brightness newBrightness = brightness ?? (isDark ? Brightness.dark : Brightness.light);
final Brightness newBrightness =
brightness ?? (isDark ? Brightness.dark : Brightness.light);
final SystemUiOverlayStyle overlayStyle;
switch (newBrightness) {
case Brightness.dark:
@ -231,7 +233,8 @@ bool _isTransitionable(BuildContext context) {
/// * [IsekaiSliverNavigationBar] for a navigation bar to be placed in a
/// scrolling list and that supports iOS-11-style large titles.
/// * <https://developer.apple.com/design/human-interface-guidelines/ios/bars/navigation-bars/>
class IsekaiNavigationBar extends StatefulWidget implements ObstructingPreferredSizeWidget {
class IsekaiNavigationBar extends StatefulWidget
implements ObstructingPreferredSizeWidget {
/// Creates a navigation bar in the iOS style.
const IsekaiNavigationBar({
super.key,
@ -444,7 +447,8 @@ class _IsekaiNavigationBarState extends State<IsekaiNavigationBar> {
CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ??
CupertinoTheme.of(context).barBackgroundColor;
final _NavigationBarStaticComponents components = _NavigationBarStaticComponents(
final _NavigationBarStaticComponents components =
_NavigationBarStaticComponents(
keys: keys,
route: ModalRoute.of(context),
userLeading: widget.leading,
@ -480,7 +484,9 @@ class _IsekaiNavigationBarState extends State<IsekaiNavigationBar> {
// Get the context that might have a possibly changed CupertinoTheme.
builder: (BuildContext context) {
return Hero(
tag: widget.heroTag == _defaultHeroTag ? _HeroTag(Navigator.of(context)) : widget.heroTag,
tag: widget.heroTag == _defaultHeroTag
? _HeroTag(Navigator.of(context))
: widget.heroTag,
createRectTween: _linearTranslateWithLargestRectSizeTween,
placeholderBuilder: _navBarHeroLaunchPadBuilder,
flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
@ -488,8 +494,10 @@ class _IsekaiNavigationBarState extends State<IsekaiNavigationBar> {
child: _TransitionableNavigationBar(
componentsKeys: keys,
backgroundColor: backgroundColor,
backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
backButtonTextStyle:
CupertinoTheme.of(context).textTheme.navActionTextStyle,
titleTextStyle:
CupertinoTheme.of(context).textTheme.navTitleTextStyle,
largeTitleTextStyle: null,
border: widget.border,
hasUserMiddle: widget.middle != null,
@ -688,7 +696,8 @@ class IsekaiSliverNavigationBar extends StatefulWidget {
final bool stretch;
@override
State<IsekaiSliverNavigationBar> createState() => _IsekaiSliverNavigationBarState();
State<IsekaiSliverNavigationBar> createState() =>
_IsekaiSliverNavigationBarState();
}
// A state class exists for the nav bar so that the keys of its sub-components
@ -705,7 +714,8 @@ class _IsekaiSliverNavigationBarState extends State<IsekaiSliverNavigationBar> {
@override
Widget build(BuildContext context) {
final _NavigationBarStaticComponents components = _NavigationBarStaticComponents(
final _NavigationBarStaticComponents components =
_NavigationBarStaticComponents(
keys: keys,
route: ModalRoute.of(context),
userLeading: widget.leading,
@ -726,7 +736,8 @@ class _IsekaiSliverNavigationBarState extends State<IsekaiSliverNavigationBar> {
keys: keys,
components: components,
userMiddle: widget.middle,
backgroundColor: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ??
backgroundColor: CupertinoDynamicColor.maybeResolve(
widget.backgroundColor, context) ??
CupertinoTheme.of(context).barBackgroundColor,
brightness: widget.brightness,
border: widget.border,
@ -734,17 +745,18 @@ class _IsekaiSliverNavigationBarState extends State<IsekaiSliverNavigationBar> {
actionsForegroundColor: CupertinoTheme.of(context).primaryColor,
transitionBetweenRoutes: widget.transitionBetweenRoutes,
heroTag: widget.heroTag,
persistentHeight:
(_kNavBarPersistentHeight * scaleFactor) + MediaQuery.of(context).padding.top,
persistentHeight: (_kNavBarPersistentHeight * scaleFactor) +
MediaQuery.of(context).padding.top,
alwaysShowMiddle: widget.middle != null,
stretchConfiguration: widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
stretchConfiguration:
widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
),
);
}
}
class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDelegate
with DiagnosticableTreeMixin {
class _LargeTitleNavigationBarSliverDelegate
extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
_LargeTitleNavigationBarSliverDelegate({
required this.keys,
required this.components,
@ -782,17 +794,20 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
@override
double get maxExtent =>
persistentHeight +
(_kNavBarLargeTitleHeightExtension * MediaQuery.of(Get.context!).textScaleFactor);
(_kNavBarLargeTitleHeightExtension *
MediaQuery.of(Get.context!).textScaleFactor);
@override
OverScrollHeaderStretchConfiguration? stretchConfiguration;
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final bool showLargeTitle =
shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
final _PersistentNavigationBar persistentNavigationBar = _PersistentNavigationBar(
final _PersistentNavigationBar persistentNavigationBar =
_PersistentNavigationBar(
components: components,
padding: padding,
// If a user specified middle exists, always show it. Otherwise, show
@ -836,7 +851,9 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
child: Semantics(
header: true,
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
style: CupertinoTheme.of(context)
.textTheme
.navLargeTitleTextStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: components.largeTitle!,
@ -864,7 +881,9 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
}
return Hero(
tag: heroTag == _defaultHeroTag ? _HeroTag(Navigator.of(context)) : heroTag,
tag: heroTag == _defaultHeroTag
? _HeroTag(Navigator.of(context))
: heroTag,
createRectTween: _linearTranslateWithLargestRectSizeTween,
flightShuttleBuilder: _navBarHeroFlightShuttleBuilder,
placeholderBuilder: _navBarHeroLaunchPadBuilder,
@ -874,10 +893,13 @@ class _LargeTitleNavigationBarSliverDelegate extends SliverPersistentHeaderDeleg
// needs to wrap the top level RenderBox rather than a RenderSliver.
child: _TransitionableNavigationBar(
componentsKeys: keys,
backgroundColor: CupertinoDynamicColor.resolve(backgroundColor, context),
backButtonTextStyle: CupertinoTheme.of(context).textTheme.navActionTextStyle,
backgroundColor:
CupertinoDynamicColor.resolve(backgroundColor, context),
backButtonTextStyle:
CupertinoTheme.of(context).textTheme.navActionTextStyle,
titleTextStyle: CupertinoTheme.of(context).textTheme.navTitleTextStyle,
largeTitleTextStyle: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
largeTitleTextStyle:
CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
border: border,
hasUserMiddle: userMiddle != null,
largeExpanded: showLargeTitle,
@ -972,7 +994,8 @@ class _PersistentNavigationBar extends StatelessWidget {
double scaleFactor = MediaQuery.of(context).textScaleFactor;
return SizedBox(
height: (_kNavBarPersistentHeight * scaleFactor) + MediaQuery.of(context).padding.top,
height: (_kNavBarPersistentHeight * scaleFactor) +
MediaQuery.of(context).padding.top,
child: SafeArea(
bottom: false,
child: paddedToolbar,
@ -1331,10 +1354,11 @@ class IsekaiNavigationBarBackButton extends StatelessWidget {
);
}
TextStyle actionTextStyle = CupertinoTheme.of(context).textTheme.navActionTextStyle;
TextStyle actionTextStyle =
CupertinoTheme.of(context).textTheme.navActionTextStyle;
if (color != null) {
actionTextStyle =
actionTextStyle.copyWith(color: CupertinoDynamicColor.maybeResolve(color, context));
actionTextStyle = actionTextStyle.copyWith(
color: CupertinoDynamicColor.maybeResolve(color, context));
}
return CupertinoButton(
@ -1342,12 +1366,13 @@ class IsekaiNavigationBarBackButton extends StatelessWidget {
child: Semantics(
container: true,
excludeSemantics: true,
label: 'Back',
label: '返回',
button: true,
child: DefaultTextStyle(
style: actionTextStyle,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
constraints:
const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
@ -1432,7 +1457,8 @@ class _BackLabel extends StatelessWidget {
// `child` is never passed in into ValueListenableBuilder so it's always
// null here and unused.
Widget _buildPreviousTitleWidget(BuildContext context, String? previousTitle, Widget? child) {
Widget _buildPreviousTitleWidget(
BuildContext context, String? previousTitle, Widget? child) {
previousTitle ??= "返回";
Text textWidget = Text(
@ -1442,7 +1468,7 @@ class _BackLabel extends StatelessWidget {
);
if (previousTitle.length > 12) {
textWidget = const Text('Back');
textWidget = const Text('返回');
}
return Align(
@ -1456,7 +1482,8 @@ class _BackLabel extends StatelessWidget {
Widget build(BuildContext context) {
if (specifiedPreviousTitle != null) {
return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
} else if (route is CupertinoRouteTransitionMixin<dynamic> && !route!.isFirst) {
} else if (route is CupertinoRouteTransitionMixin<dynamic> &&
!route!.isFirst) {
final CupertinoRouteTransitionMixin<dynamic> cupertinoRoute =
route! as CupertinoRouteTransitionMixin<dynamic>;
// There is no timing issue because the previousTitle Listenable changes
@ -1507,8 +1534,8 @@ class _TransitionableNavigationBar extends StatelessWidget {
final Widget child;
RenderBox get renderBox {
final RenderBox box =
componentsKeys.navBarBoxKey.currentContext!.findRenderObject()! as RenderBox;
final RenderBox box = componentsKeys.navBarBoxKey.currentContext!
.findRenderObject()! as RenderBox;
assert(
box.attached,
'_TransitionableNavigationBar.renderBox should be called when building '
@ -1618,19 +1645,31 @@ class _NavigationBarTransition extends StatelessWidget {
},
),
// Draw all the components on top of the empty bar box.
if (componentsTransition.bottomBackChevron != null) componentsTransition.bottomBackChevron!,
if (componentsTransition.bottomBackLabel != null) componentsTransition.bottomBackLabel!,
if (componentsTransition.bottomLeading != null) componentsTransition.bottomLeading!,
if (componentsTransition.bottomMiddle != null) componentsTransition.bottomMiddle!,
if (componentsTransition.bottomLargeTitle != null) componentsTransition.bottomLargeTitle!,
if (componentsTransition.bottomTrailing != null) componentsTransition.bottomTrailing!,
if (componentsTransition.bottomBackChevron != null)
componentsTransition.bottomBackChevron!,
if (componentsTransition.bottomBackLabel != null)
componentsTransition.bottomBackLabel!,
if (componentsTransition.bottomLeading != null)
componentsTransition.bottomLeading!,
if (componentsTransition.bottomMiddle != null)
componentsTransition.bottomMiddle!,
if (componentsTransition.bottomLargeTitle != null)
componentsTransition.bottomLargeTitle!,
if (componentsTransition.bottomTrailing != null)
componentsTransition.bottomTrailing!,
// Draw top components on top of the bottom components.
if (componentsTransition.topLeading != null) componentsTransition.topLeading!,
if (componentsTransition.topBackChevron != null) componentsTransition.topBackChevron!,
if (componentsTransition.topBackLabel != null) componentsTransition.topBackLabel!,
if (componentsTransition.topMiddle != null) componentsTransition.topMiddle!,
if (componentsTransition.topLargeTitle != null) componentsTransition.topLargeTitle!,
if (componentsTransition.topTrailing != null) componentsTransition.topTrailing!,
if (componentsTransition.topLeading != null)
componentsTransition.topLeading!,
if (componentsTransition.topBackChevron != null)
componentsTransition.topBackChevron!,
if (componentsTransition.topBackLabel != null)
componentsTransition.topBackLabel!,
if (componentsTransition.topMiddle != null)
componentsTransition.topMiddle!,
if (componentsTransition.topLargeTitle != null)
componentsTransition.topLargeTitle!,
if (componentsTransition.topTrailing != null)
componentsTransition.topTrailing!,
];
// The actual outer box is big enough to contain both the bottom and top
@ -1638,7 +1677,8 @@ class _NavigationBarTransition extends StatelessWidget {
// can actually be outside the linearly lerp'ed Rect in the middle of
// the animation, such as the topLargeTitle.
return SizedBox(
height: math.max(heightTween.begin!, heightTween.end!) + MediaQuery.of(context).padding.top,
height: math.max(heightTween.begin!, heightTween.end!) +
MediaQuery.of(context).padding.top,
width: double.infinity,
child: Stack(
children: children,
@ -1691,7 +1731,8 @@ class _NavigationBarComponentsTransition {
topLargeExpanded = topNavBar.largeExpanded,
transitionBox =
// paintBounds are based on offset zero so it's ok to expand the Rects.
bottomNavBar.renderBox.paintBounds.expandToInclude(topNavBar.renderBox.paintBounds),
bottomNavBar.renderBox.paintBounds
.expandToInclude(topNavBar.renderBox.paintBounds),
forwardDirection = directionality == TextDirection.ltr ? 1.0 : -1.0;
static final Animatable<double> fadeOut = Tween<double>(
@ -1738,11 +1779,13 @@ class _NavigationBarComponentsTransition {
GlobalKey key, {
required RenderBox from,
}) {
final RenderBox componentBox = key.currentContext!.findRenderObject()! as RenderBox;
final RenderBox componentBox =
key.currentContext!.findRenderObject()! as RenderBox;
assert(componentBox.attached);
return RelativeRect.fromRect(
componentBox.localToGlobal(Offset.zero, ancestor: from) & componentBox.size,
componentBox.localToGlobal(Offset.zero, ancestor: from) &
componentBox.size,
transitionBox,
);
}
@ -1767,8 +1810,10 @@ class _NavigationBarComponentsTransition {
required RenderBox toNavBarBox,
required Widget child,
}) {
final RenderBox fromBox = fromKey.currentContext!.findRenderObject()! as RenderBox;
final RenderBox toBox = toKey.currentContext!.findRenderObject()! as RenderBox;
final RenderBox fromBox =
fromKey.currentContext!.findRenderObject()! as RenderBox;
final RenderBox toBox =
toKey.currentContext!.findRenderObject()! as RenderBox;
final bool isLTR = forwardDirection > 0;
@ -1784,7 +1829,8 @@ class _NavigationBarComponentsTransition {
);
final Offset fromAnchorInFromBox =
fromBox.localToGlobal(fromAnchorLocal, ancestor: fromNavBarBox);
final Offset toAnchorInToBox = toBox.localToGlobal(toAnchorLocal, ancestor: toNavBarBox);
final Offset toAnchorInToBox =
toBox.localToGlobal(toAnchorLocal, ancestor: toNavBarBox);
// We can't get ahold of the render box of the stack (i.e., `transitionBox`)
// we place components on yet, but we know the stack needs to be top-leading
@ -1796,10 +1842,13 @@ class _NavigationBarComponentsTransition {
// coordinates.
final Offset translation = isLTR
? toAnchorInToBox - fromAnchorInFromBox
: Offset(toNavBarBox.size.width - toAnchorInToBox.dx, toAnchorInToBox.dy) -
Offset(fromNavBarBox.size.width - fromAnchorInFromBox.dx, fromAnchorInFromBox.dy);
: Offset(toNavBarBox.size.width - toAnchorInToBox.dx,
toAnchorInToBox.dy) -
Offset(fromNavBarBox.size.width - fromAnchorInFromBox.dx,
fromAnchorInFromBox.dy);
final RelativeRect fromBoxMargin = positionInTransitionBox(fromKey, from: fromNavBarBox);
final RelativeRect fromBoxMargin =
positionInTransitionBox(fromKey, from: fromNavBarBox);
final Offset fromOriginInTransitionBox = Offset(
isLTR ? fromBoxMargin.left : fromBoxMargin.right,
fromBoxMargin.top,
@ -1831,14 +1880,16 @@ class _NavigationBarComponentsTransition {
}
Widget? get bottomLeading {
final KeyedSubtree? bottomLeading = bottomComponents.leadingKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? bottomLeading =
bottomComponents.leadingKey.currentWidget as KeyedSubtree?;
if (bottomLeading == null) {
return null;
}
return Positioned.fromRelativeRect(
rect: positionInTransitionBox(bottomComponents.leadingKey, from: bottomNavBarBox),
rect: positionInTransitionBox(bottomComponents.leadingKey,
from: bottomNavBarBox),
child: FadeTransition(
opacity: fadeOutBy(0.4),
child: bottomLeading.child,
@ -1855,7 +1906,8 @@ class _NavigationBarComponentsTransition {
}
return Positioned.fromRelativeRect(
rect: positionInTransitionBox(bottomComponents.backChevronKey, from: bottomNavBarBox),
rect: positionInTransitionBox(bottomComponents.backChevronKey,
from: bottomNavBarBox),
child: FadeTransition(
opacity: fadeOutBy(0.6),
child: DefaultTextStyle(
@ -1874,8 +1926,9 @@ class _NavigationBarComponentsTransition {
return null;
}
final RelativeRect from =
positionInTransitionBox(bottomComponents.backLabelKey, from: bottomNavBarBox);
final RelativeRect from = positionInTransitionBox(
bottomComponents.backLabelKey,
from: bottomNavBarBox);
// Transition away by sliding horizontally to the leading edge off of the screen.
final RelativeRectTween positionTween = RelativeRectTween(
@ -1901,9 +1954,12 @@ class _NavigationBarComponentsTransition {
}
Widget? get bottomMiddle {
final KeyedSubtree? bottomMiddle = bottomComponents.middleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? bottomMiddle =
bottomComponents.middleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topBackLabel =
topComponents.backLabelKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topLeading =
topComponents.leadingKey.currentWidget as KeyedSubtree?;
// The middle component is non-null when the nav bar is a large title
// nav bar but would be invisible when expanded, therefore don't show it here.
@ -1942,7 +1998,8 @@ class _NavigationBarComponentsTransition {
// fade.
if (bottomMiddle != null && topLeading != null) {
return Positioned.fromRelativeRect(
rect: positionInTransitionBox(bottomComponents.middleKey, from: bottomNavBarBox),
rect: positionInTransitionBox(bottomComponents.middleKey,
from: bottomNavBarBox),
child: FadeTransition(
opacity: fadeOutBy(bottomHasUserMiddle ? 0.4 : 0.7),
// Keep the font when transitioning into a non-back label leading.
@ -1960,8 +2017,10 @@ class _NavigationBarComponentsTransition {
Widget? get bottomLargeTitle {
final KeyedSubtree? bottomLargeTitle =
bottomComponents.largeTitleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topBackLabel =
topComponents.backLabelKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topLeading =
topComponents.leadingKey.currentWidget as KeyedSubtree?;
if (bottomLargeTitle == null || !bottomLargeExpanded) {
return null;
@ -1997,8 +2056,9 @@ class _NavigationBarComponentsTransition {
if (bottomLargeTitle != null && topLeading != null) {
// Unlike bottom middle, the bottom large title moves when it can't
// transition to the top back label position.
final RelativeRect from =
positionInTransitionBox(bottomComponents.largeTitleKey, from: bottomNavBarBox);
final RelativeRect from = positionInTransitionBox(
bottomComponents.largeTitleKey,
from: bottomNavBarBox);
final RelativeRectTween positionTween = RelativeRectTween(
begin: from,
@ -2037,7 +2097,8 @@ class _NavigationBarComponentsTransition {
}
return Positioned.fromRelativeRect(
rect: positionInTransitionBox(bottomComponents.trailingKey, from: bottomNavBarBox),
rect: positionInTransitionBox(bottomComponents.trailingKey,
from: bottomNavBarBox),
child: FadeTransition(
opacity: fadeOutBy(0.6),
child: bottomTrailing.child,
@ -2046,14 +2107,16 @@ class _NavigationBarComponentsTransition {
}
Widget? get topLeading {
final KeyedSubtree? topLeading = topComponents.leadingKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topLeading =
topComponents.leadingKey.currentWidget as KeyedSubtree?;
if (topLeading == null) {
return null;
}
return Positioned.fromRelativeRect(
rect: positionInTransitionBox(topComponents.leadingKey, from: topNavBarBox),
rect:
positionInTransitionBox(topComponents.leadingKey, from: topNavBarBox),
child: FadeTransition(
opacity: fadeInFrom(0.6),
child: topLeading.child,
@ -2071,15 +2134,17 @@ class _NavigationBarComponentsTransition {
return null;
}
final RelativeRect to =
positionInTransitionBox(topComponents.backChevronKey, from: topNavBarBox);
final RelativeRect to = positionInTransitionBox(
topComponents.backChevronKey,
from: topNavBarBox);
RelativeRect from = to;
// If it's the first page with a back chevron, shift in slightly from the
// right.
if (bottomBackChevron == null) {
final RenderBox topBackChevronBox =
topComponents.backChevronKey.currentContext!.findRenderObject()! as RenderBox;
topComponents.backChevronKey.currentContext!.findRenderObject()!
as RenderBox;
from = to.shift(
Offset(
forwardDirection * topBackChevronBox.size.width * 2.0,
@ -2106,20 +2171,24 @@ class _NavigationBarComponentsTransition {
}
Widget? get topBackLabel {
final KeyedSubtree? bottomMiddle = bottomComponents.middleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? bottomMiddle =
bottomComponents.middleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? bottomLargeTitle =
bottomComponents.largeTitleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topBackLabel = topComponents.backLabelKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topBackLabel =
topComponents.backLabelKey.currentWidget as KeyedSubtree?;
if (topBackLabel == null) {
return null;
}
final RenderAnimatedOpacity? topBackLabelOpacity = topComponents.backLabelKey.currentContext
final RenderAnimatedOpacity? topBackLabelOpacity = topComponents
.backLabelKey.currentContext
?.findAncestorRenderObjectOfType<RenderAnimatedOpacity>();
Animation<double>? midClickOpacity;
if (topBackLabelOpacity != null && topBackLabelOpacity.opacity.value < 1.0) {
if (topBackLabelOpacity != null &&
topBackLabelOpacity.opacity.value < 1.0) {
midClickOpacity = animation.drive(Tween<double>(
begin: 0.0,
end: topBackLabelOpacity.opacity.value,
@ -2131,7 +2200,9 @@ class _NavigationBarComponentsTransition {
// content text might be different. For instance, if the bottomLargeTitle
// text is too long, the topBackLabel will say 'Back' instead of the original
// text.
if (bottomLargeTitle != null && topBackLabel != null && bottomLargeExpanded) {
if (bottomLargeTitle != null &&
topBackLabel != null &&
bottomLargeExpanded) {
return slideFromLeadingEdge(
fromKey: bottomComponents.largeTitleKey,
fromNavBarBox: bottomNavBarBox,
@ -2177,7 +2248,8 @@ class _NavigationBarComponentsTransition {
}
Widget? get topMiddle {
final KeyedSubtree? topMiddle = topComponents.middleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topMiddle =
topComponents.middleKey.currentWidget as KeyedSubtree?;
if (topMiddle == null) {
return null;
@ -2189,9 +2261,10 @@ class _NavigationBarComponentsTransition {
return null;
}
final RelativeRect to = positionInTransitionBox(topComponents.middleKey, from: topNavBarBox);
final RenderBox toBox =
topComponents.middleKey.currentContext!.findRenderObject()! as RenderBox;
final RelativeRect to =
positionInTransitionBox(topComponents.middleKey, from: topNavBarBox);
final RenderBox toBox = topComponents.middleKey.currentContext!
.findRenderObject()! as RenderBox;
final bool isLTR = forwardDirection > 0;
@ -2228,14 +2301,16 @@ class _NavigationBarComponentsTransition {
}
Widget? get topTrailing {
final KeyedSubtree? topTrailing = topComponents.trailingKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topTrailing =
topComponents.trailingKey.currentWidget as KeyedSubtree?;
if (topTrailing == null) {
return null;
}
return Positioned.fromRelativeRect(
rect: positionInTransitionBox(topComponents.trailingKey, from: topNavBarBox),
rect: positionInTransitionBox(topComponents.trailingKey,
from: topNavBarBox),
child: FadeTransition(
opacity: fadeInFrom(0.4),
child: topTrailing.child,
@ -2244,14 +2319,15 @@ class _NavigationBarComponentsTransition {
}
Widget? get topLargeTitle {
final KeyedSubtree? topLargeTitle = topComponents.largeTitleKey.currentWidget as KeyedSubtree?;
final KeyedSubtree? topLargeTitle =
topComponents.largeTitleKey.currentWidget as KeyedSubtree?;
if (topLargeTitle == null || !topLargeExpanded) {
return null;
}
final RelativeRect to =
positionInTransitionBox(topComponents.largeTitleKey, from: topNavBarBox);
final RelativeRect to = positionInTransitionBox(topComponents.largeTitleKey,
from: topNavBarBox);
// Shift in from the trailing edge of the screen.
final RelativeRectTween positionTween = RelativeRectTween(
@ -2341,7 +2417,8 @@ Widget _navBarHeroFlightShuttleBuilder(
final _TransitionableNavigationBar fromNavBar =
fromHeroWidget.child as _TransitionableNavigationBar;
final _TransitionableNavigationBar toNavBar = toHeroWidget.child as _TransitionableNavigationBar;
final _TransitionableNavigationBar toNavBar =
toHeroWidget.child as _TransitionableNavigationBar;
assert(fromNavBar.componentsKeys != null);
assert(toNavBar.componentsKeys != null);

@ -89,7 +89,8 @@ class IsekaiPageScaffold extends StatefulWidget {
class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
void _handleStatusBarTap() {
final ScrollController? primaryScrollController = PrimaryScrollController.of(context);
final ScrollController? primaryScrollController =
PrimaryScrollController.of(context);
// Only act on the scroll controller if it has any attached scroll positions.
if (primaryScrollController != null && primaryScrollController.hasClients) {
primaryScrollController.animateTo(
@ -111,7 +112,8 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
final bool topFullObstruction;
if (widget.navigationBar != null) {
// Propagate top padding and include viewInsets if appropriate
topPadding = widget.navigationBar!.preferredSize.height + existingMediaQuery.padding.top;
topPadding = widget.navigationBar!.preferredSize.height +
existingMediaQuery.padding.top;
topFullObstruction = widget.navigationBar!.shouldFullyObstruct(context);
} else {
@ -125,12 +127,11 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
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;
bottomPadding = widget.bottomNavigationBar!.preferredSize.height +
existingMediaQuery.padding.bottom;
bottomFullObstruction = widget.bottomNavigationBar!.shouldFullyObstruct(context);
bottomFullObstruction =
widget.bottomNavigationBar!.shouldFullyObstruct(context);
} else {
// Propagate bottom padding and include viewInsets if appropriate
bottomPadding = existingMediaQuery.padding.bottom;
@ -154,7 +155,9 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
),
child: Padding(
padding: EdgeInsets.only(
top: (widget.navigationBar == null) ? 0 : (topFullObstruction ? topPadding : 0),
top: (widget.navigationBar == null)
? 0
: (topFullObstruction ? topPadding : 0),
bottom: (widget.bottomNavigationBar == null)
? 0
: (bottomFullObstruction ? bottomPadding : 0),
@ -165,7 +168,8 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
return DecoratedBox(
decoration: BoxDecoration(
color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ??
color: CupertinoDynamicColor.maybeResolve(
widget.backgroundColor, context) ??
CupertinoTheme.of(context).scaffoldBackgroundColor,
),
child: Stack(

@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
class NavBarButton extends StatelessWidget {
final IconData icon;
final VoidCallback? onPressed;
final String? semanticLabel;
const NavBarButton(
{super.key, required this.icon, this.onPressed, this.semanticLabel});
@override
Widget build(BuildContext context) {
final theme = CupertinoTheme.of(context);
var color = theme.textTheme.navActionTextStyle.color ?? theme.primaryColor;
return CupertinoButton(
padding: EdgeInsets.zero,
onPressed: onPressed,
child: Icon(
icon,
color: color,
size: 26,
),
);
}
}

@ -0,0 +1,29 @@
import 'package:flutter/cupertino.dart';
import 'package:isekai_wiki/components/gesture_detector.dart';
import 'package:isekai_wiki/reactive/reactive.dart';
class TOCList extends StatefulWidget {
const TOCList({super.key});
@override
State<StatefulWidget> createState() => _TOCListState();
}
class _TOCListState extends ReactiveState<TOCList> {
@override
Widget render(BuildContext context) {
return Container();
}
}
class TOCItem extends StatelessWidget {
final VoidCallback? onTap;
final bool active;
const TOCItem({super.key, this.onTap, this.active = false});
@override
Widget build(BuildContext context) {
return Container();
}
}

@ -1,4 +1,5 @@
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';
@ -12,11 +13,14 @@ 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 = "";
InAppWebViewController? webviewCotroller;
ScrollController scrollController = ScrollController();
var pageInfo = Rx<PageInfo?>(null);
var parseInfo = Rx<MWParseInfo?>(null);
@ -25,6 +29,7 @@ class WikiPageParserController extends GetxController {
var textZoom = 100.obs;
var loading = true.obs;
var pageHeight = 0.obs;
@override
void onInit() {
@ -47,6 +52,12 @@ class WikiPageParserController extends GetxController {
});
}
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;
@ -55,7 +66,7 @@ class WikiPageParserController extends GetxController {
var uri = Uri.parse(Global.siteConfig.renderTemplateUrl);
tplData = await BaseApi.get(uri);
} else {
tplData = await DefaultAssetBundle.of(Get.context!).loadString("assets/tpl/wikiPage.html");
tplData = await rootBundle.loadString("assets/tpl/wikiPage.html");
}
renderTemplate = SimpleTemplate(tpl: tplData);
}
@ -66,6 +77,7 @@ class WikiPageParserController extends GetxController {
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",
@ -76,16 +88,23 @@ class WikiPageParserController extends GetxController {
tagList.add('<link rel="stylesheet" href="$uri" />');
}
if (parseInfo.value?.modulestyles != null && parseInfo.value!.modulestyles.isNotEmpty) {
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": parseInfo.value!.modulestyles.join("|"),
"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();
}
@ -103,10 +122,17 @@ class WikiPageParserController extends GetxController {
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 = {
@ -128,27 +154,83 @@ class WikiPageParserController extends GetxController {
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;
debugPrint("sectionChange: $anchor");
},
);
if (contentHtml.value.isNotEmpty) {
reloadHtml();
}
}
void onPageCommitVisible(InAppWebViewController controller, Uri? uri) {
controller.injectCSSCode(source: """
: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: """
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: 100)).then((value) {
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) {}
}
class WikiPageParser extends StatefulWidget {
@ -156,7 +238,8 @@ class WikiPageParser extends StatefulWidget {
final MWParseInfo? parseInfo;
final EdgeInsets? padding;
const WikiPageParser({super.key, this.pageInfo, this.parseInfo, this.padding});
const WikiPageParser(
{super.key, this.pageInfo, this.parseInfo, this.padding});
@override
State<StatefulWidget> createState() {
@ -179,7 +262,8 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
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.textZoom.value =
(MediaQuery.of(Get.context!).textScaleFactor * 100).round();
}
Widget _buildRender() {
@ -212,9 +296,22 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
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,
),
),
),
@ -231,6 +328,7 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
@override
Widget build(BuildContext context) {
var sc = Get.find<AppSettingsController>();
return Obx(() => sc.betaPageRender.value ? _buildRender() : _buildWebview());
return Obx(
() => sc.betaPageRender.value ? _buildRender() : _buildWebview());
}
}

@ -13,25 +13,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'app.dart';
Future<void> init() async {
//
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
SystemChrome.setSystemUIOverlayStyle(
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
Global.webOrigin = Uri.base.origin;

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_slider_drawer/flutter_slider_drawer.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/api/mw/list.dart';
import 'package:isekai_wiki/api/mw/mw_api.dart';
@ -7,8 +8,10 @@ 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/bottom_nav_bar.dart';
import 'package:isekai_wiki/components/nav_bar_button.dart';
import 'package:isekai_wiki/components/safearea_builder.dart';
import 'package:isekai_wiki/components/wikipage_parser.dart';
import 'package:isekai_wiki/models/favorite_list.dart';
import 'package:isekai_wiki/reactive/reactive.dart';
import 'package:isekai_wiki/utils/dialog.dart';
import 'package:isekai_wiki/utils/error.dart';
@ -22,7 +25,11 @@ class MinimumArticleData {
final String? mainCategory;
final DateTime? updateTime;
MinimumArticleData({required this.title, this.description, this.mainCategory, this.updateTime});
MinimumArticleData(
{required this.title,
this.description,
this.mainCategory,
this.updateTime});
}
class ArticleCategoryData {
@ -35,6 +42,8 @@ class ArticleCategoryData {
class ArticlePageController extends GetxController {
var loading = true.obs;
var menuSlider = GlobalKey<SliderDrawerState>();
var pageTitle = "".obs;
var pageId = 0.obs;
@ -50,9 +59,11 @@ class ArticlePageController extends GetxController {
loading.value = true;
MWResponse<List<PageInfo>> pageInfoRes;
if (pageId.value != 0) {
pageInfoRes = await MWApiList.getPageInfoList(pageids: [pageId.value]);
pageInfoRes =
await MWApiList.getPageInfoList(pageids: [pageId.value]);
} else {
pageInfoRes = await MWApiList.getPageInfoList(titles: [pageTitle.value]);
pageInfoRes =
await MWApiList.getPageInfoList(titles: [pageTitle.value]);
}
if (pageInfoRes.data.isEmpty) {
throw MWApiErrorException(code: 'no-page', info: "页面信息丢失");
@ -67,7 +78,8 @@ class ArticlePageController extends GetxController {
var parseRes = await MWApiParse.parse(pageId: pageId.value);
parseInfo.value = parseRes.data;
} catch (err, stack) {
alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误");
alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err),
title: "错误");
if (kDebugMode) {
print("Exception in page: $err");
stack.printError();
@ -77,6 +89,14 @@ class ArticlePageController extends GetxController {
}
}
}
void handleFlowButtonClick() {
alert(Get.overlayContext!, "评论功能暂未完成");
}
void handleMenuButtonClick() {
menuSlider.currentState?.openSlider();
}
}
class ArticlePage extends StatefulWidget {
@ -84,7 +104,8 @@ class ArticlePage extends StatefulWidget {
final String? targetPage;
final int? targetPageId;
const ArticlePage({super.key, this.targetPage, this.targetPageId, this.initialArticleData});
const ArticlePage(
{super.key, this.targetPage, this.targetPageId, this.initialArticleData});
@override
State<StatefulWidget> createState() => _ArticlePageState();
@ -102,6 +123,13 @@ class _ArticlePageState extends ReactiveState<ArticlePage> {
c.loadPageContent();
}
@override
void dispose() {
super.dispose();
c.dispose();
}
@override
void receiveProps() {
c.pageTitle.value = widget.targetPage ?? "";
@ -111,17 +139,75 @@ class _ArticlePageState extends ReactiveState<ArticlePage> {
@override
Widget render(BuildContext context) {
return IsekaiPageScaffold(
navigationBar: IsekaiNavigationBar(
middle: Obx(() => Text(c.displayTitle.value)),
final flc = Get.find<FavoriteListController>();
return SliderDrawer(
key: c.menuSlider,
isCupertino: true,
appBar: null,
showCover: true,
slideDirection: SlideDirection.RIGHT_TO_LEFT,
slider: Container(
color: CupertinoColors.systemRed,
),
bottomNavigationBar: BottomNavigationBar(child: SizedBox()),
child: SafeAreaBuilder(
builder: (context, padding) => Obx(
() => WikiPageParser(
padding: padding,
pageInfo: c.pageInfo.value,
parseInfo: c.parseInfo.value,
child: IsekaiPageScaffold(
navigationBar: IsekaiNavigationBar(
middle: Obx(() => Text(c.displayTitle.value)),
),
bottomNavigationBar: BottomNavigationBar(
child: Padding(
padding: const EdgeInsets.all(0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
NavBarButton(
icon: CupertinoIcons.chat_bubble_2,
onPressed: c.handleFlowButtonClick,
semanticLabel: "讨论",
),
Obx(() {
if (c.pageInfo.value != null &&
flc.isFavorite(c.pageInfo.value!)) {
return NavBarButton(
icon: CupertinoIcons.heart_fill,
onPressed: () {},
semanticLabel: "取消收藏",
);
} else {
return NavBarButton(
icon: CupertinoIcons.heart,
onPressed: () {},
semanticLabel: "收藏",
);
}
}),
NavBarButton(
icon: CupertinoIcons.share,
onPressed: c.handleFlowButtonClick,
semanticLabel: "分享",
),
NavBarButton(
icon: CupertinoIcons.textformat_alt,
onPressed: c.handleFlowButtonClick,
semanticLabel: "阅读设置",
),
NavBarButton(
icon: CupertinoIcons.list_bullet,
onPressed: c.handleMenuButtonClick,
semanticLabel: "目录",
),
],
),
),
),
child: SafeAreaBuilder(
builder: (context, padding) => Obx(
() => WikiPageParser(
padding: padding,
pageInfo: c.pageInfo.value,
parseInfo: c.parseInfo.value,
),
),
),
),

@ -15,9 +15,15 @@ import '../components/collapsed_tab.dart';
import '../global.dart';
import '../styles.dart';
const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
color: Color(0x4C000000),
darkColor: Color(0x29000000),
);
enum HomeTabs { newest, followed }
class HomeController extends GetxController with GetSingleTickerProviderStateMixin {
class HomeController extends GetxController
with GetSingleTickerProviderStateMixin {
double _navSearchButtonOffset = 90;
var showNavSearchButton = false.obs;
@ -36,12 +42,15 @@ class HomeController extends GetxController with GetSingleTickerProviderStateMix
void onInit() {
tabController = TabController(length: 2, vsync: this);
_navSearchButtonOffset = 48 * MediaQuery.of(Get.context!).textScaleFactor + 48;
_navSearchButtonOffset =
48 * MediaQuery.of(Get.context!).textScaleFactor + 48;
scrollController.addListener(() {
if (scrollController.offset >= _navSearchButtonOffset && !showNavSearchButton.value) {
if (scrollController.offset >= _navSearchButtonOffset &&
!showNavSearchButton.value) {
showNavSearchButton.value = true;
} else if (scrollController.offset < _navSearchButtonOffset && showNavSearchButton.value) {
} else if (scrollController.offset < _navSearchButtonOffset &&
showNavSearchButton.value) {
showNavSearchButton.value = false;
}
});
@ -99,7 +108,8 @@ class HomeTab extends StatelessWidget {
duration: const Duration(milliseconds: 100),
child: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.bell, size: 26, color: Styles.themeNavTitleColor),
child: const Icon(CupertinoIcons.bell,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {},
),
),
@ -115,7 +125,8 @@ class HomeTab extends StatelessWidget {
duration: const Duration(milliseconds: 100),
child: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.search, size: 26, color: Styles.themeNavTitleColor),
child: const Icon(CupertinoIcons.search,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {
onSearchClick?.call();
},
@ -141,7 +152,8 @@ class HomeTab extends StatelessWidget {
leading: _buildSearchIconButton(),
backgroundColor: Styles.themeMainColor,
brightness: Brightness.dark,
largeTitle: const Text('首页', style: TextStyle(color: Styles.themeNavTitleColor)),
largeTitle: const Text('首页',
style: TextStyle(color: Styles.themeNavTitleColor)),
trailing: _buildNotificationIconButton(),
border: Border.all(style: BorderStyle.none),
),
@ -161,7 +173,8 @@ class HomeTab extends StatelessWidget {
),
],
),
padding: const EdgeInsets.only(bottom: 10, left: 12, right: 12),
padding:
const EdgeInsets.only(bottom: 10, left: 12, right: 12),
child: CupertinoButton(
color: Colors.white,
padding: const EdgeInsets.all(0),
@ -174,10 +187,12 @@ class HomeTab extends StatelessWidget {
children: [
Container(
padding: const EdgeInsets.all(1),
child: const Icon(CupertinoIcons.search, color: Colors.black54),
child: const Icon(CupertinoIcons.search,
color: Colors.black54),
),
const Text("搜索页面...",
textAlign: TextAlign.center, style: TextStyle(color: Colors.black54))
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54))
],
),
),
@ -208,7 +223,10 @@ class HomeTab extends StatelessWidget {
indicatorColor: Styles.themeMainColor,
labelColor: Styles.themeMainColor,
unselectedLabelColor: Colors.black45,
tabs: const [CollapsedTabText('最新'), CollapsedTabText('关注')],
tabs: const [
CollapsedTabText('最新'),
CollapsedTabText('关注')
],
onTap: (int selected) {},
),
),
@ -218,6 +236,26 @@ class HomeTab extends StatelessWidget {
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
minHeight: 1,
maxHeight: 1,
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
boxShadow: [
BoxShadow(
color: Theme.of(context).shadowColor,
spreadRadius: 2,
blurRadius: 4,
offset: const Offset(0, 2),
)
],
),
),
),
),
CupertinoSliverRefreshControl(
onRefresh: c.handleRefresh,
),
@ -254,7 +292,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => max(maxHeight, minHeight);
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}

@ -61,7 +61,6 @@ class IsekaiWikiTabsPage extends StatelessWidget {
resizeToAvoidBottomInset: false,
tabBar: CupertinoTabBar(
backgroundColor: CupertinoTheme.of(context).barBackgroundColor,
border: const Border(top: BorderSide(color: CupertinoColors.systemGrey5, width: 2)),
height: 56,
onTap: c.handleTapTab,
items: const <BottomNavigationBarItem>[

@ -40,7 +40,8 @@ class WelcomePageController extends GetxController {
);
} catch (err, stack) {
// ignore: prefer_interpolation_to_compose_strings
alert(Get.overlayContext!, "无法加载站点配置:" + ErrorUtils.getErrorMessage(err), title: "错误");
alert(Get.overlayContext!, "无法加载站点配置:" + ErrorUtils.getErrorMessage(err),
title: "错误");
if (kDebugMode) {
print("Exception in logout: $err");
stack.printError();
@ -57,58 +58,57 @@ class WelcomePage extends StatelessWidget {
Widget build(BuildContext context) {
var c = Get.put(WelcomePageController());
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: IsekaiPageScaffold(
backgroundColor: CupertinoColors.white,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: ExactAssetImage('assets/images/title.png'),
fit: BoxFit.cover,
),
return IsekaiPageScaffold(
backgroundColor: CupertinoColors.white,
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: ExactAssetImage('assets/images/title.png'),
fit: BoxFit.cover,
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(color: Colors.black.withOpacity(0.4)),
),
),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
child: Container(
decoration: BoxDecoration(color: Colors.black.withOpacity(0.4)),
),
),
SafeArea(
child: OrientationBuilder(
builder: (context, orientation) => Padding(
padding: orientation == Orientation.portrait
? const EdgeInsets.only(top: 48, right: 20, bottom: 32, left: 20)
: const EdgeInsets.symmetric(horizontal: 20, vertical: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 0,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
child: _buildSiteTitle(context, c),
),
),
SafeArea(
child: OrientationBuilder(
builder: (context, orientation) => Padding(
padding: orientation == Orientation.portrait
? const EdgeInsets.only(
top: 48, right: 20, bottom: 32, left: 20)
: const EdgeInsets.symmetric(horizontal: 20, vertical: 32),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 0,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 24, horizontal: 8),
child: _buildSiteTitle(context, c),
),
],
),
const SizedBox(height: 36),
_buildActions(context, c),
],
),
),
],
),
const SizedBox(height: 36),
_buildActions(context, c),
],
),
),
),
],
),
),
],
),
);
}
@ -121,7 +121,8 @@ class WelcomePage extends StatelessWidget {
data: MediaQueryData(textScaleFactor: 1),
child: Text(
Global.siteTitle,
style: TextStyle(fontSize: 48, color: Colors.white, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 48, color: Colors.white, fontWeight: FontWeight.bold),
),
),
Text(
@ -186,7 +187,9 @@ class WelcomePage extends StatelessWidget {
Obx(
() => CupertinoButton.filled(
disabledColor: theme.primaryColor.withOpacity(0.6),
onPressed: c.policyAccepted.value && !c.isLoading.value ? c.handleClickContinue : null,
onPressed: c.policyAccepted.value && !c.isLoading.value
? c.handleClickContinue
: null,
child: const Text("继续"),
),
),

@ -37,10 +37,12 @@ abstract class Styles {
);
static ThemeData materialLightTheme = ThemeData.light().copyWith(
shadowColor: Colors.black12,
cardTheme: cardTheme,
);
static ThemeData materialDarkTheme = ThemeData.dark().copyWith(
shadowColor: Colors.black12,
cardTheme: cardTheme.copyWith(
color: const Color.fromRGBO(28, 28, 28, 1),
),
@ -55,7 +57,8 @@ abstract class Styles {
static const TextStyle listTileLargeTitle = TextStyle(fontSize: 18);
static const TextStyle listTileSubTitle = TextStyle(fontSize: 16);
static const TextStyle loadingDialogTitle = TextStyle(fontSize: 18, fontWeight: FontWeight.w500);
static const TextStyle loadingDialogTitle =
TextStyle(fontSize: 18, fontWeight: FontWeight.w500);
static const Color websiteNavbarColor = Color.fromRGBO(33, 37, 41, 1);
@ -67,12 +70,14 @@ abstract class Styles {
darkColor: Color.fromRGBO(41, 41, 41, 1),
);
static const Color themePageBackgroundColor = CupertinoDynamicColor.withBrightness(
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(
static const Color themePureBackgroundColor =
CupertinoDynamicColor.withBrightness(
color: Color.fromRGBO(255, 255, 255, 1),
darkColor: Color.fromRGBO(0, 0, 0, 1),
);

@ -0,0 +1 @@
Subproject commit d9c92541d120e653b69a114fafdb6648455ec621

File diff suppressed because it is too large Load Diff

@ -43,10 +43,10 @@ dependencies:
provider: ^6.0.0
animations: ^2.0.4
flutter_displaymode: ^0.3.2
flutter_scale_tap: ^1.0.5
roundcheckbox: ^2.0.5
like_button: ^2.0.4
skeletons: ^0.0.3
scroll_bottom_navigation_bar: ^4.0.0
modal_bottom_sheet: ^2.1.2
fluttertoast: ^8.1.2
animated_snack_bar: ^0.3.0
@ -57,7 +57,6 @@ dependencies:
web_smooth_scroll: ^1.0.0
json_annotation: ^4.7.0
flutter_html: ^2.2.1
ruby_text: ^3.0.1
package_info_plus: ^3.0.2
pull_down_button: ^0.4.1
cached_network_image: ^3.2.3
@ -71,6 +70,9 @@ dependencies:
get_storage: ^2.0.3
freezed_annotation: ^2.2.0
flutter_slider_drawer:
path: "./packages/flutter_slider_drawer"
dev_dependencies:
flutter_test:
sdk: flutter
@ -103,6 +105,7 @@ flutter:
assets:
- assets/images/title.png
- assets/tpl/wikiPage.html
- assets/js/app_bridge.js
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware

Loading…
Cancel
Save