Merge pull request #159 from gskinnerTeam/feature/routing_fixes

Feature/routing fixes
This commit is contained in:
Shawn
2024-01-22 15:45:05 -07:00
committed by GitHub
24 changed files with 261 additions and 99 deletions

View File

@ -427,5 +427,7 @@
"timelineEvent1969ce": "Apollo 11 mission lands on the moon",
"privacyPolicy": "Privacy Policy",
"privacyStatement": "As explained in our {privacyUrl} we do not collect any personal information.",
"@privacyStatement": {"placeholders": {"privacyUrl": {}}}
"@privacyStatement": {"placeholders": {"privacyUrl": {}}},
"pageNotFoundBackButton": "Back to civilization",
"pageNotFoundMessage": "The page you are looking for does not exist."
}

View File

@ -405,5 +405,7 @@
"timelineEvent1957ce": "苏联发射斯普特尼克1号",
"timelineEvent1969ce": "阿波罗11号在月球着陆",
"privacyPolicy": "隐私政策",
"privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉gskinner 不会收集您的个人信息。"
"privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉gskinner 不会收集您的个人信息。",
"pageNotFoundBackButton": "回到文明",
"pageNotFoundMessage": "您正在寻找的页面不存在"
}

View File

@ -81,7 +81,7 @@ class AppLogic {
if (showIntro) {
appRouter.go(ScreenPaths.intro);
} else {
appRouter.go(ScreenPaths.home);
appRouter.go(initialDeeplink ?? ScreenPaths.home);
}
}

View File

