diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fad02fb3..e8be0a2e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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." } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 9f33749c..e970ed12 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -405,5 +405,7 @@ "timelineEvent1957ce": "苏联发射斯普特尼克1号", "timelineEvent1969ce": "阿波罗11号在月球着陆", "privacyPolicy": "隐私政策", - "privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉,gskinner 不会收集您的个人信息。" + "privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉,gskinner 不会收集您的个人信息。", + "pageNotFoundBackButton": "回到文明", + "pageNotFoundMessage": "您正在寻找的页面不存在" } \ No newline at end of file diff --git a/lib/logic/app_logic.dart b/lib/logic/app_logic.dart index 5b0878cd..100b0b5c 100644 --- a/lib/logic/app_logic.dart +++ b/lib/logic/app_logic.dart @@ -81,7 +81,7 @@ class AppLogic { if (showIntro) { appRouter.go(ScreenPaths.intro); } else { - appRouter.go(ScreenPaths.home); + appRouter.go(initialDeeplink ?? ScreenPaths.home); } } diff --git a/lib/logic/common/save_load_mixin.dart b/lib/logic/common/save_load_mixin.dart index affd5f25..3fe5dcd5 100644 --- a/lib/logic/common/save_load_mixin.dart +++ b/lib/logic/common/save_load_mixin.dart @@ -16,7 +16,7 @@ mixin ThrottledSaveLoadMixin { } Future save() async { - debugPrint('Saving...'); + if (!kIsWeb) debugPrint('Saving...'); try { await _file.save(toJson()); } on Exception catch (e) { diff --git a/lib/logic/settings_logic.dart b/lib/logic/settings_logic.dart index 9af3321e..8ece523b 100644 --- a/lib/logic/settings_logic.dart +++ b/lib/logic/settings_logic.dart @@ -10,15 +10,25 @@ class SettingsLogic with ThrottledSaveLoadMixin { late final hasDismissedSearchMessage = ValueNotifier(false)..addListener(scheduleSave); late final isSearchPanelOpen = ValueNotifier(true)..addListener(scheduleSave); late final currentLocale = ValueNotifier(null)..addListener(scheduleSave); + late final prevWonderIndex = ValueNotifier(null)..addListener(scheduleSave); final bool useBlurs = !PlatformInfo.isAndroid; + Future 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 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 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(); - } } diff --git a/lib/main.dart b/lib/main.dart index e486080e..cc784bf4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(); diff --git a/lib/router.dart b/lib/router.dart index bee38ac3..460f79ef 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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 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 } diff --git a/lib/ui/common/controls/circle_buttons.dart b/lib/ui/common/controls/circle_buttons.dart index e25c358d..ef80916d 100644 --- a/lib/ui/common/controls/circle_buttons.dart +++ b/lib/ui/common/controls/circle_buttons.dart @@ -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); + } } } } diff --git a/lib/ui/common/wonderous_logo.dart b/lib/ui/common/wonderous_logo.dart new file mode 100644 index 00000000..fb88e86e --- /dev/null +++ b/lib/ui/common/wonderous_logo.dart @@ -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, + ); +} diff --git a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart index 14583f32..b45c3572 100644 --- a/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart +++ b/lib/ui/screens/artifact/artifact_carousel/artifact_carousel_screen.dart @@ -34,13 +34,13 @@ class _ArtifactScreenState extends State { _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, diff --git a/lib/ui/screens/artifact/artifact_search/artifact_search_screen.dart b/lib/ui/screens/artifact/artifact_search/artifact_search_screen.dart index b67d6187..61789a81 100644 --- a/lib/ui/screens/artifact/artifact_search/artifact_search_screen.dart +++ b/lib/ui/screens/artifact/artifact_search/artifact_search_screen.dart @@ -65,7 +65,7 @@ class _ArtifactSearchScreenState extends State 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; diff --git a/lib/ui/screens/collectible_found/collectible_found_screen.dart b/lib/ui/screens/collectible_found/collectible_found_screen.dart index 3a968667..57d6a1ae 100644 --- a/lib/ui/screens/collectible_found/collectible_found_screen.dart +++ b/lib/ui/screens/collectible_found/collectible_found_screen.dart @@ -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)); } } diff --git a/lib/ui/screens/collection/widgets/_collection_list_card.dart b/lib/ui/screens/collection/widgets/_collection_list_card.dart index a1a94eca..65192012 100644 --- a/lib/ui/screens/collection/widgets/_collection_list_card.dart +++ b/lib/ui/screens/collection/widgets/_collection_list_card.dart @@ -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)); } diff --git a/lib/ui/screens/editorial/editorial_screen.dart b/lib/ui/screens/editorial/editorial_screen.dart index 6c746da1..9a9e5956 100644 --- a/lib/ui/screens/editorial/editorial_screen.dart +++ b/lib/ui/screens/editorial/editorial_screen.dart @@ -63,6 +63,8 @@ class _WonderEditorialScreenState extends State { _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 { alignment: backBtnAlign, child: Padding( padding: EdgeInsets.all($styles.insets.sm), - child: BackBtn(icon: AppIcons.north), + child: BackBtn(icon: AppIcons.north, onPressed: _handleBackPressed), ), ), ) diff --git a/lib/ui/screens/editorial/widgets/_scrolling_content.dart b/lib/ui/screens/editorial/widgets/_scrolling_content.dart index db6e386b..90a8f6b8 100644 --- a/lib/ui/screens/editorial/widgets/_scrolling_content.dart +++ b/lib/ui/screens/editorial/widgets/_scrolling_content.dart @@ -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, diff --git a/lib/ui/screens/home/wonders_home_screen.dart b/lib/ui/screens/home/wonders_home_screen.dart index 8d7e6dcf..8c61853b 100644 --- a/lib/ui/screens/home/wonders_home_screen.dart +++ b/lib/ui/screens/home/wonders_home_screen.dart @@ -54,16 +54,20 @@ class _HomeScreenState extends State 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 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; diff --git a/lib/ui/screens/home_menu/home_menu.dart b/lib/ui/screens/home_menu/home_menu.dart index fdce4781..bccca5d2 100644 --- a/lib/ui/screens/home_menu/home_menu.dart +++ b/lib/ui/screens/home_menu/home_menu.dart @@ -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 { 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); diff --git a/lib/ui/screens/page_not_found/page_not_found.dart b/lib/ui/screens/page_not_found/page_not_found.dart new file mode 100644 index 00000000..29a1ec90 --- /dev/null +++ b/lib/ui/screens/page_not_found/page_not_found.dart @@ -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), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart b/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart index 466954f3..2bf1df70 100644 --- a/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart +++ b/lib/ui/screens/wonder_details/wonder_details_tab_menu.dart @@ -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 diff --git a/lib/ui/screens/wonder_details/wonders_details_screen.dart b/lib/ui/screens/wonder_details/wonders_details_screen.dart index 86b16895..d885bb99 100644 --- a/lib/ui/screens/wonder_details/wonders_details_screen.dart +++ b/lib/ui/screens/wonder_details/wonders_details_screen.dart @@ -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 createState() => _WonderDetailsScreenState(); @@ -21,24 +21,39 @@ class _WonderDetailsScreenState extends State 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 onChange: _handleTabMenuSized, child: WonderDetailsTabMenu( tabController: _tabController, + onTap: _handleTabTapped, wonderType: wonder.type, showBg: showTabBarBg, axis: _useNavRail ? Axis.vertical : Axis.horizontal), diff --git a/lib/ui/screens/wonder_events/widgets/_timeline_btn.dart b/lib/ui/screens/wonder_events/widgets/_timeline_btn.dart index 5bfc4805..1045f711 100644 --- a/lib/ui/screens/wonder_events/widgets/_timeline_btn.dart +++ b/lib/ui/screens/wonder_events/widgets/_timeline_btn.dart @@ -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( diff --git a/lib/ui/screens/wonder_events/wonder_events.dart b/lib/ui/screens/wonder_events/wonder_events.dart index 95db3609..960bd0ea 100644 --- a/lib/ui/screens/wonder_events/wonder_events.dart +++ b/lib/ui/screens/wonder_events/wonder_events.dart @@ -35,7 +35,7 @@ class _WonderEventsState extends State { @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; diff --git a/pubspec.lock b/pubspec.lock index 98666183..d57db6d4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 3054a346..e0d9bb18 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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