diff --git a/lib/Components/rounded_input_field.dart b/lib/Components/rounded_input_field.dart index 16da056..08e6419 100644 --- a/lib/Components/rounded_input_field.dart +++ b/lib/Components/rounded_input_field.dart @@ -14,6 +14,10 @@ class RoundedInputField extends StatelessWidget { final double fontSize; final String? autofill; final bool isInt; + + /// A preferer a [initialValue] pour un champ vivant dans un ecran qui se + /// reconstruit : le texte survit alors a la reconstruction du TextFormField. + final TextEditingController? controller; const RoundedInputField({ Key? key, this.hintText, @@ -27,7 +31,8 @@ class RoundedInputField extends StatelessWidget { this.isEmail = false, this.fontSize = 13, this.autofill, - this.isInt = false + this.isInt = false, + this.controller, }) : super(key: key); @override @@ -36,10 +41,11 @@ class RoundedInputField extends StatelessWidget { color: color, child: TextFormField ( onChanged: onChanged, - initialValue: initialValue, + controller: controller, + initialValue: controller == null ? initialValue : null, cursorColor: textColor, maxLength: maxLength, - autofillHints: autofill != null ? [autofill!] : [], + autofillHints: autofill != null ? [autofill!] : const [], keyboardType: isEmail ? TextInputType.emailAddress : isInt ? TextInputType.number : TextInputType.text, inputFormatters: !isInt ? [] : [ FilteringTextInputFormatter.digitsOnly diff --git a/lib/Components/rounded_password_field.dart b/lib/Components/rounded_password_field.dart index e5a6ef1..bd1c10a 100644 --- a/lib/Components/rounded_password_field.dart +++ b/lib/Components/rounded_password_field.dart @@ -6,10 +6,12 @@ import 'package:manager_app/constants.dart'; class RoundedPasswordField extends StatefulWidget { final ValueChanged onChanged; final String initialValue; + final TextEditingController? controller; const RoundedPasswordField({ Key? key, required this.onChanged, - required this.initialValue + required this.initialValue, + this.controller, }) : super(key: key); @override @@ -25,8 +27,9 @@ class _RoundedPasswordFieldState extends State { child: TextFormField( obscureText: isVisible, onChanged: widget.onChanged, - initialValue: widget.initialValue, - autofillHints: [AutofillHints.password], + controller: widget.controller, + initialValue: widget.controller == null ? widget.initialValue : null, + autofillHints: const [AutofillHints.password], cursorColor: kPrimaryColor, style: TextStyle(fontSize: 16, color: kInk), decoration: InputDecoration( diff --git a/lib/Screens/login_screen.dart b/lib/Screens/login_screen.dart index ad39466..0607813 100644 --- a/lib/Screens/login_screen.dart +++ b/lib/Screens/login_screen.dart @@ -2,7 +2,6 @@ import 'dart:html'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:go_router/go_router.dart'; import 'package:manager_app/Components/common_loader.dart'; @@ -44,6 +43,12 @@ class _LoginScreenState extends State { Storage localStorage = window.localStorage; late final Future appVersion = getAppVersion(); + // Champs pilotes par un controller, et non par initialValue : l'ouverture du + // clavier mobile redimensionne la fenetre, ce qui reconstruit l'ecran. Avec + // initialValue, le TextFormField repartait de zero a chaque reconstruction. + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + void authenticateTRY(AppContext appContext, bool fromClick) async { clientAPI = Client(this.host!); @@ -188,9 +193,23 @@ class _LoginScreenState extends State { this.pinCode = localStorage.entries.where((e) => e.key == "pinCode").first.value; } } + if (window.location.href.contains("localhost")) { + email = "test@email.be"; + password = "kljqsdkljqsd"; + } + + emailController.text = email; + passwordController.text = password; super.initState(); } + @override + void dispose() { + emailController.dispose(); + passwordController.dispose(); + super.dispose(); + } + ManagerAppContext initInstance(ManagerAppContext managerAppContext) { var url = window.location.href; if (!url.contains("localhost")) { @@ -209,8 +228,6 @@ class _LoginScreenState extends State { print("subdomain not found"); } } else { - this.email = "test@email.be"; - this.password = "kljqsdkljqsd"; print("localhost.. set mymuseum instance by default"); } @@ -224,6 +241,11 @@ class _LoginScreenState extends State { initInstance(appContext.getContext()); return Scaffold( + // Le Scaffold ne se redimensionne pas a l'ouverture du clavier : sur web + // mobile ce redimensionnement relance une mise en page pendant que le + // navigateur ouvre le clavier, et le focus n'y survit pas. Le contenu est + // deja dans un SingleChildScrollView, rien ne reste sous le clavier. + resizeToAvoidBottomInset: false, body: LayoutBuilder( builder: (context, constraints) { final isMobile = constraints.maxWidth < 550; @@ -307,23 +329,31 @@ class _LoginScreenState extends State { }, ), SizedBox(height: 32), + // Pas d'AutofillGroup ici : sur Flutter web il se + // materialise par un
DOM qui porte les inputs + // caches. La reconstruction declenchee par l'ouverture + // du clavier mobile le recreait, l'input focus etait + // detruit et le clavier se refermait aussitot. Les + // autofillHints des champs suffisent au navigateur. Form( key: widget.key, - child: AutofillGroup( - child: Column( + child: Column( children: [ RoundedInputField( + key: const ValueKey('login-email'), hintText: AppLocalizations.of(context)!.email, autofill: AutofillHints.email, onChanged: (value) => email = value, icon: Icons.person_outline, - initialValue: email, + controller: emailController, isEmail: true, fontSize: 16, ), const SizedBox(height: kSpace4), RoundedPasswordField( + key: const ValueKey('login-password'), initialValue: password, + controller: passwordController, onChanged: (value) => password = value, ), if (kIsWeb) @@ -357,10 +387,7 @@ class _LoginScreenState extends State { fontSize: 16, vertical: 15, horizontal: 30, - press: () { - TextInput.finishAutofillContext(); - authenticateTRY(appContext, true); - }, + press: () => authenticateTRY(appContext, true), ), ) : CommonLoader(iconSize: 40), @@ -374,7 +401,6 @@ class _LoginScreenState extends State { ), ], ), - ), ), ], ), diff --git a/lib/main.dart b/lib/main.dart index e3bd53a..7fb7b0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,10 +11,8 @@ import 'package:manager_app/Screens/Main/main_screen.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; -import 'Components/common_loader.dart'; import 'Helpers/FileHelper.dart'; import 'Models/session.dart'; -import 'Screens/Main/main_screen.dart'; import 'Screens/login_screen.dart'; import 'Screens/Auth/forgot_password_screen.dart'; import 'Screens/Auth/set_password_screen.dart'; @@ -52,161 +50,118 @@ Future main() async { usePathUrlStrategy(); - runApp( - ChangeNotifierProvider( - create: (_) => AppContext(managerAppContext), - child: MaterialApp( - debugShowCheckedModeBanner: false, - locale: const Locale('fr'), - supportedLocales: const [Locale('fr')], - localizationsDelegates: const [ - AppLocalizations.delegate, - FlutterQuillLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - home: FutureBuilder( - future: getInstanceInfo(managerAppContext), - builder: (context, asyncSnapshot) { - if (asyncSnapshot.connectionState == ConnectionState.done) { - var instanceDTO = asyncSnapshot.data; - managerAppContext.instanceId = instanceDTO?.id; - managerAppContext.instanceDTO = instanceDTO; + // getInstanceInfo etait appele depuis l'arbre passe a runApp, et le GoRouter + // fabrique dans le builder d'un FutureBuilder : chaque passage du builder + // rendait un routeur neuf, donc un arbre neuf et un historique de navigation + // perdu. Les deux sont desormais resolus une seule fois, avant runApp. + var instanceDTO = await getInstanceInfo(managerAppContext); + managerAppContext.instanceId = instanceDTO?.id; + managerAppContext.instanceDTO = instanceDTO; - String initialRoute = currentPath!.isNotEmpty ? currentPath : '/login'; - - var _router = GoRouter( - initialLocation: initialRoute, - redirect: (context, state) { - var instanceId = managerAppContext.instanceId; - const publicPaths = ['/login', '/forgot-password', '/set-password']; - if (instanceId == null && !publicPaths.contains(state.fullPath)) { - return '/login'; - } - - - if (instanceId != null) { - if(state.fullPath == '/login' || state.fullPath == "") { - if(managerAppContext.instanceDTO!.isMobile!) { - return '/main/mobile'; - } - if(managerAppContext.instanceDTO!.isTablet!) { - return '/main/tablet'; - } - if(managerAppContext.instanceDTO!.isWeb!) { - return '/main/web'; - } - if(managerAppContext.instanceDTO!.isVR!) { - return '/main/vr'; - } - } - return state.matchedLocation; - } - - return null; - }, - routes: [ - GoRoute( - path: '/login', - builder: (context, state) => LoginScreen(), - ), - GoRoute( - path: '/forgot-password', - builder: (context, state) => ForgotPasswordScreen(), - ), - GoRoute( - path: '/set-password', - builder: (context, state) => SetPasswordScreen( - token: state.uri.queryParameters['token'], - ), - ), - GoRoute( - path: '/main/:view', - builder: (context, state) { - final view = state.pathParameters['view']; - // La clé porte l'instance : sans elle, un SuperAdmin - // qui change d'instance en restant sur la même vue - // gardait le State de tous les écrans en dessous, donc - // les données de l'instance précédente. - return MainScreen( - key: ValueKey('${managerAppContext.instanceId}/$view'), - instance: managerAppContext.instanceDTO!, - view: view, - ); - }, - ), - GoRoute( - path: '/policy', - builder: (context, state) => PolicyScreen(), - ), - GoRoute( - path: '/policy/mdlf', - builder: (context, state) => PolicyScreen(param: "mdlf"), - ), - GoRoute( - path: '/policy/fort', - builder: (context, state) => PolicyScreen(param: "fort"), - ), - ], - errorBuilder: (context, state) => MaterialApp( - debugShowCheckedModeBanner: false, - home: Scaffold( - body: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.85, - child: Text( - AppLocalizations.of(context)!.pageNotFound, - textAlign: TextAlign.center, - style: TextStyle(color: kPrimaryColor, fontSize: 20.0), - ), - ), - ), - ), - ), - ); - - return MyApp( - session: session, - managerAppContext: managerAppContext, - router: _router - ); - } else if (asyncSnapshot.connectionState == ConnectionState.none) { - return MaterialApp( - debugShowCheckedModeBanner: false, - home: Scaffold( - body: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.85, - child: Text( - "No data", - textAlign: TextAlign.center, - style: TextStyle(color: kPrimaryColor, fontSize: 20.0), - ), - ), - ), - ), - ); - } else { - return MaterialApp( - debugShowCheckedModeBanner: false, - home: Scaffold( - body: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.85, - child: Container( - height: 250, - child: CommonLoader() - ), - ), - ), - ), - ); - } - } - ), + runApp( + ChangeNotifierProvider( + create: (_) => AppContext(managerAppContext), + child: MyApp( + session: session, + managerAppContext: managerAppContext, + router: buildRouter( + managerAppContext, + currentPath!.isNotEmpty ? currentPath : '/login', ), - ) + ), + ), + ); +} + +GoRouter buildRouter(ManagerAppContext managerAppContext, String initialLocation) { + return GoRouter( + initialLocation: initialLocation, + redirect: (context, state) { + var instanceId = managerAppContext.instanceId; + const publicPaths = ['/login', '/forgot-password', '/set-password']; + if (instanceId == null && !publicPaths.contains(state.fullPath)) { + return '/login'; + } + + + if (instanceId != null) { + if(state.fullPath == '/login' || state.fullPath == "") { + if(managerAppContext.instanceDTO!.isMobile!) { + return '/main/mobile'; + } + if(managerAppContext.instanceDTO!.isTablet!) { + return '/main/tablet'; + } + if(managerAppContext.instanceDTO!.isWeb!) { + return '/main/web'; + } + if(managerAppContext.instanceDTO!.isVR!) { + return '/main/vr'; + } + } + return state.matchedLocation; + } + + return null; + }, + routes: [ + GoRoute( + path: '/login', + builder: (context, state) => LoginScreen(), + ), + GoRoute( + path: '/forgot-password', + builder: (context, state) => ForgotPasswordScreen(), + ), + GoRoute( + path: '/set-password', + builder: (context, state) => SetPasswordScreen( + token: state.uri.queryParameters['token'], + ), + ), + GoRoute( + path: '/main/:view', + builder: (context, state) { + final view = state.pathParameters['view']; + // La clé porte l'instance : sans elle, un SuperAdmin + // qui change d'instance en restant sur la même vue + // gardait le State de tous les écrans en dessous, donc + // les données de l'instance précédente. + return MainScreen( + key: ValueKey('${managerAppContext.instanceId}/$view'), + instance: managerAppContext.instanceDTO!, + view: view, + ); + }, + ), + GoRoute( + path: '/policy', + builder: (context, state) => PolicyScreen(), + ), + GoRoute( + path: '/policy/mdlf', + builder: (context, state) => PolicyScreen(param: "mdlf"), + ), + GoRoute( + path: '/policy/fort', + builder: (context, state) => PolicyScreen(param: "fort"), + ), + ], + errorBuilder: (context, state) => MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.85, + child: Text( + AppLocalizations.of(context)!.pageNotFound, + textAlign: TextAlign.center, + style: TextStyle(color: kPrimaryColor, fontSize: 20.0), + ), + ), + ), + ), + ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index a044ba8..8d24064 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 3.1.3+11 +version: 3.1.4+12 environment: sdk: ">=3.1.0 <4.0.0"