manager-app/lib/main.dart
Thomas Fransolet 0df5dc03fa Web mobile : le clavier se refermait aussitot ouvert sur le login
Le meta viewport manquant n'expliquait pas tout : le clavier s'ouvrait puis se
refermait apres une demi-seconde. C'est le focus qui etait perdu, pas la saisie
qui n'arrivait pas.

Sur Flutter web, un AutofillGroup se materialise par un <form> DOM qui porte les
inputs caches par lesquels passe la saisie. L'ouverture du clavier redimensionne
la fenetre, l'ecran se reconstruit, le groupe d'autofill est renegocie et le
<form> recree : l'input focus disparait avec lui. Sur desktop rien ne
redimensionne au clic, d'ou un bug invisible hors mobile.

L'AutofillGroup est retire — les autofillHints portes par chaque champ suffisent
au navigateur pour proposer le remplissage, le groupe ne servait qu'a
finishAutofillContext. Trois protections completent la correction :

- les deux champs passent par un TextEditingController detenu par le State, donc
  la saisie survit a une reconstruction, la ou initialValue repartait de zero ;
- ils portent une ValueKey stable, pour etre reapparies plutot que recrees si la
  structure de leurs freres bouge ;
- le Scaffold ne se redimensionne plus a l'ouverture du clavier ; le contenu est
  deja dans un SingleChildScrollView, rien ne reste masque.

Au passage, le pre-remplissage de developpement (localhost) etait ecrit dans
build() a chaque reconstruction : il passe dans initState, ou il a du sens.

Le routeur, lui, etait fabrique dans le builder d'un FutureBuilder et l'appel a
getInstanceInfo partait de l'arbre passe a runApp. Chaque passage du builder
rendait un GoRouter neuf, donc un arbre neuf et un historique perdu. Les deux
sont desormais resolus une fois, avant runApp — 45 lignes de moins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:07:17 +02:00

290 lines
9.7 KiB
Dart

import 'dart:ui';
import 'package:manager_app/l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:go_router/go_router.dart';
import 'package:manager_api_new/api.dart';
import 'package:manager_app/Models/managerContext.dart';
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 'Helpers/FileHelper.dart';
import 'Models/session.dart';
import 'Screens/login_screen.dart';
import 'Screens/Auth/forgot_password_screen.dart';
import 'Screens/Auth/set_password_screen.dart';
import 'Screens/Policy/policy_screen.dart';
import 'app_context.dart';
import 'client.dart';
import 'constants.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:toastification/toastification.dart';
import 'dart:html' as html;
//import 'package:window_size/window_size.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
var session = await loadJsonSessionFile();
ManagerAppContext managerAppContext = ManagerAppContext();
managerAppContext.instanceId = session.instanceId;
await Firebase.initializeApp(
// Replace with actual values
options: FirebaseOptions(
apiKey: "AIzaSyCFXuDsslqHiPpK6WTcxdIvTDP3ioaaxp4",
authDomain: "mymuseum-3b97f.firebaseapp.com",
projectId: "mymuseum-3b97f",
storageBucket: "mymuseum-3b97f.appspot.com",
messagingSenderId: "1034665398515",
appId: "1:1034665398515:web:688e2a7b3465e621d6a786",
measurementId: "G-EKKHJLJ1E9"
),
);
String? currentPath = html.window.location.pathname;
usePathUrlStrategy();
// 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;
runApp(
ChangeNotifierProvider<AppContext>(
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),
),
),
),
),
),
);
}
Future<InstanceDTO?> getInstanceInfo(ManagerAppContext managerAppContext) async {
var session = await loadJsonSessionFile();
try {
managerAppContext.instanceId = managerAppContext.instanceId ?? session.instanceId;
managerAppContext.host = managerAppContext.host ?? session.host;
managerAppContext.accessToken = managerAppContext.accessToken ?? session.token;
managerAppContext.email = managerAppContext.email ?? session.email;
managerAppContext.role = managerAppContext.role ?? UserRole.fromJson(session.role);
if(session.instanceId != null) {
if(managerAppContext.clientAPI == null) {
managerAppContext.clientAPI = Client(session.host!);
managerAppContext.clientAPI!.apiApi!.addDefaultHeader('authorization', 'Bearer '+ managerAppContext.accessToken!);
}
InstanceDTO? instance = await managerAppContext.clientAPI!.instanceApi!.instanceGetDetail(managerAppContext.instanceId!);
managerAppContext.instanceDTO = instance;
return instance;
}
return null;
} catch(e) {
// todo
/*Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(Route<dynamic> route) => false,
);*/
return null;
}
}
class MyApp extends StatefulWidget {
final Session session;
final ManagerAppContext managerAppContext;
final GoRouter router;
MyApp({required this.session, required this.managerAppContext, required this.router});
// This widget is the root of your application.
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final GlobalKey<_MyAppState> mainKey = GlobalKey();
@override
Widget build(BuildContext context) {
final locale = Provider.of<AppContext>(context).getContext()?.locale ?? const Locale('fr');
return ToastificationWrapper(
child: MaterialApp.router(
routerConfig: widget.router,
key: mainKey,
builder: (context, child) => ResponsiveBreakpoints.builder(
child: child!,
breakpoints: [
const Breakpoint(start: 0, end: 550, name: MOBILE),
const Breakpoint(start: 550, end: 800, name: TABLET),
const Breakpoint(start: 801, end: 1920, name: DESKTOP),
const Breakpoint(start: 1921, end: double.infinity, name: '4K'),
],
),
locale: locale,
supportedLocales: const [Locale('fr'), Locale('en'), Locale('nl')],
localizationsDelegates: const [
AppLocalizations.delegate,
FlutterQuillLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
scrollBehavior: MyCustomScrollBehavior(),
debugShowCheckedModeBanner: false,
title: 'MyInfoMate - Manager',
//initialRoute: widget.initialRoute,
/*supportedLocales: [
const Locale('en', 'US'),
//const Locale('fr', 'FR'),
],*/
theme: ThemeData(
primaryColor: kPrimaryColor,
scaffoldBackgroundColor: kBackgroundColor,
colorScheme: ColorScheme.light(
primary: kPrimaryColor,
onPrimary: Colors.white,
secondary: kPrimaryColor,
onSecondary: Colors.white,
error: kError,
),
textTheme: TextTheme(bodyLarge: TextStyle(color: kBodyTextColor)),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
/*routes: {
'/login': (context) => LoginScreen(session: widget.session),
'/main': (context) => MainScreen(),
'/policy': (context) => PolicyScreen(),
'/policy/mdlf': (context) => PolicyScreen(param: "mdlf"),
'/policy/fort': (context) => PolicyScreen(param: "fort"),
},
onUnknownRoute: (settings) => MaterialPageRoute(
builder: (context) => Container(child: Center(child: Text("Not found"))),
),*/
),
);
}
}
Future<Session> loadJsonSessionFile() async {
Session session = await FileHelper().readSessionWeb();
//print(session);
return session;
}
class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like dragDevices
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}