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",
|
"timelineEvent1969ce": "Apollo 11 mission lands on the moon",
|
||||||
"privacyPolicy": "Privacy Policy",
|
"privacyPolicy": "Privacy Policy",
|
||||||
"privacyStatement": "As explained in our {privacyUrl} we do not collect any personal information.",
|
"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号",
|
"timelineEvent1957ce": "苏联发射斯普特尼克1号",
|
||||||
"timelineEvent1969ce": "阿波罗11号在月球着陆",
|
"timelineEvent1969ce": "阿波罗11号在月球着陆",
|
||||||
"privacyPolicy": "隐私政策",
|
"privacyPolicy": "隐私政策",
|
||||||
"privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉,gskinner 不会收集您的个人信息。"
|
"privacyStatement": "gskinner 非常重视对用户隐私的保护,正如{privacyUrl}里所诉,gskinner 不会收集您的个人信息。",
|
||||||
|
"pageNotFoundBackButton": "回到文明",
|
||||||
|
"pageNotFoundMessage": "您正在寻找的页面不存在"
|
||||||
}
|
}
|
@ -81,7 +81,7 @@ class AppLogic {
|
|||||||
if (showIntro) {
|
if (showIntro) {
|
||||||
appRouter.go(ScreenPaths.intro);
|
appRouter.go(ScreenPaths.intro);
|
||||||
} else {
|
} else {
|
||||||
appRouter.go(ScreenPaths.home);
|
appRouter.go(initialDeeplink ?? ScreenPaths.home);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ mixin ThrottledSaveLoadMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> save() async {
|
Future<void> save() async {
|
||||||
debugPrint('Saving...');
|
if (!kIsWeb) debugPrint('Saving...');
|
||||||
try {
|
try {
|
||||||
await _file.save(toJson());
|
await _file.save(toJson());
|
||||||
} on Exception catch (e) {
|
} on Exception catch (e) {
|
||||||
|
@ -10,15 +10,25 @@ class SettingsLogic with ThrottledSaveLoadMixin {
|
|||||||
late final hasDismissedSearchMessage = ValueNotifier<bool>(false)..addListener(scheduleSave);
|
late final hasDismissedSearchMessage = ValueNotifier<bool>(false)..addListener(scheduleSave);
|
||||||
late final isSearchPanelOpen = ValueNotifier<bool>(true)..addListener(scheduleSave);
|
late final isSearchPanelOpen = ValueNotifier<bool>(true)..addListener(scheduleSave);
|
||||||
late final currentLocale = ValueNotifier<String?>(null)..addListener(scheduleSave);
|
late final currentLocale = ValueNotifier<String?>(null)..addListener(scheduleSave);
|
||||||
|
late final prevWonderIndex = ValueNotifier<int?>(null)..addListener(scheduleSave);
|
||||||
|
|
||||||
final bool useBlurs = !PlatformInfo.isAndroid;
|
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
|
@override
|
||||||
void copyFromJson(Map<String, dynamic> value) {
|
void copyFromJson(Map<String, dynamic> value) {
|
||||||
hasCompletedOnboarding.value = value['hasCompletedOnboarding'] ?? false;
|
hasCompletedOnboarding.value = value['hasCompletedOnboarding'] ?? false;
|
||||||
hasDismissedSearchMessage.value = value['hasDismissedSearchMessage'] ?? false;
|
hasDismissedSearchMessage.value = value['hasDismissedSearchMessage'] ?? false;
|
||||||
currentLocale.value = value['currentLocale'];
|
currentLocale.value = value['currentLocale'];
|
||||||
isSearchPanelOpen.value = value['isSearchPanelOpen'] ?? false;
|
isSearchPanelOpen.value = value['isSearchPanelOpen'] ?? false;
|
||||||
|
prevWonderIndex.value = value['lastWonderIndex'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -28,14 +38,7 @@ class SettingsLogic with ThrottledSaveLoadMixin {
|
|||||||
'hasDismissedSearchMessage': hasDismissedSearchMessage.value,
|
'hasDismissedSearchMessage': hasDismissedSearchMessage.value,
|
||||||
'currentLocale': currentLocale.value,
|
'currentLocale': currentLocale.value,
|
||||||
'isSearchPanelOpen': isSearchPanelOpen.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();
|
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||||
// Keep native splash screen up until app is finished bootstrapping
|
// Keep native splash screen up until app is finished bootstrapping
|
||||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||||
|
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||||
|
|
||||||
// Start app
|
// Start app
|
||||||
registerSingletons();
|
registerSingletons();
|
||||||
|
|
||||||
runApp(WondersApp());
|
runApp(WondersApp());
|
||||||
await appLogic.bootstrap();
|
await appLogic.bootstrap();
|
||||||
|
|
||||||
|
143
lib/router.dart
143
lib/router.dart
@ -1,13 +1,14 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:wonders/common_libs.dart';
|
import 'package:wonders/common_libs.dart';
|
||||||
import 'package:wonders/ui/common/modals//fullscreen_video_viewer.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/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_details/artifact_details_screen.dart';
|
||||||
import 'package:wonders/ui/screens/artifact/artifact_search/artifact_search_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/collection/collection_screen.dart';
|
||||||
import 'package:wonders/ui/screens/home/wonders_home_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/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/timeline/timeline_screen.dart';
|
||||||
import 'package:wonders/ui/screens/wonder_details/wonders_details_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 intro = '/welcome';
|
||||||
static String home = '/home';
|
static String home = '/home';
|
||||||
static String settings = '/settings';
|
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 wonderDetails(WonderType type, {required int tabIndex}) => '$home/wonder/${type.name}?t=$tabIndex';
|
||||||
static String highlights(WonderType type) => '/highlights/${type.name}';
|
|
||||||
static String search(WonderType type) => '/search/${type.name}';
|
/// Dynamically nested pages, always added on to the existing path
|
||||||
static String artifact(String id) => '/artifact/$id';
|
static String video(String id) => _appendToCurrentPath('/video/$id');
|
||||||
static String collection(String id) => '/collection?id=$id';
|
static String search(WonderType type) => _appendToCurrentPath('/search/${type.name}');
|
||||||
static String maps(WonderType type) => '/maps/${type.name}';
|
static String maps(WonderType type) => _appendToCurrentPath('/maps/${type.name}');
|
||||||
static String timeline(WonderType? type) => '/timeline?type=${type?.name ?? ''}';
|
static String timeline(WonderType? type) => _appendToCurrentPath('/timeline?type=${type?.name ?? ''}');
|
||||||
static String wallpaperPhoto(WonderType type) => '/wallpaperPhoto/${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
|
/// Routing table, matches string paths to UI Screens, optionally parses params from the paths
|
||||||
final appRouter = GoRouter(
|
final appRouter = GoRouter(
|
||||||
redirect: _handleRedirect,
|
redirect: _handleRedirect,
|
||||||
|
errorPageBuilder: (context, state) => MaterialPage(child: PageNotFound(state.uri.toString())),
|
||||||
routes: [
|
routes: [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
builder: (context, router, navigator) {
|
builder: (context, router, navigator) {
|
||||||
@ -38,40 +72,50 @@ final appRouter = GoRouter(
|
|||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
AppRoute(ScreenPaths.splash, (_) => Container(color: $styles.colors.greyStrong)), // This will be hidden
|
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(ScreenPaths.intro, (_) => IntroScreen()),
|
||||||
AppRoute('/wonder/:type', (s) {
|
AppRoute(ScreenPaths.home, (_) => HomeScreen(), routes: [
|
||||||
int tab = int.tryParse(s.queryParams['t'] ?? '') ?? 0;
|
_timelineRoute,
|
||||||
return WonderDetailsScreen(
|
_collectionRoute,
|
||||||
type: _parseWonderType(s.params['type']),
|
AppRoute(
|
||||||
initialTabIndex: tab,
|
'wonder/:detailsType',
|
||||||
);
|
(s) {
|
||||||
}, useFade: true),
|
int tab = int.tryParse(s.uri.queryParameters['t'] ?? '') ?? 0;
|
||||||
AppRoute('/timeline', (s) {
|
return WonderDetailsScreen(
|
||||||
return TimelineScreen(type: _tryParseWonderType(s.queryParams['type']!));
|
type: _parseWonderType(s.pathParameters['detailsType']),
|
||||||
}),
|
tabIndex: tab,
|
||||||
AppRoute('/video/:id', (s) {
|
);
|
||||||
return FullscreenVideoViewer(id: s.params['id']!);
|
},
|
||||||
}),
|
useFade: true,
|
||||||
AppRoute('/highlights/:type', (s) {
|
// Wonder sub-routes
|
||||||
return ArtifactCarouselScreen(type: _parseWonderType(s.params['type']));
|
routes: [
|
||||||
}),
|
_timelineRoute,
|
||||||
AppRoute('/search/:type', (s) {
|
_collectionRoute,
|
||||||
return ArtifactSearchScreen(type: _parseWonderType(s.params['type']));
|
_artifactRoute,
|
||||||
}),
|
// Youtube Video
|
||||||
AppRoute('/artifact/:id', (s) {
|
AppRoute('video/:videoId', (s) {
|
||||||
return ArtifactDetailsScreen(artifactId: s.params['id']!);
|
return FullscreenVideoViewer(id: s.pathParameters['videoId']!);
|
||||||
}),
|
}),
|
||||||
AppRoute('/collection', (s) {
|
|
||||||
return CollectionScreen(fromId: s.queryParams['id'] ?? '');
|
// Search
|
||||||
}),
|
AppRoute(
|
||||||
AppRoute('/maps/:type', (s) {
|
'search/:searchType',
|
||||||
return FullscreenMapsViewer(type: _parseWonderType(s.params['type']));
|
(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;
|
final bool useFade;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? get initialDeeplink => _initialDeeplink;
|
||||||
|
String? _initialDeeplink;
|
||||||
|
|
||||||
String? _handleRedirect(BuildContext context, GoRouterState state) {
|
String? _handleRedirect(BuildContext context, GoRouterState state) {
|
||||||
// Prevent anyone from navigating away from `/` if app is starting up.
|
// 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;
|
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
|
return null; // do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +143,11 @@ class BackBtn extends StatelessWidget {
|
|||||||
if (onPressed != null) {
|
if (onPressed != null) {
|
||||||
onPressed?.call();
|
onPressed?.call();
|
||||||
} else {
|
} 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;
|
_currentArtifactIndex.value = _wrappedPageIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSearchTap() => context.push(ScreenPaths.search(widget.type));
|
void _handleSearchTap() => context.go(ScreenPaths.search(widget.type));
|
||||||
|
|
||||||
void _handleArtifactTap(int index) {
|
void _handleArtifactTap(int index) {
|
||||||
int delta = index - _currentPage.value.round();
|
int delta = index - _currentPage.value.round();
|
||||||
if (delta == 0) {
|
if (delta == 0) {
|
||||||
HighlightData data = _artifacts[index % _artifacts.length];
|
HighlightData data = _artifacts[index % _artifacts.length];
|
||||||
context.push(ScreenPaths.artifact(data.artifactId));
|
context.go(ScreenPaths.artifact(data.artifactId));
|
||||||
} else {
|
} else {
|
||||||
_pageController?.animateToPage(
|
_pageController?.animateToPage(
|
||||||
_currentPage.value.round() + delta,
|
_currentPage.value.round() + delta,
|
||||||
|
@ -65,7 +65,7 @@ class _ArtifactSearchScreenState extends State<ArtifactSearchScreen> with GetItS
|
|||||||
_updateFilter();
|
_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() {
|
void _handlePanelControllerChanged() {
|
||||||
settingsLogic.isSearchPanelOpen.value = panelController.value;
|
settingsLogic.isSearchPanelOpen.value = panelController.value;
|
||||||
|
@ -184,6 +184,6 @@ class CollectibleFoundScreen extends StatelessWidget {
|
|||||||
|
|
||||||
void _handleViewCollectionPressed(BuildContext context) {
|
void _handleViewCollectionPressed(BuildContext context) {
|
||||||
Navigator.pop(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;
|
final String fromId;
|
||||||
|
|
||||||
void _showDetails(BuildContext context, CollectibleData collectible) {
|
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));
|
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;
|
_scrollPos.value = _scroller.position.pixels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleBackPressed() => context.go(ScreenPaths.home);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(builder: (_, constraints) {
|
return LayoutBuilder(builder: (_, constraints) {
|
||||||
@ -181,7 +183,7 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
|
|||||||
alignment: backBtnAlign,
|
alignment: backBtnAlign,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all($styles.insets.sm),
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
void handlePressed() => context.push(ScreenPaths.video(id));
|
// On btn pressed:
|
||||||
|
void handlePressed() => context.go(ScreenPaths.video(id));
|
||||||
|
|
||||||
return MergeSemantics(
|
return MergeSemantics(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: 400),
|
constraints: BoxConstraints(maxWidth: 400),
|
||||||
@ -227,7 +229,7 @@ class _MapsThumbnailState extends State<_MapsThumbnail> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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();
|
if (PlatformInfo.isDesktop) return SizedBox.shrink();
|
||||||
return AspectRatio(
|
return AspectRatio(
|
||||||
aspectRatio: 1.65,
|
aspectRatio: 1.65,
|
||||||
|
@ -54,16 +54,20 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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,
|
// 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);
|
_pageController = PageController(viewportFraction: 1, initialPage: initialPage);
|
||||||
_wonderIndex = initialPage % _numWonders;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handlePageChanged(value) {
|
void _handlePageChanged(value) {
|
||||||
|
final newIndex = value % _numWonders;
|
||||||
|
if (newIndex == _wonderIndex) return; // Exit early if we're already on this page
|
||||||
setState(() {
|
setState(() {
|
||||||
_wonderIndex = value % _numWonders;
|
_wonderIndex = newIndex;
|
||||||
|
settingsLogic.prevWonderIndex.value = _wonderIndex;
|
||||||
});
|
});
|
||||||
AppHaptics.lightImpact();
|
AppHaptics.lightImpact();
|
||||||
}
|
}
|
||||||
@ -104,7 +108,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
|
|
||||||
void _showDetailsPage() async {
|
void _showDetailsPage() async {
|
||||||
_swipeOverride = _swipeController.swipeAmt.value;
|
_swipeOverride = _swipeController.swipeAmt.value;
|
||||||
context.push(ScreenPaths.wonderDetails(currentWonder.type));
|
context.go(ScreenPaths.wonderDetails(currentWonder.type, tabIndex: 0));
|
||||||
await Future.delayed(100.ms);
|
await Future.delayed(100.ms);
|
||||||
_swipeOverride = null;
|
_swipeOverride = null;
|
||||||
_fadeInOnNextBuild = true;
|
_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/app_header.dart';
|
||||||
import 'package:wonders/ui/common/controls/locale_switcher.dart';
|
import 'package:wonders/ui/common/controls/locale_switcher.dart';
|
||||||
import 'package:wonders/ui/common/pop_navigator_underlay.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';
|
import 'package:wonders/ui/screens/home_menu/about_dialog_content.dart';
|
||||||
|
|
||||||
class HomeMenu extends StatefulWidget {
|
class HomeMenu extends StatefulWidget {
|
||||||
@ -32,19 +33,14 @@ class _HomeMenuState extends State<HomeMenu> {
|
|||||||
applicationIcon: Container(
|
applicationIcon: Container(
|
||||||
color: $styles.colors.black,
|
color: $styles.colors.black,
|
||||||
padding: EdgeInsets.all($styles.insets.xs),
|
padding: EdgeInsets.all($styles.insets.xs),
|
||||||
child: Image.asset(
|
child: WonderousLogo(width: 52),
|
||||||
ImagePaths.appLogoPlain,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
width: 52,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
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 minTabSize = 25;
|
||||||
static const double maxTabSize = 100;
|
static const double maxTabSize = 100;
|
||||||
|
|
||||||
const WonderDetailsTabMenu(
|
const WonderDetailsTabMenu({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.tabController,
|
required this.tabController,
|
||||||
this.showBg = false,
|
this.showBg = false,
|
||||||
required this.wonderType,
|
required this.wonderType,
|
||||||
this.axis = Axis.horizontal})
|
this.axis = Axis.horizontal,
|
||||||
: super(key: key);
|
required this.onTap,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final TabController tabController;
|
final TabController tabController;
|
||||||
final bool showBg;
|
final bool showBg;
|
||||||
@ -20,6 +21,8 @@ class WonderDetailsTabMenu extends StatelessWidget {
|
|||||||
final Axis axis;
|
final Axis axis;
|
||||||
bool get isVertical => axis == Axis.vertical;
|
bool get isVertical => axis == Axis.vertical;
|
||||||
|
|
||||||
|
final void Function(int index) onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color iconColor = showBg ? $styles.colors.black : $styles.colors.white;
|
Color iconColor = showBg ? $styles.colors.black : $styles.colors.white;
|
||||||
@ -103,6 +106,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
|
|||||||
color: iconColor,
|
color: iconColor,
|
||||||
axis: axis,
|
axis: axis,
|
||||||
mainAxisSize: tabBtnSize,
|
mainAxisSize: tabBtnSize,
|
||||||
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
_TabBtn(
|
_TabBtn(
|
||||||
1,
|
1,
|
||||||
@ -112,6 +116,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
|
|||||||
color: iconColor,
|
color: iconColor,
|
||||||
axis: axis,
|
axis: axis,
|
||||||
mainAxisSize: tabBtnSize,
|
mainAxisSize: tabBtnSize,
|
||||||
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
_TabBtn(
|
_TabBtn(
|
||||||
2,
|
2,
|
||||||
@ -121,6 +126,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
|
|||||||
color: iconColor,
|
color: iconColor,
|
||||||
axis: axis,
|
axis: axis,
|
||||||
mainAxisSize: tabBtnSize,
|
mainAxisSize: tabBtnSize,
|
||||||
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
_TabBtn(
|
_TabBtn(
|
||||||
3,
|
3,
|
||||||
@ -130,6 +136,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
|
|||||||
color: iconColor,
|
color: iconColor,
|
||||||
axis: axis,
|
axis: axis,
|
||||||
mainAxisSize: tabBtnSize,
|
mainAxisSize: tabBtnSize,
|
||||||
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
@ -155,7 +162,7 @@ class _WonderHomeBtn extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CircleBtn(
|
return CircleBtn(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => context.go(ScreenPaths.home),
|
||||||
bgColor: $styles.colors.white,
|
bgColor: $styles.colors.white,
|
||||||
semanticLabel: $strings.wonderDetailsTabSemanticBack,
|
semanticLabel: $strings.wonderDetailsTabSemanticBack,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
@ -184,6 +191,7 @@ class _TabBtn extends StatelessWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
required this.axis,
|
required this.axis,
|
||||||
required this.mainAxisSize,
|
required this.mainAxisSize,
|
||||||
|
required this.onTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
static const double crossBtnSize = 60;
|
static const double crossBtnSize = 60;
|
||||||
@ -195,14 +203,12 @@ class _TabBtn extends StatelessWidget {
|
|||||||
final String label;
|
final String label;
|
||||||
final Axis axis;
|
final Axis axis;
|
||||||
final double mainAxisSize;
|
final double mainAxisSize;
|
||||||
|
final void Function(int index) onTap;
|
||||||
|
|
||||||
bool get _isVertical => axis == Axis.vertical;
|
bool get _isVertical => axis == Axis.vertical;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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;
|
bool selected = tabController.index == index;
|
||||||
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
|
||||||
final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png';
|
final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png';
|
||||||
@ -217,7 +223,7 @@ class _TabBtn extends StatelessWidget {
|
|||||||
label: tabLabel,
|
label: tabLabel,
|
||||||
child: ExcludeSemantics(
|
child: ExcludeSemantics(
|
||||||
child: AppBtn.basic(
|
child: AppBtn.basic(
|
||||||
onPressed: () => tabController.index = index,
|
onPressed: () => onTap(index),
|
||||||
semanticLabel: label,
|
semanticLabel: label,
|
||||||
minimumSize: _isVertical ? Size(crossBtnSize, mainAxisSize) : Size(mainAxisSize, crossBtnSize),
|
minimumSize: _isVertical ? Size(crossBtnSize, mainAxisSize) : Size(mainAxisSize, crossBtnSize),
|
||||||
// Image icon
|
// 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';
|
import 'package:wonders/ui/screens/wonder_events/wonder_events.dart';
|
||||||
|
|
||||||
class WonderDetailsScreen extends StatefulWidget with GetItStatefulWidgetMixin {
|
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 WonderType type;
|
||||||
final int initialTabIndex;
|
final int tabIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WonderDetailsScreen> createState() => _WonderDetailsScreenState();
|
State<WonderDetailsScreen> createState() => _WonderDetailsScreenState();
|
||||||
@ -21,24 +21,39 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
|
|||||||
late final _tabController = TabController(
|
late final _tabController = TabController(
|
||||||
length: 4,
|
length: 4,
|
||||||
vsync: this,
|
vsync: this,
|
||||||
initialIndex: widget.initialTabIndex,
|
initialIndex: _clampIndex(widget.tabIndex),
|
||||||
)..addListener(_handleTabChanged);
|
)..addListener(_handleTabChanged);
|
||||||
AnimationController? _fade;
|
AnimationController? _fade;
|
||||||
|
|
||||||
double? _tabBarSize;
|
double? _tabBarSize;
|
||||||
bool _useNavRail = false;
|
bool _useNavRail = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant WonderDetailsScreen oldWidget) {
|
||||||
|
if (oldWidget.tabIndex != widget.tabIndex) {
|
||||||
|
_tabController.index = _clampIndex(widget.tabIndex);
|
||||||
|
}
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _clampIndex(int index) => index.clamp(0, 3);
|
||||||
|
|
||||||
void _handleTabChanged() {
|
void _handleTabChanged() {
|
||||||
_fade?.forward(from: 0);
|
_fade?.forward(from: 0);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleTabTapped(int index) {
|
||||||
|
_tabController.index = index;
|
||||||
|
context.go(ScreenPaths.wonderDetails(widget.type, tabIndex: _tabController.index));
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTabMenuSized(Size size) {
|
void _handleTabMenuSized(Size size) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_tabBarSize = (_useNavRail ? size.width : size.height) - WonderDetailsTabMenu.buttonInset;
|
_tabBarSize = (_useNavRail ? size.width : size.height) - WonderDetailsTabMenu.buttonInset;
|
||||||
@ -76,6 +91,7 @@ class _WonderDetailsScreenState extends State<WonderDetailsScreen>
|
|||||||
onChange: _handleTabMenuSized,
|
onChange: _handleTabMenuSized,
|
||||||
child: WonderDetailsTabMenu(
|
child: WonderDetailsTabMenu(
|
||||||
tabController: _tabController,
|
tabController: _tabController,
|
||||||
|
onTap: _handleTabTapped,
|
||||||
wonderType: wonder.type,
|
wonderType: wonder.type,
|
||||||
showBg: showTabBarBg,
|
showBg: showTabBarBg,
|
||||||
axis: _useNavRail ? Axis.vertical : Axis.horizontal),
|
axis: _useNavRail ? Axis.vertical : Axis.horizontal),
|
||||||
|
@ -7,7 +7,7 @@ class _TimelineBtn extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
void handleBtnPressed() => context.push(ScreenPaths.timeline(type));
|
void handleBtnPressed() => context.go(ScreenPaths.timeline(type));
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: $styles.insets.md),
|
padding: EdgeInsets.symmetric(horizontal: $styles.insets.md),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
@ -35,7 +35,7 @@ class _WonderEventsState extends State<WonderEvents> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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
|
// 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.
|
// 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;
|
final twoColumnAspect = PlatformInfo.isMobile ? .85 : 1;
|
||||||
|
@ -292,10 +292,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: bd7e671d26fd39c78cba82070fa34ef1f830b0e7ed1aeebccabc6561302a7ee5
|
sha256: "3b40e751eaaa855179b416974d59d29669e750d2e50fcdb2b37f1cb0ca8c803a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.5.9"
|
version: "13.0.1"
|
||||||
google_maps:
|
google_maps:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -31,7 +31,7 @@ dependencies:
|
|||||||
get_it_mixin: ^4.2.2
|
get_it_mixin: ^4.2.2
|
||||||
google_maps_flutter: ^2.5.3
|
google_maps_flutter: ^2.5.3
|
||||||
google_maps_flutter_web: ^0.5.4+3
|
google_maps_flutter_web: ^0.5.4+3
|
||||||
go_router: ^6.5.5
|
go_router: ^13.0.1
|
||||||
home_widget: ^0.3.0
|
home_widget: ^0.3.0
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
image: ^4.1.3
|
image: ^4.1.3
|
||||||
|
Reference in New Issue
Block a user