Files
2023-12-27 16:10:09 +08:00

687 lines
22 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';
import 'package:provider/provider.dart';
import '/backend/schema/enums/enums.dart';
import '/auth/custom_auth/custom_auth_user_provider.dart';
import '/index.dart';
import '/main.dart';
import '/flutterlib/flutter_theme.dart';
import '/flutterlib/lat_lng.dart';
import '/flutterlib/place.dart';
import '/flutterlib/flutter_util.dart';
import 'serialization_util.dart';
export 'package:go_router/go_router.dart';
export 'serialization_util.dart';
const kTransitionInfoKey = '__transition_info__';
class AppStateNotifier extends ChangeNotifier {
AppStateNotifier._();
static AppStateNotifier? _instance;
static AppStateNotifier get instance => _instance ??= AppStateNotifier._();
NumstationAuthUser? initialUser;
NumstationAuthUser? user;
bool showSplashImage = true;
String? _redirectLocation;
/// Determines whether the app will refresh and build again when a sign
/// in or sign out happens. This is useful when the app is launched or
/// on an unexpected logout. However, this must be turned off when we
/// intend to sign in/out and then navigate or perform any actions after.
/// Otherwise, this will trigger a refresh and interrupt the action(s).
bool notifyOnAuthChange = true;
bool get loading => user == null || showSplashImage;
bool get loggedIn => user?.loggedIn ?? false;
bool get initiallyLoggedIn => initialUser?.loggedIn ?? false;
bool get shouldRedirect => loggedIn && _redirectLocation != null;
String getRedirectLocation() => _redirectLocation!;
bool hasRedirect() => _redirectLocation != null;
void setRedirectLocationIfUnset(String loc) => _redirectLocation ??= loc;
void clearRedirectLocation() => _redirectLocation = null;
/// Mark as not needing to notify on a sign in / out when we intend
/// to perform subsequent actions (such as navigation) afterwards.
void updateNotifyOnAuthChange(bool notify) => notifyOnAuthChange = notify;
void update(NumstationAuthUser newUser) {
final shouldUpdate =
user?.uid == null || newUser.uid == null || user?.uid != newUser.uid;
initialUser ??= newUser;
user = newUser;
// Refresh the app on auth change unless explicitly marked otherwise.
// No need to update unless the user has changed.
if (notifyOnAuthChange && shouldUpdate) {
notifyListeners();
}
// Once again mark the notifier as needing to update on auth change
// (in order to catch sign in / out events).
updateNotifyOnAuthChange(true);
}
void stopShowingSplashImage() {
showSplashImage = false;
notifyListeners();
}
}
GoRouter createRouter(AppStateNotifier appStateNotifier) => GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
refreshListenable: appStateNotifier,
errorBuilder: (context, state) =>
appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(),
routes: [
FFRoute(
name: '_initialize',
path: '/',
builder: (context, _) =>
appStateNotifier.loggedIn ? MenuWidget() : LoginWidget(),
),
FFRoute(
name: 'login',
path: '/login',
builder: (context, params) => LoginWidget(),
),
FFRoute(
name: 'signup',
path: '/signup',
builder: (context, params) => SignupWidget(),
),
FFRoute(
name: 'login1',
path: '/login1',
builder: (context, params) => Login1Widget(),
),
FFRoute(
name: 'login2',
path: '/login2',
builder: (context, params) => Login2Widget(),
),
FFRoute(
name: 'login3',
path: '/login3',
builder: (context, params) => Login3Widget(),
),
FFRoute(
name: 'login4',
path: '/login4',
builder: (context, params) => Login4Widget(),
),
FFRoute(
name: 'login5',
path: '/login5',
builder: (context, params) => Login5Widget(),
),
FFRoute(
name: 'tnc',
path: '/tnc',
builder: (context, params) => TncWidget(),
),
FFRoute(
name: 'policy',
path: '/policy',
builder: (context, params) => PolicyWidget(),
),
FFRoute(
name: 'menu',
path: '/menu',
builder: (context, params) => MenuWidget(),
),
FFRoute(
name: 'dashboard',
path: '/dashboard',
builder: (context, params) => DashboardWidget(),
),
FFRoute(
name: 'test',
path: '/test',
builder: (context, params) => TestWidget(),
),
FFRoute(
name: 'setting',
path: '/setting',
builder: (context, params) => SettingWidget(),
),
FFRoute(
name: 'user',
path: '/user',
builder: (context, params) => UserWidget(),
),
FFRoute(
name: 'bk1',
path: '/bk1',
builder: (context, params) => Bk1Widget(),
),
FFRoute(
name: 'enquiry',
path: '/enquiry',
builder: (context, params) => EnquiryWidget(),
),
FFRoute(
name: 'myenquiry',
path: '/myenquiry',
builder: (context, params) => MyenquiryWidget(),
),
FFRoute(
name: 'myenquiry1',
path: '/myenquiry1',
builder: (context, params) => Myenquiry1Widget(),
),
FFRoute(
name: 'language',
path: '/language',
builder: (context, params) => LanguageWidget(),
),
FFRoute(
name: 'account1',
path: '/account1',
builder: (context, params) => Account1Widget(),
),
FFRoute(
name: 'chatbox',
path: '/chatbox',
builder: (context, params) => ChatboxWidget(),
),
FFRoute(
name: 'compsec',
path: '/compsec',
builder: (context, params) => CompsecWidget(),
),
FFRoute(
name: 'compsec1',
path: '/compsec1',
builder: (context, params) => Compsec1Widget(),
),
FFRoute(
name: 'compsec2',
path: '/compsec2',
builder: (context, params) => Compsec2Widget(),
),
FFRoute(
name: 'compsec3',
path: '/compsec3',
builder: (context, params) => Compsec3Widget(),
),
FFRoute(
name: 'compsec4',
path: '/compsec4',
builder: (context, params) => Compsec4Widget(),
),
FFRoute(
name: 'compsec5',
path: '/compsec5',
builder: (context, params) => Compsec5Widget(),
),
FFRoute(
name: 'bk2',
path: '/bk2',
builder: (context, params) => Bk2Widget(),
),
FFRoute(
name: 'bk3',
path: '/bk3',
builder: (context, params) => Bk3Widget(),
),
FFRoute(
name: 'bk4',
path: '/bk4',
builder: (context, params) => Bk4Widget(),
),
FFRoute(
name: 'bk4_1',
path: '/bk41',
builder: (context, params) => Bk41Widget(),
),
FFRoute(
name: 'bk4_2',
path: '/bk42',
builder: (context, params) => Bk42Widget(),
),
FFRoute(
name: 'search',
path: '/search',
builder: (context, params) => SearchWidget(),
),
FFRoute(
name: 'bk_checkout',
path: '/bkCheckout',
builder: (context, params) => BkCheckoutWidget(),
),
FFRoute(
name: 'bk_checkout1',
path: '/bkCheckout1',
builder: (context, params) => BkCheckout1Widget(),
),
FFRoute(
name: 'compsec_checkout',
path: '/compsecCheckout',
builder: (context, params) => CompsecCheckoutWidget(),
),
FFRoute(
name: 'compsec_checkout1',
path: '/compsecCheckout1',
builder: (context, params) => CompsecCheckout1Widget(),
),
FFRoute(
name: 'compsec_checkout2',
path: '/compsecCheckout2',
builder: (context, params) => CompsecCheckout2Widget(),
),
FFRoute(
name: 'compsecEnq',
path: '/compsecEnq',
builder: (context, params) => CompsecEnqWidget(),
),
FFRoute(
name: 'compsecEnq1',
path: '/compsecEnq1',
builder: (context, params) => CompsecEnq1Widget(),
),
FFRoute(
name: 'compsec_changename',
path: '/compsecChangename',
builder: (context, params) => CompsecChangenameWidget(),
),
FFRoute(
name: 'compsec_changeaddress',
path: '/compsecChangeaddress',
builder: (context, params) => CompsecChangeaddressWidget(),
),
FFRoute(
name: 'compsec_changedirector',
path: '/compsecChangedirector',
builder: (context, params) => CompsecChangedirectorWidget(),
),
FFRoute(
name: 'compsec_changesecretary',
path: '/compsecChangesecretary',
builder: (context, params) => CompsecChangesecretaryWidget(),
),
FFRoute(
name: 'compsec_changesecrORdir',
path: '/compsecChangesecrORdir',
builder: (context, params) => CompsecChangesecrORdirWidget(),
),
FFRoute(
name: 'compsec_resignation',
path: '/compsecResignation',
builder: (context, params) => CompsecResignationWidget(),
),
FFRoute(
name: 'compsec_transferofShares',
path: '/compsecTransferofShares',
builder: (context, params) => CompsecTransferofSharesWidget(),
),
FFRoute(
name: 'compsec_otherRequest',
path: '/compsecOtherRequest',
builder: (context, params) => CompsecOtherRequestWidget(),
),
FFRoute(
name: 'account',
path: '/account',
builder: (context, params) => AccountWidget(),
),
FFRoute(
name: 'notification',
path: '/notification',
builder: (context, params) => NotificationWidget(),
),
FFRoute(
name: 'compsecQueue',
path: '/compsecQueue',
builder: (context, params) => CompsecQueueWidget(),
),
FFRoute(
name: 'compsecQueue1_1',
path: '/compsecQueue11',
builder: (context, params) => CompsecQueue11Widget(),
),
FFRoute(
name: 'testing',
path: '/testing',
builder: (context, params) => TestingWidget(),
),
FFRoute(
name: 'newuser',
path: '/newuser',
builder: (context, params) => NewuserWidget(
checkPermission23:
params.getParam('checkPermission23', ParamType.bool),
checkPremission22:
params.getParam('checkPremission22', ParamType.bool),
),
),
FFRoute(
name: 'userDetail',
path: '/userDetail',
builder: (context, params) => UserDetailWidget(
firstName: params.getParam('firstName', ParamType.String),
lastName: params.getParam('lastName', ParamType.String),
phone: params.getParam('phone', ParamType.int),
email: params.getParam('email', ParamType.String),
status: params.getParam('status', ParamType.String),
),
),
FFRoute(
name: 'compsecDigitalTrans_list',
path: '/compsecDigitalTransList',
builder: (context, params) => CompsecDigitalTransListWidget(),
),
FFRoute(
name: 'compsecServicedraft',
path: '/compsecServicedraft',
builder: (context, params) => CompsecServicedraftWidget(),
),
FFRoute(
name: 'setting1',
path: '/setting1',
builder: (context, params) => Setting1Widget(),
),
FFRoute(
name: 'editUser',
path: '/editUser',
builder: (context, params) => EditUserWidget(
firstName: params.getParam('firstName', ParamType.String),
lastName: params.getParam('lastName', ParamType.String),
phone: params.getParam('phone', ParamType.int),
email: params.getParam('email', ParamType.String),
userID: params.getParam('userID', ParamType.int),
checkPermission23:
params.getParam('checkPermission23', ParamType.bool),
checkPremission22:
params.getParam('checkPremission22', ParamType.bool),
roleID: params.getParam('roleID', ParamType.String),
),
),
FFRoute(
name: 'bk_view',
path: '/bkView',
builder: (context, params) => BkViewWidget(
fileUrl: params.getParam('fileUrl', ParamType.JSON),
),
),
FFRoute(
name: 'search1',
path: '/search1',
builder: (context, params) => Search1Widget(
search: params.getParam('search', ParamType.String),
status: params.getParam('status', ParamType.String),
category: params.getParam('category', ParamType.String),
fixDate: params.getParam('fixDate', ParamType.String),
dateFrom: params.getParam('dateFrom', ParamType.String),
dateTo: params.getParam('dateTo', ParamType.String),
),
),
FFRoute(
name: 'search_page',
path: '/searchPage',
builder: (context, params) => SearchPageWidget(),
)
].map((r) => r.toRoute(appStateNotifier)).toList(),
);
extension NavParamExtensions on Map<String, String?> {
Map<String, String> get withoutNulls => Map.fromEntries(
entries
.where((e) => e.value != null)
.map((e) => MapEntry(e.key, e.value!)),
);
}
extension NavigationExtensions on BuildContext {
void goNamedAuth(
String name,
bool mounted, {
Map<String, String> pathParameters = const <String, String>{},
Map<String, String> queryParameters = const <String, String>{},
Object? extra,
bool ignoreRedirect = false,
}) =>
!mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect)
? null
: goNamed(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
void pushNamedAuth(
String name,
bool mounted, {
Map<String, String> pathParameters = const <String, String>{},
Map<String, String> queryParameters = const <String, String>{},
Object? extra,
bool ignoreRedirect = false,
}) =>
!mounted || GoRouter.of(this).shouldRedirect(ignoreRedirect)
? null
: pushNamed(
name,
pathParameters: pathParameters,
queryParameters: queryParameters,
extra: extra,
);
void safePop() {
// If there is only one route on the stack, navigate to the initial
// page instead of popping.
if (canPop()) {
pop();
} else {
go('/');
}
}
}
extension GoRouterExtensions on GoRouter {
AppStateNotifier get appState => AppStateNotifier.instance;
void prepareAuthEvent([bool ignoreRedirect = false]) =>
appState.hasRedirect() && !ignoreRedirect
? null
: appState.updateNotifyOnAuthChange(false);
bool shouldRedirect(bool ignoreRedirect) =>
!ignoreRedirect && appState.hasRedirect();
void clearRedirectLocation() => appState.clearRedirectLocation();
void setRedirectLocationIfUnset(String location) =>
appState.updateNotifyOnAuthChange(false);
}
extension _GoRouterStateExtensions on GoRouterState {
Map<String, dynamic> get extraMap =>
extra != null ? extra as Map<String, dynamic> : {};
Map<String, dynamic> get allParams => <String, dynamic>{}
..addAll(pathParameters)
..addAll(queryParameters)
..addAll(extraMap);
TransitionInfo get transitionInfo => extraMap.containsKey(kTransitionInfoKey)
? extraMap[kTransitionInfoKey] as TransitionInfo
: TransitionInfo.appDefault();
}
class FFParameters {
FFParameters(this.state, [this.asyncParams = const {}]);
final GoRouterState state;
final Map<String, Future<dynamic> Function(String)> asyncParams;
Map<String, dynamic> futureParamValues = {};
// Parameters are empty if the params map is empty or if the only parameter
// present is the special extra parameter reserved for the transition info.
bool get isEmpty =>
state.allParams.isEmpty ||
(state.extraMap.length == 1 &&
state.extraMap.containsKey(kTransitionInfoKey));
bool isAsyncParam(MapEntry<String, dynamic> param) =>
asyncParams.containsKey(param.key) && param.value is String;
bool get hasFutures => state.allParams.entries.any(isAsyncParam);
Future<bool> completeFutures() => Future.wait(
state.allParams.entries.where(isAsyncParam).map(
(param) async {
final doc = await asyncParams[param.key]!(param.value)
.onError((_, __) => null);
if (doc != null) {
futureParamValues[param.key] = doc;
return true;
}
return false;
},
),
).onError((_, __) => [false]).then((v) => v.every((e) => e));
dynamic getParam<T>(
String paramName,
ParamType type, [
bool isList = false,
]) {
if (futureParamValues.containsKey(paramName)) {
return futureParamValues[paramName];
}
if (!state.allParams.containsKey(paramName)) {
return null;
}
final param = state.allParams[paramName];
// Got parameter from `extras`, so just directly return it.
if (param is! String) {
return param;
}
// Return serialized value.
return deserializeParam<T>(
param,
type,
isList,
);
}
}
class FFRoute {
const FFRoute({
required this.name,
required this.path,
required this.builder,
this.requireAuth = false,
this.asyncParams = const {},
this.routes = const [],
});
final String name;
final String path;
final bool requireAuth;
final Map<String, Future<dynamic> Function(String)> asyncParams;
final Widget Function(BuildContext, FFParameters) builder;
final List<GoRoute> routes;
GoRoute toRoute(AppStateNotifier appStateNotifier) => GoRoute(
name: name,
path: path,
redirect: (context, state) {
if (appStateNotifier.shouldRedirect) {
final redirectLocation = appStateNotifier.getRedirectLocation();
appStateNotifier.clearRedirectLocation();
return redirectLocation;
}
if (requireAuth && !appStateNotifier.loggedIn) {
appStateNotifier.setRedirectLocationIfUnset(state.location);
return '/login';
}
return null;
},
pageBuilder: (context, state) {
final ffParams = FFParameters(state, asyncParams);
final page = ffParams.hasFutures
? FutureBuilder(
future: ffParams.completeFutures(),
builder: (context, _) => builder(context, ffParams),
)
: builder(context, ffParams);
final child = appStateNotifier.loading
? Center(
child: SizedBox(
width: 50.0,
height: 50.0,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
FlutterTheme.of(context).info,
),
),
),
)
: page;
final transitionInfo = state.transitionInfo;
return transitionInfo.hasTransition
? CustomTransitionPage(
key: state.pageKey,
child: child,
transitionDuration: transitionInfo.duration,
transitionsBuilder:
(context, animation, secondaryAnimation, child) =>
PageTransition(
type: transitionInfo.transitionType,
duration: transitionInfo.duration,
reverseDuration: transitionInfo.duration,
alignment: transitionInfo.alignment,
child: child,
).buildTransitions(
context,
animation,
secondaryAnimation,
child,
),
)
: MaterialPage(key: state.pageKey, child: child);
},
routes: routes,
);
}
class TransitionInfo {
const TransitionInfo({
required this.hasTransition,
this.transitionType = PageTransitionType.fade,
this.duration = const Duration(milliseconds: 300),
this.alignment,
});
final bool hasTransition;
final PageTransitionType transitionType;
final Duration duration;
final Alignment? alignment;
static TransitionInfo appDefault() => TransitionInfo(hasTransition: false);
}
class RootPageContext {
const RootPageContext(this.isRootPage, [this.errorRoute]);
final bool isRootPage;
final String? errorRoute;
static bool isInactiveRootPage(BuildContext context) {
final rootPageContext = context.read<RootPageContext?>();
final isRootPage = rootPageContext?.isRootPage ?? false;
final location = GoRouter.of(context).location;
return isRootPage &&
location != '/' &&
location != rootPageContext?.errorRoute;
}
static Widget wrap(Widget child, {String? errorRoute}) => Provider.value(
value: RootPageContext(true, errorRoute),
child: child,
);
}