mirror of
https://github.com/gskinnerTeam/flutter-wonderous-app.git
synced 2025-08-06 18:24:29 +08:00
Merge pull request #159 from gskinnerTeam/feature/routing_fixes
Feature/routing fixes
This commit is contained in:
@ -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."
|
||||
}
|
@ -405,5 +405,7 @@
|
||||
"timelineEvent1957ce": "苏联发射斯普特尼克1号",
|
||||
"timelineEvent1969ce": "阿波罗11号在月球着陆",
|
||||
"privacyPolicy": "隐私政策",
|
||||
"privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉,gskinner 不会收集您的个人信息。"
|
||||
"privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉,gskinner 不会收集您的个人信息。",
|
||||
"pageNotFoundBackButton": "回到文明",
|
||||
"pageNotFoundMessage": "您正在寻找的页面不存在"
|
||||
}
|
@ -81,7 +81,7 @@ class AppLogic {
|
||||
if (showIntro) {
|
||||
appRouter.go(ScreenPaths.intro);
|
||||
} else {
|
||||
appRouter.go(ScreenPaths.home);
|
||||
appRouter.go(initialDeeplink ?? ScreenPaths.home);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
143
lib/router.dart
143
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<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
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
lib/ui/common/wonderous_logo.dart
Normal file
16
lib/ui/common/wonderous_logo.dart
Normal 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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
54
lib/ui/screens/page_not_found/page_not_found.dart
Normal file
54
lib/ui/screens/page_not_found/page_not_found.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user