Thomas Fransolet 9f582dd8a7 Lot K : tablet-app reconstruit et rebranché sur le contrat Postgres v3
Premier vrai build depuis avril. Le diagnostic des docs — « un seul défaut,
purement Gradle, peut-être réglé par 5a6701d » — tenait parce que personne
n'avait lancé la commande.

K1 — dix crans d'outillage, chacun dicté par l'erreur du précédent :
Gradle 7.5 → 8.11.1, AGP 7.2.0 → 8.9.1, Kotlin 1.9.0 → 2.3.10,
enableUncompressedNativeLibs retirée (supprimée en AGP 8.1), jcenter → mavenCentral,
heap 1536M → 4096M, Jetifier coupé, et retrait du resolutionStrategy qui forçait
androidx.lifecycle à 2.4.0 « to fix mapbox issue » : il réglait un problème d'il y
a trois ans et causait celui d'aujourd'hui, mapbox_maps_flutter 2.21.1 appelant
setViewTreeLifecycleOwner. Les trois derniers crans étaient de simples alignements
sur mymuseum-visitapp, qui utilise le même plugin sans ces problèmes.

Le bloc buildscript commenté de android/build.gradle est supprimé : il déclarait
une config qui n'était pas celle appliquée et a fait conclure deux fois à tort.

K2 — les 132 erreurs Dart que le Gradle masquait. Trois causes : roundedValue,
isDate, isHour, isSectionImageBackground et screenPercentageSectionsMainPage ont
migré vers AppConfigurationLink ; GeoPointDTO.latitude/longitude sont devenus
geometry ; le reste en découlait. Deux bugs latents trouvés au passage :

- applicationInstanceDTO n'était assigné que si l'instance avait l'IA — il servait
  de drapeau d'assistant. Ce DTO portant désormais les AppConfigurationLink, les
  cinq réglages seraient retombés sur leurs défauts sur MDLF et le Fort, sans
  erreur ni log. Drapeau rendu explicite, en préservant le ET instance × canal.
- ConfigurationDTO.isTablet ayant disparu, le filtre des configurations tablette
  n'avait plus de source : il porte sur les liens du canal AppType.Tablet.

La formule de clé par coordonnées de geo_point_filter, dupliquée à quatre endroits
alors que les deux côtés de l'appariement doivent produire la même valeur, ne vit
plus qu'à un seul (Helpers/geo.dart). Ce fichier documente aussi les deux
conventions de coordonnées du projet : [lng, lat] pour les GeoPoint, [lat, lng]
pour les géométries de GuidedStep.

K4 — l'écran Game repris de mymuseum-visitapp, Screens/Puzzle supprimé. La tablette
gagne le puzzle glissant, le bouton d'indice et un dimensionnement au ratio de
l'image. Les couleurs, codées en dur par flavor client chez mymuseum, sont dérivées
de configuration.primaryColor : tablet-app n'a pas de flavor, un seul APK sert tous
les clients.

Reste à vérifier sur device : le rendu de l'écran Game, et l'écran de sélection de
configuration, qui sera vide si les configurations ne sont pas rattachées au canal
tablette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:18:07 +02:00

