L'accueil de la borne passe en bento, réglable depuis le manager

Même grille à six colonnes que le menu du casque, même algorithme de
placement que le mobile et l'aperçu du manager. La borne envoie son
deviceId : elle reçoit son propre agencement, pas la configuration nue.

La hauteur de cellule se déduit de la hauteur disponible et non de la
largeur — une borne ne défile pas. L'orientation est concentrée en un point,
pour que le portrait de la V2 soit un branchement et pas une réécriture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-09-17 11:36:19 +02:00
parent 296b7a7519
commit 081d9a00d9
2 changed files with 152 additions and 89 deletions

View File

@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:manager_api_new/api.dart';
import 'package:myinfomate_layout/myinfomate_layout.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
@ -67,7 +68,6 @@ class _MainViewWidget extends State<MainViewWidget> {
AppConfigurationLink? appConfigurationLink;
ValueNotifier<DateTime?> currentHourDate = ValueNotifier<DateTime?>((DateTime.now().toUtc()).add(Duration(hours: 1)));
late Color backgroundColor;
int rowCount = 4;
bool isImageBackground = false;
@ -373,7 +373,13 @@ class _MainViewWidget extends State<MainViewWidget> {
if(isInit) {
try {
final rawList = await tabletAppContext.clientAPI!.sectionApi!.sectionGetFromConfigurationDetail(tabletAppContext.configuration!.id!);
// Le deviceId désigne le lien de cette borne, donc son agencement : ordre,
// tailles bento et sections masquées pour le seul canal kiosk. Sans lui, le
// serveur rend la configuration nue et la borne perd sa mise en page.
final rawList = await tabletAppContext.clientAPI!.sectionApi!.sectionGetFromConfigurationDetail(
tabletAppContext.configuration!.id!,
deviceId: tabletAppContext.deviceId,
);
rawSectionsData = jsonDecode(jsonEncode(rawList));
List<SectionDTO> sectionList = jsonDecode(jsonEncode(rawSectionsData)).map((json) => SectionDTO.fromJson(json)).whereType<SectionDTO>().toList();
sectionsLocal = sectionList;
@ -397,20 +403,84 @@ class _MainViewWidget extends State<MainViewWidget> {
return sectionsLocal;
}
/// Le nombre de colonnes de la grille bento.
///
/// **6**, comme le menu du casque : c'est la même vue, sur un écran qui a la même
/// forme. Doit rester aligné sur `kKioskColumns` de `kiosk_screen.dart` côté
/// manager-app, sinon l'aperçu montre au client une grille que sa borne ne rend pas.
///
/// **Point de décision d'orientation** (lot K9) : la borne est en paysage en V1, le
/// portrait est un objectif V2. Tout ce qui dépend de l'orientation passe par ici
/// plutôt que par un `if portrait` semé dans l'écran — la V2 devient un branchement,
/// pas une réécriture.
int get bentoColumns => 6;
/// L'écran principal en bento : chaque section occupe colSpan × rowSpan cellules,
/// réglées par canal dans manager-app. Même placement dense que le mobile et l'aperçu
/// du manager (`myinfomate_layout`), que le web refait en CSS Grid et le casque en C#.
///
/// La hauteur de cellule se déduit de la **hauteur disponible**, pas de la largeur :
/// une borne ne défile pas, la grille doit tenir dans l'écran.
getGridSections(AppContext appContext, Color textColor) {
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
if(sectionsLocal != null) {
if (sectionsLocal == null) {
return Center(
child: Text("Aucune section à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect)),
);
}
var isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
return Center(
child: Container(
//color: Colors.greenAccent,
child: GridView.builder(
shrinkWrap: true,
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: rowCount),
itemCount: sectionsLocal!.length,
itemBuilder: (BuildContext context, int index) {
// Les index des sections affichées. On garde l'index d'origine : `rawSectionsData`
// est apparié à `sectionsLocal` position par position, et une section masquée
// décalerait les deux listes l'une par rapport à l'autre.
final visibleIndexes = <int>[];
for (var i = 0; i < sectionsLocal!.length; i++) {
if (sectionsLocal![i].isActive != false) visibleIndexes.add(i);
}
final layout = bentoLayout(
visibleIndexes
.map((i) => BentoItem(
id: sectionsLocal![i].id ?? '$i',
colSpan: (sectionsLocal![i].gridColSpan ?? 1).clamp(1, bentoColumns),
rowSpan: (sectionsLocal![i].gridRowSpan ?? 1).clamp(1, 2),
))
.toList(),
bentoColumns,
);
return LayoutBuilder(
builder: (context, constraints) {
const gap = 16.0;
final rows = layout.rowCount < 1 ? 1 : layout.rowCount;
final cellWidth = (constraints.maxWidth - (bentoColumns - 1) * gap) / bentoColumns;
final cellHeight = (constraints.maxHeight - (rows - 1) * gap) / rows;
// Un placement par item, dans l'ordre des items : `placements[i]` désigne la
// section `visibleIndexes[i]`.
return Stack(
children: [
for (var i = 0; i < layout.placements.length; i++)
Positioned(
left: layout.placements[i].col * (cellWidth + gap),
top: layout.placements[i].row * (cellHeight + gap),
width: layout.placements[i].colSpan * cellWidth +
(layout.placements[i].colSpan - 1) * gap,
height: layout.placements[i].rowSpan * cellHeight +
(layout.placements[i].rowSpan - 1) * gap,
child: sectionTile(
appContext, tabletAppContext, textColor, visibleIndexes[i], isPortrait),
),
],
);
},
);
}
Widget sectionTile(AppContext appContext, TabletAppContext tabletAppContext,
Color textColor, int index, bool isPortrait) {
return InkWell(
onTap: () {
setState(() {
@ -429,7 +499,6 @@ class _MainViewWidget extends State<MainViewWidget> {
decoration: isImageBackground ? boxDecoration(appContext, sectionsLocal![index], false, rawSectionsData[index]) : null,
//color: Colors.yellow,
padding: EdgeInsets.all(isImageBackground ? 18 : 5),
margin: EdgeInsets.symmetric(vertical: isImageBackground ? 25 : 10, horizontal: 25),
child: isImageBackground ? Align(
alignment: Alignment.bottomRight,
child: FractionallySizedBox(
@ -483,16 +552,6 @@ class _MainViewWidget extends State<MainViewWidget> {
),
);
}
),
),
);
} else {
Center(
child: Text("Aucune section à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect)),
);
}
}
getPlatformInfo() async {
return await PackageInfo.fromPlatform();

View File

@ -81,6 +81,10 @@ dependencies:
#archive: ^3.6.1
manager_api_new:
path: ../manager-app/manager_api_new
# Le placement bento de l'écran principal, partagé avec manager-app (aperçu),
# mymuseum-visitapp et — porté en C# — le menu du casque.
myinfomate_layout:
path: ../manager-app/myinfomate_layout
dev_dependencies:
flutter_test: