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:
parent
296b7a7519
commit
081d9a00d9
@ -13,6 +13,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||||
import 'package:manager_api_new/api.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:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@ -67,7 +68,6 @@ class _MainViewWidget extends State<MainViewWidget> {
|
|||||||
AppConfigurationLink? appConfigurationLink;
|
AppConfigurationLink? appConfigurationLink;
|
||||||
ValueNotifier<DateTime?> currentHourDate = ValueNotifier<DateTime?>((DateTime.now().toUtc()).add(Duration(hours: 1)));
|
ValueNotifier<DateTime?> currentHourDate = ValueNotifier<DateTime?>((DateTime.now().toUtc()).add(Duration(hours: 1)));
|
||||||
late Color backgroundColor;
|
late Color backgroundColor;
|
||||||
int rowCount = 4;
|
|
||||||
|
|
||||||
bool isImageBackground = false;
|
bool isImageBackground = false;
|
||||||
|
|
||||||
@ -373,7 +373,13 @@ class _MainViewWidget extends State<MainViewWidget> {
|
|||||||
|
|
||||||
if(isInit) {
|
if(isInit) {
|
||||||
try {
|
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));
|
rawSectionsData = jsonDecode(jsonEncode(rawList));
|
||||||
List<SectionDTO> sectionList = jsonDecode(jsonEncode(rawSectionsData)).map((json) => SectionDTO.fromJson(json)).whereType<SectionDTO>().toList();
|
List<SectionDTO> sectionList = jsonDecode(jsonEncode(rawSectionsData)).map((json) => SectionDTO.fromJson(json)).whereType<SectionDTO>().toList();
|
||||||
sectionsLocal = sectionList;
|
sectionsLocal = sectionList;
|
||||||
@ -397,101 +403,154 @@ class _MainViewWidget extends State<MainViewWidget> {
|
|||||||
return sectionsLocal;
|
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) {
|
getGridSections(AppContext appContext, Color textColor) {
|
||||||
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
|
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
|
||||||
|
|
||||||
if(sectionsLocal != null) {
|
if (sectionsLocal == null) {
|
||||||
var isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
|
|
||||||
|
|
||||||
return Center(
|
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) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
//sectionSelected = sectionsLocal![index];
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) {
|
|
||||||
return SectionPageDetail(configurationDTO: configurationDTO, sectionDTO: sectionsLocal![index], textColor: textColor, isImageBackground: isImageBackground, elementToShow: getContent(tabletAppContext, sectionsLocal![index], isImageBackground, rawSectionsData[index]), isFromMenu: false);
|
|
||||||
},
|
|
||||||
),// For pushAndRemoveUntil
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
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(
|
|
||||||
heightFactor: 0.5,
|
|
||||||
child: Container(
|
|
||||||
//color: Colors.green,
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: HtmlWidget(
|
|
||||||
sectionsLocal![index].title!.firstWhere((translation) => translation.language == appContext.getContext().language).value ?? "",
|
|
||||||
customStylesBuilder: (element) {
|
|
||||||
return {'text-align': 'right', 'font-family': "Roboto"};
|
|
||||||
},
|
|
||||||
textStyle: TextStyle(fontSize: isPortrait ? 10 : 25),//calculateFontSize(constraints.maxWidth, constraints.maxHeight, kIsWeb ? kWebMenuTitleDetailSize : kMenuTitleDetailSize)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
) : Column(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 8,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: sectionsLocal![index].imageSource == null ? kBackgroundLight : null, // default color if no image
|
|
||||||
shape: BoxShape.rectangle,
|
|
||||||
image: sectionsLocal![index].imageSource != null || sectionsLocal![index].type == SectionType.Video ? new DecorationImage(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
image: ImageCustomProvider.getImageProvider(appContext, sectionsLocal![index].imageId, sectionsLocal![index].type == SectionType.Video ? getYoutubeThumbnailUrl(rawSectionsData[index]) : sectionsLocal![index].imageSource!),
|
|
||||||
): null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: Center(
|
|
||||||
child: HtmlWidget(
|
|
||||||
sectionsLocal![index].title!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value ?? "",
|
|
||||||
customStylesBuilder: (element) {
|
|
||||||
return {'text-align': 'center', 'font-family': "Roboto"};
|
|
||||||
},
|
|
||||||
textStyle: TextStyle(fontSize: isPortrait ? 10 : 25),//calculateFontSize(constraints.maxWidth, constraints.maxHeight, kIsWeb ? kWebMenuTitleDetailSize : kMenuTitleDetailSize)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Center(
|
|
||||||
child: Text("Aucune section à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect)),
|
child: Text("Aucune section à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
|
||||||
|
|
||||||
|
// 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(() {
|
||||||
|
//sectionSelected = sectionsLocal![index];
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) {
|
||||||
|
return SectionPageDetail(configurationDTO: configurationDTO, sectionDTO: sectionsLocal![index], textColor: textColor, isImageBackground: isImageBackground, elementToShow: getContent(tabletAppContext, sectionsLocal![index], isImageBackground, rawSectionsData[index]), isFromMenu: false);
|
||||||
|
},
|
||||||
|
),// For pushAndRemoveUntil
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
decoration: isImageBackground ? boxDecoration(appContext, sectionsLocal![index], false, rawSectionsData[index]) : null,
|
||||||
|
//color: Colors.yellow,
|
||||||
|
padding: EdgeInsets.all(isImageBackground ? 18 : 5),
|
||||||
|
child: isImageBackground ? Align(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: FractionallySizedBox(
|
||||||
|
heightFactor: 0.5,
|
||||||
|
child: Container(
|
||||||
|
//color: Colors.green,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: HtmlWidget(
|
||||||
|
sectionsLocal![index].title!.firstWhere((translation) => translation.language == appContext.getContext().language).value ?? "",
|
||||||
|
customStylesBuilder: (element) {
|
||||||
|
return {'text-align': 'right', 'font-family': "Roboto"};
|
||||||
|
},
|
||||||
|
textStyle: TextStyle(fontSize: isPortrait ? 10 : 25),//calculateFontSize(constraints.maxWidth, constraints.maxHeight, kIsWeb ? kWebMenuTitleDetailSize : kMenuTitleDetailSize)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
) : Column(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 8,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: sectionsLocal![index].imageSource == null ? kBackgroundLight : null, // default color if no image
|
||||||
|
shape: BoxShape.rectangle,
|
||||||
|
image: sectionsLocal![index].imageSource != null || sectionsLocal![index].type == SectionType.Video ? new DecorationImage(
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
image: ImageCustomProvider.getImageProvider(appContext, sectionsLocal![index].imageId, sectionsLocal![index].type == SectionType.Video ? getYoutubeThumbnailUrl(rawSectionsData[index]) : sectionsLocal![index].imageSource!),
|
||||||
|
): null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Center(
|
||||||
|
child: HtmlWidget(
|
||||||
|
sectionsLocal![index].title!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value ?? "",
|
||||||
|
customStylesBuilder: (element) {
|
||||||
|
return {'text-align': 'center', 'font-family': "Roboto"};
|
||||||
|
},
|
||||||
|
textStyle: TextStyle(fontSize: isPortrait ? 10 : 25),//calculateFontSize(constraints.maxWidth, constraints.maxHeight, kIsWeb ? kWebMenuTitleDetailSize : kMenuTitleDetailSize)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlatformInfo() async {
|
getPlatformInfo() async {
|
||||||
|
|||||||
@ -81,6 +81,10 @@ dependencies:
|
|||||||
#archive: ^3.6.1
|
#archive: ^3.6.1
|
||||||
manager_api_new:
|
manager_api_new:
|
||||||
path: ../manager-app/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:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user