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

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; 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';
@ -10,6 +11,47 @@ import 'models/model.dart';
import 'pages/tab_page.dart'; import 'pages/tab_page.dart';
import 'styles.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 { class IsekaiWikiApp extends StatelessWidget {
const IsekaiWikiApp({super.key}); const IsekaiWikiApp({super.key});
@ -49,12 +91,16 @@ class IsekaiWikiApp extends StatelessWidget {
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;
var brightness = MediaQuery.of(context).platformBrightness; var brightness = MediaQuery.of(context).platformBrightness;
return Theme( return IsekaiWikiAppWrapper(
data: brightness != Brightness.dark child: Theme(
? Styles.materialLightTheme data: brightness != Brightness.dark
: Styles.materialDarkTheme, ? Styles.materialLightTheme
child: CupertinoTheme( : Styles.materialDarkTheme,
data: Styles.cupertinoTheme.copyWith(brightness: brightness), child: child), child: CupertinoTheme(
data:
Styles.cupertinoTheme.copyWith(brightness: brightness),
child: child),
),
); );
} }
}, },

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

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

@ -89,7 +89,8 @@ class IsekaiPageScaffold extends StatefulWidget {
class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> { class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
void _handleStatusBarTap() { 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. // Only act on the scroll controller if it has any attached scroll positions.
if (primaryScrollController != null && primaryScrollController.hasClients) { if (primaryScrollController != null && primaryScrollController.hasClients) {
primaryScrollController.animateTo( primaryScrollController.animateTo(
@ -111,7 +112,8 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
final bool topFullObstruction; final bool topFullObstruction;
if (widget.navigationBar != null) { if (widget.navigationBar != null) {
// Propagate top padding and include viewInsets if appropriate // 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); topFullObstruction = widget.navigationBar!.shouldFullyObstruct(context);
} else { } else {
@ -125,12 +127,11 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
final bool bottomFullObstruction; final bool bottomFullObstruction;
if (widget.bottomNavigationBar != null) { if (widget.bottomNavigationBar != null) {
// Propagate bottom padding and include viewInsets if appropriate // Propagate bottom padding and include viewInsets if appropriate
final double bottomBarHeight = widget.bottomNavigationBar?.preferredSize.height ?? 0; bottomPadding = widget.bottomNavigationBar!.preferredSize.height +
bottomPadding = widget.resizeToAvoidBottomInset existingMediaQuery.padding.bottom;
? existingMediaQuery.padding.bottom + bottomBarHeight
: bottomBarHeight;
bottomFullObstruction = widget.bottomNavigationBar!.shouldFullyObstruct(context); bottomFullObstruction =
widget.bottomNavigationBar!.shouldFullyObstruct(context);
} else { } else {
// Propagate bottom padding and include viewInsets if appropriate // Propagate bottom padding and include viewInsets if appropriate
bottomPadding = existingMediaQuery.padding.bottom; bottomPadding = existingMediaQuery.padding.bottom;
@ -154,7 +155,9 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
), ),
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
top: (widget.navigationBar == null) ? 0 : (topFullObstruction ? topPadding : 0), top: (widget.navigationBar == null)
? 0
: (topFullObstruction ? topPadding : 0),
bottom: (widget.bottomNavigationBar == null) bottom: (widget.bottomNavigationBar == null)
? 0 ? 0
: (bottomFullObstruction ? bottomPadding : 0), : (bottomFullObstruction ? bottomPadding : 0),
@ -165,7 +168,8 @@ class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
return DecoratedBox( return DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context) ?? color: CupertinoDynamicColor.maybeResolve(
widget.backgroundColor, context) ??
CupertinoTheme.of(context).scaffoldBackgroundColor, CupertinoTheme.of(context).scaffoldBackgroundColor,
), ),
child: Stack( 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/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_html/flutter_html.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/models/user.dart';
import 'package:isekai_wiki/reactive/reactive.dart'; import 'package:isekai_wiki/reactive/reactive.dart';
import 'package:isekai_wiki/utils/simpleTemplate.dart'; import 'package:isekai_wiki/utils/simpleTemplate.dart';
import 'package:mime/mime.dart';
class WikiPageParserController extends GetxController { class WikiPageParserController extends GetxController {
static SimpleTemplate? renderTemplate; static SimpleTemplate? renderTemplate;
static String jsBridge = "";
InAppWebViewController? webviewCotroller; InAppWebViewController? webviewCotroller;
ScrollController scrollController = ScrollController();
var pageInfo = Rx<PageInfo?>(null); var pageInfo = Rx<PageInfo?>(null);
var parseInfo = Rx<MWParseInfo?>(null); var parseInfo = Rx<MWParseInfo?>(null);
@ -25,6 +29,7 @@ class WikiPageParserController extends GetxController {
var textZoom = 100.obs; var textZoom = 100.obs;
var loading = true.obs; var loading = true.obs;
var pageHeight = 0.obs;
@override @override
void onInit() { 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 { Future<SimpleTemplate> getRenderTemplate() async {
if (renderTemplate == null) { if (renderTemplate == null) {
String tplData; String tplData;
@ -55,7 +66,7 @@ class WikiPageParserController extends GetxController {
var uri = Uri.parse(Global.siteConfig.renderTemplateUrl); var uri = Uri.parse(Global.siteConfig.renderTemplateUrl);
tplData = await BaseApi.get(uri); tplData = await BaseApi.get(uri);
} else { } else {
tplData = await DefaultAssetBundle.of(Get.context!).loadString("assets/tpl/wikiPage.html"); tplData = await rootBundle.loadString("assets/tpl/wikiPage.html");
} }
renderTemplate = SimpleTemplate(tpl: tplData); renderTemplate = SimpleTemplate(tpl: tplData);
} }
@ -66,6 +77,7 @@ class WikiPageParserController extends GetxController {
List<String> tagList = []; List<String> tagList = [];
var resUri = Uri.parse(Global.siteConfig.resourceLoaderUrl); var resUri = Uri.parse(Global.siteConfig.resourceLoaderUrl);
// CSS
if (Global.siteConfig.moduleStyles.isNotEmpty) { if (Global.siteConfig.moduleStyles.isNotEmpty) {
var uri = resUri.replace(queryParameters: { var uri = resUri.replace(queryParameters: {
"lang": pageInfo.value?.pagelanguage ?? "en", "lang": pageInfo.value?.pagelanguage ?? "en",
@ -76,16 +88,23 @@ class WikiPageParserController extends GetxController {
tagList.add('<link rel="stylesheet" href="$uri" />'); 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: { var uri = resUri.replace(queryParameters: {
"lang": pageInfo.value?.pagelanguage ?? "en", "lang": pageInfo.value?.pagelanguage ?? "en",
"modules": parseInfo.value!.modulestyles.join("|"), "modules": pageModules.join("|"),
"only": "styles", "only": "styles",
"skin": Global.siteConfig.renderTheme, "skin": Global.siteConfig.renderTheme,
}); });
tagList.add('<link rel="stylesheet" href="$uri" />'); tagList.add('<link rel="stylesheet" href="$uri" />');
} }
// JS Bridge
tagList.add('<script type="text/javascript">$jsBridge</script>');
return tagList.join(); return tagList.join();
} }
@ -103,10 +122,17 @@ class WikiPageParserController extends GetxController {
classList.add("skin-${Global.siteConfig.renderTheme}"); classList.add("skin-${Global.siteConfig.renderTheme}");
if (GetPlatform.isAndroid) {
classList.add("mugenapp-android");
} else if (GetPlatform.isIOS) {
classList.add("mugenapp-ios");
}
return classList.join(" "); return classList.join(" ");
} }
void reloadHtml() async { void reloadHtml() async {
await initJsBridge();
var tpl = await getRenderTemplate(); var tpl = await getRenderTemplate();
var tplParams = { var tplParams = {
@ -128,27 +154,83 @@ class WikiPageParserController extends GetxController {
void onWebViewCreated(InAppWebViewController controller) { void onWebViewCreated(InAppWebViewController controller) {
webviewCotroller = 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) { if (contentHtml.value.isNotEmpty) {
reloadHtml(); reloadHtml();
} }
} }
void onPageCommitVisible(InAppWebViewController controller, Uri? uri) { void onPageCommitVisible(InAppWebViewController controller, Uri? uri) {
controller.injectCSSCode(source: """ controller.evaluateJavascript(source: """
:root { document.documentElement.style.setProperty('--safe-area-top', '${safeAreaPadding.value.top}px');
--safe-area-top: ${safeAreaPadding.value.top}px; document.documentElement.style.setProperty('--safe-area-right', '${safeAreaPadding.value.right}px');
--safe-area-bottom: ${safeAreaPadding.value.bottom}px; document.documentElement.style.setProperty('--safe-area-bottom', '${safeAreaPadding.value.bottom}px');
--safe-area-left: ${safeAreaPadding.value.left}px; document.documentElement.style.setProperty('--safe-area-left', '${safeAreaPadding.value.left}px');
--safe-area-right: ${safeAreaPadding.value.right}px; navigator.safeArea = {
} top: ${safeAreaPadding.value.top},
right: ${safeAreaPadding.value.right},
bottom: ${safeAreaPadding.value.bottom},
left: ${safeAreaPadding.value.left}
};
"""); """);
if (contentHtml.value.isNotEmpty) { if (contentHtml.value.isNotEmpty) {
Future.delayed(const Duration(milliseconds: 100)).then((value) { Future.delayed(const Duration(milliseconds: 1000)).then((value) {
loading.value = false; 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 { class WikiPageParser extends StatefulWidget {
@ -156,7 +238,8 @@ class WikiPageParser extends StatefulWidget {
final MWParseInfo? parseInfo; final MWParseInfo? parseInfo;
final EdgeInsets? padding; final EdgeInsets? padding;
const WikiPageParser({super.key, this.pageInfo, this.parseInfo, this.padding}); const WikiPageParser(
{super.key, this.pageInfo, this.parseInfo, this.padding});
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@ -179,7 +262,8 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
c.parseInfo.value = widget.parseInfo; c.parseInfo.value = widget.parseInfo;
c.contentHtml.value = widget.parseInfo?.text ?? ""; 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();
} }
Widget _buildRender() { Widget _buildRender() {
@ -212,9 +296,22 @@ class _WikiParserState extends ReactiveState<WikiPageParser> {
child: InAppWebView( child: InAppWebView(
onWebViewCreated: c.onWebViewCreated, onWebViewCreated: c.onWebViewCreated,
onPageCommitVisible: c.onPageCommitVisible, onPageCommitVisible: c.onPageCommitVisible,
onLoadResourceCustomScheme: c.onLoadResourceCustomScheme,
onScrollChanged: c.onScrollChanged,
initialOptions: InAppWebViewGroupOptions( initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(
disableHorizontalScroll: true,
supportZoom: true,
resourceCustomSchemes: ["mugenappres"],
),
android: AndroidInAppWebViewOptions( android: AndroidInAppWebViewOptions(
textZoom: c.textZoom.value, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var sc = Get.find<AppSettingsController>(); 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'; import 'app.dart';
Future<void> init() async { 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) { if (kIsWeb) {
// web origin // web origin
Global.webOrigin = Uri.base.origin; Global.webOrigin = Uri.base.origin;

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_slider_drawer/flutter_slider_drawer.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:isekai_wiki/api/mw/list.dart'; import 'package:isekai_wiki/api/mw/list.dart';
import 'package:isekai_wiki/api/mw/mw_api.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/page_info.dart';
import 'package:isekai_wiki/api/response/parse.dart'; import 'package:isekai_wiki/api/response/parse.dart';
import 'package:isekai_wiki/components/bottom_nav_bar.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/safearea_builder.dart';
import 'package:isekai_wiki/components/wikipage_parser.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/reactive/reactive.dart';
import 'package:isekai_wiki/utils/dialog.dart'; import 'package:isekai_wiki/utils/dialog.dart';
import 'package:isekai_wiki/utils/error.dart'; import 'package:isekai_wiki/utils/error.dart';
@ -22,7 +25,11 @@ class MinimumArticleData {
final String? mainCategory; final String? mainCategory;
final DateTime? updateTime; 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 { class ArticleCategoryData {
@ -35,6 +42,8 @@ class ArticleCategoryData {
class ArticlePageController extends GetxController { class ArticlePageController extends GetxController {
var loading = true.obs; var loading = true.obs;
var menuSlider = GlobalKey<SliderDrawerState>();
var pageTitle = "".obs; var pageTitle = "".obs;
var pageId = 0.obs; var pageId = 0.obs;
@ -50,9 +59,11 @@ class ArticlePageController extends GetxController {
loading.value = true; loading.value = true;
MWResponse<List<PageInfo>> pageInfoRes; MWResponse<List<PageInfo>> pageInfoRes;
if (pageId.value != 0) { if (pageId.value != 0) {
pageInfoRes = await MWApiList.getPageInfoList(pageids: [pageId.value]); pageInfoRes =
await MWApiList.getPageInfoList(pageids: [pageId.value]);
} else { } else {
pageInfoRes = await MWApiList.getPageInfoList(titles: [pageTitle.value]); pageInfoRes =
await MWApiList.getPageInfoList(titles: [pageTitle.value]);
} }
if (pageInfoRes.data.isEmpty) { if (pageInfoRes.data.isEmpty) {
throw MWApiErrorException(code: 'no-page', info: "页面信息丢失"); throw MWApiErrorException(code: 'no-page', info: "页面信息丢失");
@ -67,7 +78,8 @@ class ArticlePageController extends GetxController {
var parseRes = await MWApiParse.parse(pageId: pageId.value); var parseRes = await MWApiParse.parse(pageId: pageId.value);
parseInfo.value = parseRes.data; parseInfo.value = parseRes.data;
} catch (err, stack) { } catch (err, stack) {
alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误"); alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err),
title: "错误");
if (kDebugMode) { if (kDebugMode) {
print("Exception in page: $err"); print("Exception in page: $err");
stack.printError(); stack.printError();
@ -77,6 +89,14 @@ class ArticlePageController extends GetxController {
} }
} }
} }
void handleFlowButtonClick() {
alert(Get.overlayContext!, "评论功能暂未完成");
}
void handleMenuButtonClick() {
menuSlider.currentState?.openSlider();
}
} }
class ArticlePage extends StatefulWidget { class ArticlePage extends StatefulWidget {
@ -84,7 +104,8 @@ class ArticlePage extends StatefulWidget {
final String? targetPage; final String? targetPage;
final int? targetPageId; final int? targetPageId;
const ArticlePage({super.key, this.targetPage, this.targetPageId, this.initialArticleData}); const ArticlePage(
{super.key, this.targetPage, this.targetPageId, this.initialArticleData});
@override @override
State<StatefulWidget> createState() => _ArticlePageState(); State<StatefulWidget> createState() => _ArticlePageState();
@ -102,6 +123,13 @@ class _ArticlePageState extends ReactiveState<ArticlePage> {
c.loadPageContent(); c.loadPageContent();
} }
@override
void dispose() {
super.dispose();
c.dispose();
}
@override @override
void receiveProps() { void receiveProps() {
c.pageTitle.value = widget.targetPage ?? ""; c.pageTitle.value = widget.targetPage ?? "";
@ -111,17 +139,75 @@ class _ArticlePageState extends ReactiveState<ArticlePage> {
@override @override
Widget render(BuildContext context) { Widget render(BuildContext context) {
return IsekaiPageScaffold( final flc = Get.find<FavoriteListController>();
navigationBar: IsekaiNavigationBar(
middle: Obx(() => Text(c.displayTitle.value)), 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: IsekaiPageScaffold(
child: SafeAreaBuilder( navigationBar: IsekaiNavigationBar(
builder: (context, padding) => Obx( middle: Obx(() => Text(c.displayTitle.value)),
() => WikiPageParser( ),
padding: padding, bottomNavigationBar: BottomNavigationBar(
pageInfo: c.pageInfo.value, child: Padding(
parseInfo: c.parseInfo.value, 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 '../global.dart';
import '../styles.dart'; import '../styles.dart';
const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
color: Color(0x4C000000),
darkColor: Color(0x29000000),
);
enum HomeTabs { newest, followed } enum HomeTabs { newest, followed }
class HomeController extends GetxController with GetSingleTickerProviderStateMixin { class HomeController extends GetxController
with GetSingleTickerProviderStateMixin {
double _navSearchButtonOffset = 90; double _navSearchButtonOffset = 90;
var showNavSearchButton = false.obs; var showNavSearchButton = false.obs;
@ -36,12 +42,15 @@ class HomeController extends GetxController with GetSingleTickerProviderStateMix
void onInit() { void onInit() {
tabController = TabController(length: 2, vsync: this); tabController = TabController(length: 2, vsync: this);
_navSearchButtonOffset = 48 * MediaQuery.of(Get.context!).textScaleFactor + 48; _navSearchButtonOffset =
48 * MediaQuery.of(Get.context!).textScaleFactor + 48;
scrollController.addListener(() { scrollController.addListener(() {
if (scrollController.offset >= _navSearchButtonOffset && !showNavSearchButton.value) { if (scrollController.offset >= _navSearchButtonOffset &&
!showNavSearchButton.value) {
showNavSearchButton.value = true; showNavSearchButton.value = true;
} else if (scrollController.offset < _navSearchButtonOffset && showNavSearchButton.value) { } else if (scrollController.offset < _navSearchButtonOffset &&
showNavSearchButton.value) {
showNavSearchButton.value = false; showNavSearchButton.value = false;
} }
}); });
@ -99,7 +108,8 @@ class HomeTab extends StatelessWidget {
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
child: CupertinoButton( child: CupertinoButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.bell, size: 26, color: Styles.themeNavTitleColor), child: const Icon(CupertinoIcons.bell,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {}, onPressed: () {},
), ),
), ),
@ -115,7 +125,8 @@ class HomeTab extends StatelessWidget {
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
child: CupertinoButton( child: CupertinoButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.search, size: 26, color: Styles.themeNavTitleColor), child: const Icon(CupertinoIcons.search,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () { onPressed: () {
onSearchClick?.call(); onSearchClick?.call();
}, },
@ -141,7 +152,8 @@ class HomeTab extends StatelessWidget {
leading: _buildSearchIconButton(), leading: _buildSearchIconButton(),
backgroundColor: Styles.themeMainColor, backgroundColor: Styles.themeMainColor,
brightness: Brightness.dark, brightness: Brightness.dark,
largeTitle: const Text('首页', style: TextStyle(color: Styles.themeNavTitleColor)), largeTitle: const Text('首页',
style: TextStyle(color: Styles.themeNavTitleColor)),
trailing: _buildNotificationIconButton(), trailing: _buildNotificationIconButton(),
border: Border.all(style: BorderStyle.none), 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( child: CupertinoButton(
color: Colors.white, color: Colors.white,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
@ -174,10 +187,12 @@ class HomeTab extends StatelessWidget {
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(1), padding: const EdgeInsets.all(1),
child: const Icon(CupertinoIcons.search, color: Colors.black54), child: const Icon(CupertinoIcons.search,
color: Colors.black54),
), ),
const Text("搜索页面...", 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, indicatorColor: Styles.themeMainColor,
labelColor: Styles.themeMainColor, labelColor: Styles.themeMainColor,
unselectedLabelColor: Colors.black45, unselectedLabelColor: Colors.black45,
tabs: const [CollapsedTabText('最新'), CollapsedTabText('关注')], tabs: const [
CollapsedTabText('最新'),
CollapsedTabText('关注')
],
onTap: (int selected) {}, 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( CupertinoSliverRefreshControl(
onRefresh: c.handleRefresh, onRefresh: c.handleRefresh,
), ),
@ -254,7 +292,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => max(maxHeight, minHeight); double get maxExtent => max(maxHeight, minHeight);
@override @override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child); return SizedBox.expand(child: child);
} }

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

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

@ -37,10 +37,12 @@ abstract class Styles {
); );
static ThemeData materialLightTheme = ThemeData.light().copyWith( static ThemeData materialLightTheme = ThemeData.light().copyWith(
shadowColor: Colors.black12,
cardTheme: cardTheme, cardTheme: cardTheme,
); );
static ThemeData materialDarkTheme = ThemeData.dark().copyWith( static ThemeData materialDarkTheme = ThemeData.dark().copyWith(
shadowColor: Colors.black12,
cardTheme: cardTheme.copyWith( cardTheme: cardTheme.copyWith(
color: const Color.fromRGBO(28, 28, 28, 1), 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 listTileLargeTitle = TextStyle(fontSize: 18);
static const TextStyle listTileSubTitle = TextStyle(fontSize: 16); 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); static const Color websiteNavbarColor = Color.fromRGBO(33, 37, 41, 1);
@ -67,12 +70,14 @@ abstract class Styles {
darkColor: Color.fromRGBO(41, 41, 41, 1), 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), color: Color.fromRGBO(240, 240, 240, 1),
darkColor: Color.fromRGBO(0, 0, 0, 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), color: Color.fromRGBO(255, 255, 255, 1),
darkColor: Color.fromRGBO(0, 0, 0, 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 provider: ^6.0.0
animations: ^2.0.4 animations: ^2.0.4
flutter_displaymode: ^0.3.2 flutter_displaymode: ^0.3.2
flutter_scale_tap: ^1.0.5
roundcheckbox: ^2.0.5 roundcheckbox: ^2.0.5
like_button: ^2.0.4 like_button: ^2.0.4
skeletons: ^0.0.3 skeletons: ^0.0.3
scroll_bottom_navigation_bar: ^4.0.0
modal_bottom_sheet: ^2.1.2 modal_bottom_sheet: ^2.1.2
fluttertoast: ^8.1.2 fluttertoast: ^8.1.2
animated_snack_bar: ^0.3.0 animated_snack_bar: ^0.3.0
@ -57,7 +57,6 @@ dependencies:
web_smooth_scroll: ^1.0.0 web_smooth_scroll: ^1.0.0
json_annotation: ^4.7.0 json_annotation: ^4.7.0
flutter_html: ^2.2.1 flutter_html: ^2.2.1
ruby_text: ^3.0.1
package_info_plus: ^3.0.2 package_info_plus: ^3.0.2
pull_down_button: ^0.4.1 pull_down_button: ^0.4.1
cached_network_image: ^3.2.3 cached_network_image: ^3.2.3
@ -71,6 +70,9 @@ dependencies:
get_storage: ^2.0.3 get_storage: ^2.0.3
freezed_annotation: ^2.2.0 freezed_annotation: ^2.2.0
flutter_slider_drawer:
path: "./packages/flutter_slider_drawer"
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@ -103,6 +105,7 @@ flutter:
assets: assets:
- assets/images/title.png - assets/images/title.png
- assets/tpl/wikiPage.html - assets/tpl/wikiPage.html
- assets/js/app_bridge.js
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware

Loading…
Cancel
Save