402 lines
17 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:manager_api_new/api.dart';
//import 'package:mqtt_client/mqtt_browser_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart';
import 'package:tablet_app/Components/Buttons/rounded_button.dart';
import 'package:tablet_app/Components/loading.dart';
import 'package:tablet_app/Components/loading_common.dart';
import 'package:tablet_app/Components/rounded_input_field.dart';
import 'package:tablet_app/Helpers/DatabaseHelper.dart';
import 'package:tablet_app/Helpers/DeviceInfoHelper.dart';
import 'package:tablet_app/Helpers/ImageCustomProvider.dart';
import 'package:tablet_app/Helpers/MQTTHelper.dart';
import 'package:tablet_app/Models/tabletContext.dart';
import 'package:tablet_app/Screens/Configuration/dropDown_configuration.dart';
import 'package:tablet_app/Screens/MainView/main_view.dart';
import 'package:tablet_app/app_context.dart';
import 'package:tablet_app/client.dart';
import 'package:tablet_app/constants.dart';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
class ConfigViewWidget extends StatefulWidget {
ConfigViewWidget();
@override
_ConfigViewWidget createState() => _ConfigViewWidget();
}
class _ConfigViewWidget extends State<ConfigViewWidget> {
Size sizeScreen = new Size(1080.0, 1920.0); // Tablet resolution
String url = "https://api.myinfomate.be"; //DEV "http://192.168.31.96" http://192.168.31.140:8089 // PROD MDLF "http://192.168.1.19"
String? pinCode;
bool configOk = false;
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
Size size = MediaQuery.of(context).size;
TabletAppContext tabletAppContext = appContext.getContext();
if (tabletAppContext.host != null)
url = tabletAppContext.host!;
return Scaffold(
body: Container(
height: size.height,
width: size.width,
color: kBackgroundGrey,
child: Center(
child: Container(
height: size.height * 0.85,
width: size.width * 0.9,
child: configOk ? FutureBuilder(
future: getConfigurations(appContext),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null || snapshot.data!.length == 0) {
return Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(29),
color: kBackgroundLight,
),
height: size.height * 0.2,
width: size.width * 0.6,
child: Center(
child: Text(
"Veuillez créer une configuration dans le manager",
style: new TextStyle(
fontSize: 25
),
),
)
)
);
} else {
return Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(29),
color: kBackgroundLight,
),
height: size.height*0.3,
width: size.width*0.6,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(left: 15, right: 15, top: 15, bottom: 5),
child: Text(
"Choisir une configuration",
style: new TextStyle(
fontSize: 25
),
),
),
DropDownConfig(
configurations: snapshot.data,
onChange: (ConfigurationDTO configurationOut) async {
if(!kIsWeb) {
// CREATE DEVICE REQUEST
await createDevice(configurationOut, appContext);
} else {
TabletAppContext tabletAppContext = appContext.getContext();
tabletAppContext.host = url;
tabletAppContext.language = "FR"; // By Default
tabletAppContext.configuration = configurationOut;
print("Set context - WEB");
appContext.setContext(tabletAppContext);
}
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) {
return MainViewWidget();
},
),
(Route<dynamic> route) => false // For pushAndRemoveUntil
);
},
),
],
),
),
);
}
} else if (snapshot.connectionState == ConnectionState.none) {
return Text("No data");
} else {
return Center(
child: Container(
height: size.height * 0.2,
child: LoadingCommon()
)
);
}
}
) : Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(29),
color: kBackgroundLight,
),
constraints: BoxConstraints(maxWidth: 500, minWidth: 450, minHeight: 350, maxHeight: 450),
height: size.height*0.35,
width: size.width*0.6,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Entrer l'url du manager",
style: new TextStyle(
fontSize: 25
),
),
),
Padding(
padding: const EdgeInsets.only(left: 50, right: 50),
child: RoundedInputField(
hintText: "URL",
initialValue: url,
onChanged: (value) {
setState(() {
print("Setstate value");
url = value;
print(url);
});
},
icon: Icons.language
),
),
Padding(
padding: const EdgeInsets.only(left: 50, right: 50),
child: RoundedInputField(
hintText: "PIN",
onChanged: (value) {
setState(() {
pinCode = value;
});
},
icon: Icons.pin,
isString: false
),
),
RoundedButton(
text: "OK",
fontSize: 25,
press: () async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
].request();
var client = new Client(url);
print(url);
print(pinCode);
if(url.length > 0 && pinCode != null) {
var instance = await getInstanceIdByPinCode(client, pinCode!);
if (instance != null) {
Fluttertoast.showToast(
msg: "Connecté au manager",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.lightGreen,
textColor: Colors.white,
fontSize: 16.0
);
final apiKey = await _fetchAppKey(url, pinCode!);
final authenticatedClient = Client(url, apiKey: apiKey);
TabletAppContext? tabletAppContext = appContext.getContext();
if(tabletAppContext == null) {
tabletAppContext = new TabletAppContext(host: url, instanceId: instance.id);
}
tabletAppContext.instanceId = instance.id;
tabletAppContext.apiKey = apiKey;
tabletAppContext.clientAPI = authenticatedClient;
var identifier = kIsWeb ? "WEB TEST" : await DeviceInfoHelper.getDeviceDetails();
if(kIsWeb) {
//tabletAppContext.clientBrowserMQTT = new MqttBrowserClient(url.replaceAll('http://', ''),'tablet_app_'+ identifier!);
} else
{
print("init mqtt");
print(identifier);
// mobile specific
tabletAppContext.clientMQTT = new MqttServerClient(url.replaceAll('https://api.', ''),'tablet_app_'+ identifier!);
}
setState(() {
appContext.setContext(tabletAppContext!);
configOk = true;
});
} else {
Fluttertoast.showToast(
msg: "L'url ne correspond pas à un manager",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.redAccent,
textColor: Colors.white,
fontSize: 16.0
);
}
}
},
)
],
),
),
),
),
),
)
);
}
Future<void> createDevice(ConfigurationDTO configurationDTO, dynamic appContext) async {
TabletAppContext tabletAppContext = appContext.getContext();
DeviceDetailDTO newDevice = new DeviceDetailDTO();
newDevice.configurationId = configurationDTO.id;
newDevice.configuration = configurationDTO.label;
newDevice.instanceId = tabletAppContext.instanceId;
newDevice.connected = true;
newDevice.identifier = await DeviceInfoHelper.getDeviceDetails(); // TODO WEB
newDevice.ipAddressWLAN = await getIP(true);
newDevice.ipAddressETH = await getIP(false);
newDevice.name = newDevice.ipAddressWLAN;
newDevice.dateCreation = DateTime.now();
newDevice.dateUpdate = DateTime.now();
newDevice.lastConnectionLevel = DateTime.now();
newDevice.lastBatteryLevel = DateTime.now();
print(newDevice);
DeviceDetailDTO? device = await tabletAppContext.clientAPI!.deviceApi!.deviceCreate(newDevice);
print("DEVICE RESSSULT");
print(device);
if (device != null) {
// STORE IT LOCALLY !!
TabletAppContext tabletAppContext = appContext.getContext();
tabletAppContext.id = device.identifier;
tabletAppContext.deviceId = device.id;
tabletAppContext.host = url;
tabletAppContext.language = "FR"; // By Default
tabletAppContext.configuration = configurationDTO;
print("Set context");
appContext.setContext(tabletAppContext);
// STORE IT LOCALLY (SQLite)
TabletAppContext? localContext = await DatabaseHelper.instance.getData();
if (localContext != null) { // Check if sql DB exist
await DatabaseHelper.instance.update(tabletAppContext);
} else {
await DatabaseHelper.instance.insert(tabletAppContext);
}
Fluttertoast.showToast(
msg: "La tablette a bien été créée",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.lightGreen,
textColor: Colors.white,
fontSize: 16.0
);
} else {
Fluttertoast.showToast(
msg: "Une erreur est survenue lors de la création de la tablette",
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.deepOrangeAccent,
textColor: Colors.white,
fontSize: 16.0
);
}
}
Future<String?> _fetchAppKey(String baseUrl, String pinCode) async {
try {
final resp = await http.get(Uri.parse(
'$baseUrl/api/instance/app-key?pinCode=$pinCode&appType=TabletApp'));
if (resp.statusCode == 200) return jsonDecode(resp.body)['key'] as String?;
} catch (ex) {
print(ex);
}
return null;
}
Future<InstanceDTO?> getInstanceIdByPinCode(Client client, String pinCode) async {
try {
var instance = await client.instanceApi!.instanceGetInstanceByPinCode(pinCode: pinCode);
return instance;
} catch (ex) {
print(ex);
return null;
}
}
}
Future<String?> getIP(bool isWLAN) async {
for (var interface in await NetworkInterface.list()) {
print('== Interface: ${interface.name} ==');
if (interface.name == "wlan0" && isWLAN) {
// wifi
return interface.addresses.first.address;
}
if (interface.name == "eth0" && !isWLAN) {
// wired
return interface.addresses.first.address;
}
}
return null;
}
Future<List<ConfigurationDTO>?> getConfigurations(dynamic appContext) async {
TabletAppContext tabletAppContext = (appContext.getContext() as TabletAppContext);
print(tabletAppContext);
List<ConfigurationDTO>? configurations = await tabletAppContext.clientAPI!.configurationApi!.configurationGet(instanceId: tabletAppContext.instanceId);
print("number of configurations " + configurations!.length.toString());
configurations.forEach((element) {
//print(element);
});
// `ConfigurationDTO.isTablet` a disparu avec Postgres v3 : le canal n'est plus une
// propriété de la configuration, il est porté par l'ApplicationInstance, et le
// rattachement vit dans ses AppConfigurationLink. Même source que main_view.
if (tabletAppContext.instanceId == null) return configurations;
final instanceDTO = await tabletAppContext.clientAPI!.instanceApi!
.instanceGetDetail(tabletAppContext.instanceId!);
final tabletConfigurationIds = instanceDTO?.applicationInstanceDTOs
?.where((a) => a.appType == AppType.Tablet)
.expand((a) => a.configurations ?? const <AppConfigurationLink>[])
.map((link) => link.configurationId)
.toSet() ??
const <String>{};
return configurations
.where((config) => tabletConfigurationIds.contains(config.id))
.toList();
}