@ -16,7 +16,7 @@ mixin ThrottledSaveLoadMixin {
}
Future<void> save() async {
debugPrint('Saving...');
if (!kIsWeb) debugPrint('Saving...');
try {
await _file.save(toJson());
} on Exception catch (e) {

View File

@ -10,15 +10,25 @@ class SettingsLogic with ThrottledSaveLoadMixin {
late final hasDismissedSearchMessage = ValueNotifier<bool>(false)..addListener(scheduleSave);
late final isSearchPanelOpen = ValueNotifier<bool>(true)..addListener(scheduleSave);
late final currentLocale = ValueNotifier<String?>(null)..addListener(scheduleSave);
late final prevWonderIndex = ValueNotifier<int?>(null)..addListener(scheduleSave);
final bool useBlurs = !PlatformInfo.isAndroid;
Future<void> changeLocale(Locale value) async {
currentLocale.value = value.languageCode;
await localeLogic.loadIfChanged(value);
// Re-init controllers that have some cached data that is localized
wondersLogic.init();
timelineLogic.init();
}
@override
void copyFromJson(Map<String, dynamic> value) {
hasCompletedOnboarding.value = value['hasCompletedOnboarding'] ?? false;
hasDismissedSearchMessage.value = value['hasDismissedSearchMessage'] ?? false;
currentLocale.value = value['currentLocale'];
isSearchPanelOpen.value = value['isSearchPanelOpen'] ?? false;
prevWonderIndex.value = value['lastWonderIndex'];
}
@override
@ -28,14 +38,7 @@ class SettingsLogic with ThrottledSaveLoadMixin {
'hasDismissedSearchMessage': hasDismissedSearchMessage.value,
'currentLocale': currentLocale.value,
'isSearchPanelOpen': isSearchPanelOpen.value,
'lastWonderIndex': prevWonderIndex.value,
};
}
Future<void> changeLocale(Locale value) async {
currentLocale.value = value.languageCode;
await localeLogic.loadIfChanged(value);
// Re-init controllers that have some cached data that is localized
wondersLogic.init();
timelineLogic.init();
}
}

View File

@ -16,9 +16,11 @@ void main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
// Keep native splash screen up until app is finished bootstrapping
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
GoRouter.optionURLReflectsImperativeAPIs = true;
// Start app
registerSingletons();
runApp(WondersApp());
await appLogic.bootstrap();

View File

@ -1,13 +1,14 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:wonders/common_libs.dart';
import 'package:wonders/ui/common/modals//fullscreen_video_viewer.dart';
import 'package:wonders/ui/common/modals/fullscreen_maps_viewer.dart';
import 'package:wonders/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart';
import 'package:wonders/ui/screens/artifact/artifact_details/artifact_details_screen.dart';
import 'package:wonders/ui/screens/artifact/artifact_search/artifact_search_screen.dart';
import 'package:wonders/ui/screens/collection/collection_screen.dart';
import 'package:wonders/ui/screens/home/wonders_home_screen.dart';
import 'package:wonders/ui/screens/intro/intro_screen.dart';
import 'package:wonders/ui/screens/page_not_found/page_not_found.dart';
import 'package:wonders/ui/screens/timeline/timeline_screen.dart';
import 'package:wonders/ui/screens/wonder_details/wonders_details_screen.dart';
@ -17,20 +18,53 @@ class ScreenPaths {
static String intro = '/welcome';
static String home = '/home';
static String settings = '/settings';
static String wonderDetails(WonderType type, {int tabIndex = 0}) => '/wonder/${type.name}?t=$tabIndex';
static String video(String id) => '/video/$id';
static String highlights(WonderType type) => '/highlights/${type.name}';
static String search(WonderType type) => '/search/${type.name}';
static String artifact(String id) => '/artifact/$id';
static String collection(String id) => '/collection?id=$id';
static String maps(WonderType type) => '/maps/${type.name}';
static String timeline(WonderType? type) => '/timeline?type=${type?.name ?? ''}';
static String wallpaperPhoto(WonderType type) => '/wallpaperPhoto/${type.name}';
static String wonderDetails(WonderType type, {required int tabIndex}) => '$home/wonder/${type.name}?t=$tabIndex';
/// Dynamically nested pages, always added on to the existing path
static String video(String id) => _appendToCurrentPath('/video/$id');
static String search(WonderType type) => _appendToCurrentPath('/search/${type.name}');
static String maps(WonderType type) => _appendToCurrentPath('/maps/${type.name}');
static String timeline(WonderType? type) => _appendToCurrentPath('/timeline?type=${type?.name ?? ''}');
static String artifact(String id, {bool append = true}) =>
append ? _appendToCurrentPath('/artifact/$id') : '/artifact/$id';
static String collection(String id) => _appendToCurrentPath('/collection${id.isEmpty ? '' : '?id=$id'}');
static String _appendToCurrentPath(String newPath) {
final newPathUri = Uri.parse(newPath);
final currentUri = appRouter.routeInformationProvider.value.uri;
Map<String, dynamic> params = Map.of(currentUri.queryParameters);
params.addAll(newPathUri.queryParameters);
Uri? loc = Uri(path: '${currentUri.path}/${newPathUri.path}'.replaceAll('//', '/'), queryParameters: params);
return loc.toString();
}
}
// Routes that are used multiple times
AppRoute get _artifactRoute => AppRoute(
'artifact/:artifactId',
(s) => ArtifactDetailsScreen(artifactId: s.pathParameters['artifactId']!),
);
AppRoute get _timelineRoute {
return AppRoute(
'timeline',
(s) => TimelineScreen(type: _tryParseWonderType(s.uri.queryParameters['type']!)),
);
}
AppRoute get _collectionRoute {
return AppRoute(
'collection',
(s) => CollectionScreen(fromId: s.uri.queryParameters['id'] ?? ''),
routes: [_artifactRoute],
);
}
/// Routing table, matches string paths to UI Screens, optionally parses params from the paths
final appRouter = GoRouter(
redirect: _handleRedirect,
errorPageBuilder: (context, state) => MaterialPage(child: PageNotFound(state.uri.toString())),
routes: [
ShellRoute(
builder: (context, router, navigator) {
@ -38,40 +72,50 @@ final appRouter = GoRouter(
},
routes: [
AppRoute(ScreenPaths.splash, (_) => Container(color: $styles.colors.greyStrong)), // This will be hidden
AppRoute(ScreenPaths.home, (_) => HomeScreen(), routes: [
AppRoute('collection', (s) {
return CollectionScreen(fromId: s.queryParams['id'] ?? '');
}),
]),
AppRoute(ScreenPaths.intro, (_) => IntroScreen()),
AppRoute('/wonder/:type', (s) {
int tab = int.tryParse(s.queryParams['t'] ?? '') ?? 0;
return WonderDetailsScreen(
type: _parseWonderType(s.params['type']),
initialTabIndex: tab,
);
}, useFade: true),
AppRoute('/timeline', (s) {
return TimelineScreen(type: _tryParseWonderType(s.queryParams['type']!));
}),
AppRoute('/video/:id', (s) {
return FullscreenVideoViewer(id: s.params['id']!);
}),
AppRoute('/highlights/:type', (s) {
return ArtifactCarouselScreen(type: _parseWonderType(s.params['type']));
}),
AppRoute('/search/:type', (s) {
return ArtifactSearchScreen(type: _parseWonderType(s.params['type']));
}),
AppRoute('/artifact/:id', (s) {
return ArtifactDetailsScreen(artifactId: s.params['id']!);
}),
AppRoute('/collection', (s) {
return CollectionScreen(fromId: s.queryParams['id'] ?? '');
}),
AppRoute('/maps/:type', (s) {
return FullscreenMapsViewer(type: _parseWonderType(s.params['type']));
}),
AppRoute(ScreenPaths.home, (_) => HomeScreen(), routes: [
_timelineRoute,
_collectionRoute,
AppRoute(
'wonder/:detailsType',
(s) {
int tab = int.tryParse(s.uri.queryParameters['t'] ?? '') ?? 0;
return WonderDetailsScreen(
type: _parseWonderType(s.pathParameters['detailsType']),
tabIndex: tab,
);
},
useFade: true,
// Wonder sub-routes
routes: [
_timelineRoute,
_collectionRoute,
_artifactRoute,
// Youtube Video
AppRoute('video/:videoId', (s) {
return FullscreenVideoViewer(id: s.pathParameters['videoId']!);
}),
// Search
AppRoute(
'search/:searchType',
(s) {
return ArtifactSearchScreen(type: _parseWonderType(s.pathParameters['searchType']));
},
routes: [
_artifactRoute,
],
),
// Maps
AppRoute(
'maps/:mapsType',
(s) => FullscreenMapsViewer(
type: _parseWonderType(s.pathParameters['mapsType']),
)),
],
),
]),
]),
],
);
@ -103,12 +147,21 @@ class AppRoute extends GoRoute {
final bool useFade;
}
String? get initialDeeplink => _initialDeeplink;
String? _initialDeeplink;
String? _handleRedirect(BuildContext context, GoRouterState state) {
// Prevent anyone from navigating away from `/` if app is starting up.
if (!appLogic.isBootstrapComplete && state.location != ScreenPaths.splash) {
if (!appLogic.isBootstrapComplete && state.uri.path != ScreenPaths.splash) {
debugPrint('Redirecting from ${state.uri.path} to ${ScreenPaths.splash}.');
_initialDeeplink ??= state.uri.toString();
return ScreenPaths.splash;
}
debugPrint('Navigate to: ${state.location}');
if (appLogic.isBootstrapComplete && state.uri.path == ScreenPaths.splash) {
debugPrint('Redirecting from ${state.uri.path} to ${ScreenPaths.home}');
return ScreenPaths.home;
}
if (!kIsWeb) debugPrint('Navigate to: ${state.uri}');
return null; // do nothing
}

View File

@ -143,7 +143,11 @@ class BackBtn extends StatelessWidget {
if (onPressed != null) {
onPressed?.call();
} else {
Navigator.of(context).pop();
if (context.canPop()) {
context.pop();
} else {
context.go(ScreenPaths.home);
}
}
}
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:wonders/assets.dart';
class WonderousLogo extends StatelessWidget {
const WonderousLogo({super.key, this.width = 100});
final double width;
@override
Widget build(BuildContext context) => Image.asset(
ImagePaths.appLogoPlain,
fit: BoxFit.cover,
width: width,
filterQuality: FilterQuality.high,
);
}

View File

@ -34,13 +34,13 @@ class _ArtifactScreenState extends State<ArtifactCarouselScreen> {
_currentArtifactIndex.value = _wrappedPageIndex;
}
void _handleSearchTap() => context.push(ScreenPaths.search(widget.type));
void _handleSearchTap() => context.go(ScreenPaths.search(widget.type));
void _handleArtifactTap(int index) {
int delta = index - _currentPage.value.round();
if (delta == 0) {
HighlightData data = _artifacts[index % _artifacts.length];
context.push(ScreenPaths.artifact(data.artifactId));
context.go(ScreenPaths.artifact(data.artifactId));
} else {
_pageController?.animateToPage(
_currentPage.value.round() + delta,

View File

@ -65,7 +65,7 @@ class _ArtifactSearchScreenState extends State<ArtifactSearchScreen> with GetItS
_updateFilter();
}
void _handleResultPressed(SearchData o) => context.push(ScreenPaths.artifact(o.id.toString()));
void _handleResultPressed(SearchData o) => context.go(ScreenPaths.artifact(o.id.toString()));
void _handlePanelControllerChanged() {
settingsLogic.isSearchPanelOpen.value = panelController.value;

View File

@ -184,6 +184,6 @@ class CollectibleFoundScreen extends StatelessWidget {
void _handleViewCollectionPressed(BuildContext context) {
Navigator.pop(context);
context.push(ScreenPaths.collection(collectible.id));
context.go(ScreenPaths.collection(collectible.id));
}
}

View File

@ -9,7 +9,7 @@ class _CollectionListCard extends StatelessWidget with GetItMixin {
final String fromId;
void _showDetails(BuildContext context, CollectibleData collectible) {
context.push(ScreenPaths.artifact(collectible.artifactId));
context.go(ScreenPaths.artifact(collectible.artifactId));
Future.delayed(300.ms).then((_) => collectiblesLogic.setState(collectible.id, CollectibleState.explored));
}

View File

@ -63,6 +63,8 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
_scrollPos.value = _scroller.position.pixels;
}
void _handleBackPressed() => context.go(ScreenPaths.home);
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (_, constraints) {
@ -181,7 +183,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
alignment: backBtnAlign,
child: Padding(
padding: EdgeInsets.all($styles.insets.sm),
child: BackBtn(icon: AppIcons.north),
child: BackBtn(icon: AppIcons.north, onPressed: _handleBackPressed),
),
),
)

View File

@ -174,7 +174,9 @@ class _YouTubeThumbnail extends StatelessWidget {
@override
Widget build(BuildContext context) {
void handlePressed() => context.push(ScreenPaths.video(id));
// On btn pressed:
void handlePressed() => context.go(ScreenPaths.video(id));
return MergeSemantics(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 400),
@ -227,7 +229,7 @@ class _MapsThumbnailState extends State<_MapsThumbnail> {
@override
Widget build(BuildContext context) {
void handlePressed() => context.push(ScreenPaths.maps(widget.data.type));
void handlePressed() => context.go(ScreenPaths.maps(widget.data.type));
if (PlatformInfo.isDesktop) return SizedBox.shrink();
return AspectRatio(
aspectRatio: 1.65,

View File

@ -54,16 +54,20 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
@override
void initState() {
super.initState();
// Load previously saved wonderIndex if we have one
_wonderIndex = settingsLogic.prevWonderIndex.value ?? 0;
// allow 'infinite' scrolling by starting at a very high page number, add wonderIndex to start on the correct page
final initialPage = _numWonders * 100 + _wonderIndex;
// Create page controller,
// allow 'infinite' scrolling by starting at a very high page, or remember the previous value
final initialPage = _numWonders * 9999;
_pageController = PageController(viewportFraction: 1, initialPage: initialPage);
_wonderIndex = initialPage % _numWonders;
}
void _handlePageChanged(value) {
final newIndex = value % _numWonders;
if (newIndex == _wonderIndex) return; // Exit early if we're already on this page
setState(() {
_wonderIndex = value % _numWonders;
_wonderIndex = newIndex;
settingsLogic.prevWonderIndex.value = _wonderIndex;
});
AppHaptics.lightImpact();
}
@ -104,7 +108,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
void _showDetailsPage() async {
_swipeOverride = _swipeController.swipeAmt.value;
context.push(ScreenPaths.wonderDetails(currentWonder.type));
context.go(ScreenPaths.wonderDetails(currentWonder.type, tabIndex: 0));
await Future.delayed(100.ms);
_swipeOverride = null;
_fadeInOnNextBuild = true;

View File

@ -7,6 +7,7 @@ import 'package:wonders/ui/common/app_icons.dart';
import 'package:wonders/ui/common/controls/app_header.dart';
import 'package:wonders/ui/common/controls/locale_switcher.dart';
import 'package:wonders/ui/common/pop_navigator_underlay.dart';
import 'package:wonders/ui/common/wonderous_logo.dart';
import 'package:wonders/ui/screens/home_menu/about_dialog_content.dart';
class HomeMenu extends StatefulWidget {
@ -32,19 +33,14 @@ class _HomeMenuState extends State<HomeMenu> {
applicationIcon: Container(
color: $styles.colors.black,
padding: EdgeInsets.all($styles.insets.xs),
child: Image.asset(
ImagePaths.appLogoPlain,
fit: BoxFit.cover,
width: 52,
filterQuality: FilterQuality.high,
),
child: WonderousLogo(width: 52),
),
);
}
void _handleCollectionPressed(BuildContext context) => context.push(ScreenPaths.collection(''));
void _handleCollectionPressed(BuildContext context) => context.go(ScreenPaths.collection(''));
void _handleTimelinePressed(BuildContext context) => context.push(ScreenPaths.timeline(widget.data.type));
void _handleTimelinePressed(BuildContext context) => context.go(ScreenPaths.timeline(widget.data.type));
void _handleWonderPressed(BuildContext context, WonderData data) => Navigator.pop(context, data.type);

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:wonders/common_libs.dart';
import 'package:wonders/logic/common/platform_info.dart';
import 'package:wonders/ui/common/themed_text.dart';
import 'package:wonders/ui/common/wonderous_logo.dart';
class PageNotFound extends StatelessWidget {
const PageNotFound(this.url, {super.key});
final String url;
@override
Widget build(BuildContext context) {
void handleHomePressed() => context.go(ScreenPaths.home);
return Scaffold(
backgroundColor: $styles.colors.black,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
WonderousLogo(),
Gap(10),
Text(
'Wonderous',
style: $styles.text.wonderTitle.copyWith(color: $styles.colors.accent1, fontSize: 28),
),
Gap(70),
Text(
$strings.pageNotFoundMessage,
style: $styles.text.body.copyWith(color: $styles.colors.offWhite),
),
if (PlatformInfo.isDesktop) ...{
LightText(child: Text('Path: $url', style: $styles.text.bodySmall)),
},
Gap(70),
AppBtn(
minimumSize: Size(200, 0),
bgColor: $styles.colors.offWhite,
onPressed: handleHomePressed,
semanticLabel: 'Back',
child: DarkText(
child: Text(
$strings.pageNotFoundBackButton,
style: $styles.text.btn.copyWith(fontSize: 12),
),
),
)
],
),
),
);
}
}

View File

@ -6,13 +6,14 @@ class WonderDetailsTabMenu extends StatelessWidget {
static const double minTabSize = 25;
static const double maxTabSize = 100;
const WonderDetailsTabMenu(
{Key? key,
required this.tabController,
this.showBg = false,
required this.wonderType,
this.axis = Axis.horizontal})
: super(key: key);
const WonderDetailsTabMenu({
Key? key,
required this.tabController,
this.showBg = false,
required this.wonderType,
this.axis = Axis.horizontal,
required this.onTap,
}) : super(key: key);
final TabController tabController;
final bool showBg;
@ -20,6 +21,8 @@ class WonderDetailsTabMenu extends StatelessWidget {
final Axis axis;
bool get isVertical => axis == Axis.vertical;
final void Function(int index) onTap;
@override
Widget build(BuildContext context) {
Color iconColor = showBg ? $styles.colors.black : $styles.colors.white;
@ -103,6 +106,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
color: iconColor,
axis: axis,
mainAxisSize: tabBtnSize,
onTap: onTap,
),
_TabBtn(
1,
@ -112,6 +116,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
color: iconColor,
axis: axis,
mainAxisSize: tabBtnSize,
onTap: onTap,
),
_TabBtn(
2,
@ -121,6 +126,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
color: iconColor,
axis: axis,
mainAxisSize: tabBtnSize,
onTap: onTap,
),
_TabBtn(
3,
@ -130,6 +136,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
color: iconColor,
axis: axis,
mainAxisSize: tabBtnSize,
onTap: onTap,
),
]),
),
@ -155,7 +162,7 @@ class _WonderHomeBtn extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CircleBtn(
onPressed: () => Navigator.of(context).pop(),
onPressed: () => context.go(ScreenPaths.home),
bgColor: $styles.colors.white,
semanticLabel: $strings.wonderDetailsTabSemanticBack,
child: AnimatedContainer(
@ -184,6 +191,7 @@ class _TabBtn extends StatelessWidget {
required this.label,
required this.axis,
required this.mainAxisSize,
required this.onTap,
}) : super(key: key);
static const double crossBtnSize = 60;
@ -195,14 +203,12 @@ class _TabBtn extends StatelessWidget {
final String label;
final Axis axis;
final double mainAxisSize;
final void Function(int index) onTap;
bool get _isVertical => axis == Axis.vertical;
@override
Widget build(BuildContext context) {
// return _isVertical
// ? SizedBox(height: mainAxisSize, width: crossBtnSize, child: Placeholder())
// : SizedBox(height: crossBtnSize, width: mainAxisSize, child: Placeholder());
bool selected = tabController.index == index;
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png';
@ -217,7 +223,7 @@ class _TabBtn extends StatelessWidget {
label: tabLabel,
child: ExcludeSemantics(
child: AppBtn.basic(
onPressed: () => tabController.index = index,
onPressed: () => onTap(index),
semanticLabel: label,
minimumSize: _isVertical ? Size(crossBtnSize, mainAxisSize) : Size(mainAxisSize, crossBtnSize),
// Image icon

View File

@ -8,9 +8,9 @@ import 'package:wonders/ui/screens/wonder_details/wonder_details_tab_menu.dart';
import 'package:wonders/ui/screens/wonder_events/wonder_events.dart';
class WonderDetailsScreen extends StatefulWidget with GetItStatefulWidgetMixin {
WonderDetailsScreen({Key? key, required this.type, this.initialTabIndex = 0}) : super(key: key);
WonderDetailsScreen({Key? key, required this.type, this.tabIndex = 0}) : super(key: key);
final WonderType type;
final int initialTabIndex;
final int tabIndex;
@override
State<WonderDetailsScreen> createState() => _WonderDetailsScreenState();
@ -21,24 +21,39 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
late final _tabController = TabController(
length: 4,
vsync: this,
initialIndex: widget.initialTabIndex,
initialIndex: _clampIndex(widget.tabIndex),
)..addListener(_handleTabChanged);
AnimationController? _fade;
double? _tabBarSize;
bool _useNavRail = false;
@override
void didUpdateWidget(covariant WonderDetailsScreen oldWidget) {
if (oldWidget.tabIndex != widget.tabIndex) {
_tabController.index = _clampIndex(widget.tabIndex);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
int _clampIndex(int index) => index.clamp(0, 3);
void _handleTabChanged() {
_fade?.forward(from: 0);
setState(() {});
}
void _handleTabTapped(int index) {
_tabController.index = index;
context.go(ScreenPaths.wonderDetails(widget.type, tabIndex: _tabController.index));
}
void _handleTabMenuSized(Size size) {
setState(() {
_tabBarSize = (_useNavRail ? size.width : size.height) - WonderDetailsTabMenu.buttonInset;
@ -76,6 +91,7 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
onChange: _handleTabMenuSized,
child: WonderDetailsTabMenu(
tabController: _tabController,
onTap: _handleTabTapped,
wonderType: wonder.type,
showBg: showTabBarBg,
axis: _useNavRail ? Axis.vertical : Axis.horizontal),

View File

@ -7,7 +7,7 @@ class _TimelineBtn extends StatelessWidget {
@override
Widget build(BuildContext context) {
void handleBtnPressed() => context.push(ScreenPaths.timeline(type));
void handleBtnPressed() => context.go(ScreenPaths.timeline(type));
return Padding(
padding: EdgeInsets.symmetric(horizontal: $styles.insets.md),
child: SizedBox(

View File

@ -35,7 +35,7 @@ class _WonderEventsState extends State<WonderEvents> {
@override
Widget build(BuildContext context) {
void handleTimelineBtnPressed() => context.push(ScreenPaths.timeline(widget.type));
void handleTimelineBtnPressed() => context.go(ScreenPaths.timeline(widget.type));
// Main view content switches between 1 and 2 column layouts
// On mobile, use the 2 column layout on screens close to landscape (>.85). This is primarily an optimization for foldable devices which have square-ish dimensions when opened.
final twoColumnAspect = PlatformInfo.isMobile ? .85 : 1;

View File

@ -292,10 +292,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: bd7e671d26fd39c78cba82070fa34ef1f830b0e7ed1aeebccabc6561302a7ee5
sha256: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a"
url: "https://pub.dev"
source: hosted
version: "6.5.9"
version: "13.0.1"
google_maps:
dependency: transitive
description:

View File

@ -31,7 +31,7 @@ dependencies:
get_it_mixin: ^4.2.2
google_maps_flutter: ^2.5.3
google_maps_flutter_web: ^0.5.4+3
go_router: ^6.5.5
go_router: ^13.0.1
home_widget: ^0.3.0
http: ^1.1.0
image: ^4.1.3