flutter build apk --debug vert. Un visiteur part sans fermer l'écran ; le suivant trouvait le contenu du précédent. Écrit une seule fois, dans section_page_detail : il enveloppe les treize types et portait déjà les crochets initState/dispose des statistiques. L'écrire dans les écrans eux-mêmes l'aurait fait écrire treize fois. Bénéfice de bord, et ce n'est pas un détail : le retour automatique passe par le même dispose, donc il émet un sectionLeave avec sa durée réelle. Sans lui, une consultation abandonnée restait ouverte jusqu'au visiteur suivant et gonflait la durée mesurée d'autant. K8 répare donc une statistique en plus d'un écran. Listener plutôt que GestureDetector pour réarmer le minuteur : il observe les pointeurs sans entrer en concurrence avec les gestes des écrans en dessous. Une carte qu'on fait glisser ou un quiz qu'on tape ne doivent rien perdre. onPointerDown et onPointerMove, donc le défilement d'un long article compte comme une interaction. Délai dans constants.dart (kKioskIdleTimeout) plutôt qu'en dur : c'est un réglage de comportement de borne, pas une valeur d'implémentation. Non vérifié : le comportement réel après 5 minutes, qui demande un device et cinq minutes d'attente. popUntil(isFirst) suppose que l'accueil est bien la première route de la pile — à confirmer au §0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
313 lines
15 KiB
Dart
313 lines
15 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
import 'package:enum_to_string/enum_to_string.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import 'package:manager_api_new/api.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:tablet_app/Components/loading_common.dart';
|
|
import 'package:tablet_app/Components/multi_select_container.dart';
|
|
import 'package:tablet_app/Helpers/ImageCustomProvider.dart';
|
|
import 'package:tablet_app/Helpers/translationHelper.dart';
|
|
import 'package:tablet_app/Models/map-marker.dart';
|
|
import 'package:tablet_app/Models/tabletContext.dart';
|
|
import 'package:tablet_app/Screens/MainView/main_view.dart';
|
|
import 'package:tablet_app/Screens/Map/geo_point_filter.dart';
|
|
import 'package:tablet_app/Screens/Map/map_context.dart';
|
|
import 'package:html/parser.dart' show parse;
|
|
import 'package:tablet_app/Screens/Menu/menu_view.dart';
|
|
import 'package:tablet_app/Services/statisticsService.dart';
|
|
import 'package:tablet_app/app_context.dart';
|
|
import 'package:tablet_app/constants.dart';
|
|
|
|
class SectionPageDetail extends StatefulWidget {
|
|
final ConfigurationDTO configurationDTO;
|
|
final SectionDTO sectionDTO;
|
|
final Uint8List? selectedMarkerIcon;
|
|
final Color textColor;
|
|
final bool isImageBackground;
|
|
final bool isFromMenu;
|
|
final Widget elementToShow;
|
|
const SectionPageDetail({
|
|
Key? key,
|
|
required this.configurationDTO,
|
|
required this.sectionDTO,
|
|
this.selectedMarkerIcon,
|
|
required this.textColor,
|
|
required this.isImageBackground,
|
|
required this.isFromMenu,
|
|
required this.elementToShow,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
_SectionPageDetailState createState() => _SectionPageDetailState();
|
|
}
|
|
|
|
class _SectionPageDetailState extends State<SectionPageDetail> {
|
|
bool init = false;
|
|
DateTime? _sectionOpenTime;
|
|
StatisticsService? _statisticsService;
|
|
Timer? _idleTimer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_sectionOpenTime = DateTime.now();
|
|
_restartIdleTimer();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final tabletAppContext = Provider.of<AppContext>(context, listen: false).getContext() as TabletAppContext;
|
|
_statisticsService = tabletAppContext.statisticsService;
|
|
_statisticsService?.track(
|
|
VisitEventType.sectionView,
|
|
sectionId: widget.sectionDTO.id,
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Un visiteur part sans fermer l'écran ; le suivant trouve le contenu du
|
|
/// précédent. Le minuteur vit ici parce que cet écran enveloppe les treize
|
|
/// types : l'écrire dans les écrans eux-mêmes le ferait écrire treize fois.
|
|
void _restartIdleTimer() {
|
|
_idleTimer?.cancel();
|
|
_idleTimer = Timer(kKioskIdleTimeout, _returnHome);
|
|
}
|
|
|
|
void _returnHome() {
|
|
if (!mounted) return;
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_idleTimer?.cancel();
|
|
// Le retour automatique passe par ce dispose, donc il émet un sectionLeave
|
|
// avec sa durée réelle. Sans lui, une consultation abandonnée resterait
|
|
// ouverte jusqu'au visiteur suivant et fausserait la statistique.
|
|
final duration = _sectionOpenTime != null
|
|
? DateTime.now().difference(_sectionOpenTime!).inSeconds
|
|
: null;
|
|
_statisticsService?.track(
|
|
VisitEventType.sectionLeave,
|
|
sectionId: widget.sectionDTO.id,
|
|
durationSeconds: duration,
|
|
);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final appContext = Provider.of<AppContext>(context);
|
|
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
|
|
Size size = MediaQuery.of(context).size;
|
|
|
|
return Scaffold(
|
|
resizeToAvoidBottomInset: false,
|
|
// Listener plutôt que GestureDetector : il observe les pointeurs sans
|
|
// entrer en concurrence avec les gestes des écrans en dessous — une carte
|
|
// qu'on fait glisser ou un quiz qu'on tape ne doivent rien perdre.
|
|
body: Listener(
|
|
behavior: HitTestBehavior.translucent,
|
|
onPointerDown: (_) => _restartIdleTimer(),
|
|
onPointerMove: (_) => _restartIdleTimer(),
|
|
child: SingleChildScrollView(
|
|
child: Container(
|
|
height: size.height,
|
|
width: size.width,
|
|
/*color: widget.configurationDTO.imageId == null ? widget.configurationDTO.secondaryColor != null ? new Color(int.parse(widget.configurationDTO.secondaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kBackgroundGrey : null,
|
|
decoration: widget.configurationDTO.imageId != null ? BoxDecoration(
|
|
image: new DecorationImage(
|
|
fit: BoxFit.cover,
|
|
colorFilter: new ColorFilter.mode(Colors.white.withOpacity(0.8), BlendMode.lighten),
|
|
image: ImageCustomProvider.getImageProvider(appContext, widget.configurationDTO.imageId!, widget.configurationDTO.imageSource!),
|
|
),
|
|
) : null,*/
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Container(
|
|
decoration : BoxDecoration(
|
|
color: kBackgroundLight,
|
|
shape: BoxShape.rectangle,
|
|
borderRadius: BorderRadius.circular(tabletAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 30.0),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: kBackgroundSecondGrey,
|
|
spreadRadius: 0.5,
|
|
blurRadius: 5,
|
|
offset: Offset(0, 1.5), // changes position of shadow
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
//height: size.height * 0.1,
|
|
child: Container(
|
|
//color: Colors.greenAccent,
|
|
width: size.width,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
//color: Colors.grey,
|
|
//width: size.width * 0.8,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 15.0, top: 10.0, bottom: 10.0),
|
|
child: Container(
|
|
//color: Colors.grey,
|
|
child: AspectRatio(
|
|
aspectRatio: 4 / 4,
|
|
child: widget.isImageBackground ? Container(
|
|
/*width: 125,
|
|
height: 125,*/
|
|
decoration: boxDecoration(appContext, widget.sectionDTO, true),
|
|
) : Container(
|
|
decoration: BoxDecoration(
|
|
color: widget.sectionDTO.imageSource == null && widget.sectionDTO.type != SectionType.Video ? kBackgroundLight : null, // default color if no image
|
|
shape: BoxShape.rectangle,
|
|
image: widget.sectionDTO.imageSource != null || widget.sectionDTO.type == SectionType.Video ? new DecorationImage(
|
|
fit: widget.sectionDTO.type == SectionType.Video && widget.sectionDTO.imageId == null ? BoxFit.contain : BoxFit.cover,
|
|
image: ImageCustomProvider.getImageProvider(appContext, widget.sectionDTO.imageId, widget.sectionDTO.type == SectionType.Video ? getYoutubeThumbnailUrl(widget.sectionDTO) : widget.sectionDTO.imageSource!),
|
|
): null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
//width: size.width,
|
|
//color: Colors.green,
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Align(
|
|
alignment: Alignment.center,
|
|
child: HtmlWidget(
|
|
widget.sectionDTO.title!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value ?? "",
|
|
textStyle: new TextStyle(fontSize: kIsWeb ? kWebSectionTitleDetailSize : kSectionTitleDetailSize, /*color: widget.textColor,*/ fontFamily: 'Roboto'),
|
|
)
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 15.0),
|
|
child: Container(
|
|
height: size.height *0.08,
|
|
width: size.width *0.08,
|
|
child: FittedBox(
|
|
child: FloatingActionButton.extended(
|
|
backgroundColor: kBackgroundColor,
|
|
focusColor: kBackgroundColor,
|
|
splashColor: kBackgroundColor,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(tabletAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 30.0)),
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
/*Navigator.of(context).pushReplacement(
|
|
PageRouteBuilder(
|
|
transitionDuration: Duration(milliseconds: 500), // Durée de la transition en millisecondes
|
|
pageBuilder: (_, __, ___) => MainViewWidget(), // Page précédente
|
|
transitionsBuilder: (_, animation, __, child) {
|
|
return FadeTransition(
|
|
opacity: animation,
|
|
child: child,
|
|
);
|
|
},
|
|
),
|
|
);*/
|
|
},
|
|
icon: Icon(Icons.arrow_back, color: Colors.grey),
|
|
label: Text(TranslationHelper.getFromLocale(widget.isFromMenu ? "back" : "menu", appContext.getContext()), style: TextStyle(color: Colors.black))
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 9,
|
|
child: Padding(
|
|
padding: widget.sectionDTO.type != SectionType.Slider ? const EdgeInsets.only(left: 15.0, right: 15.0, top: 10.0) : const EdgeInsets.only(top: 10.0),
|
|
child: Container(
|
|
width: size.width,
|
|
//height: size.height * 0.9,
|
|
/*decoration: sectionSelected!.type != SectionType.Video && sectionSelected!.type != SectionType.Web && sectionSelected!.type != SectionType.Slider && sectionSelected!.type != SectionType.Map ? BoxDecoration(
|
|
//color: kBackgroundLight,
|
|
shape: BoxShape.rectangle,
|
|
borderRadius: BorderRadius.circular(tabletAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 30.0),
|
|
/*boxShadow: [
|
|
BoxShadow(
|
|
color: kBackgroundSecondGrey,
|
|
spreadRadius: 0.5,
|
|
blurRadius: 5,
|
|
offset: Offset(0, 1.5), // changes position of shadow
|
|
),
|
|
],*/
|
|
) :null,*/
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(bottom: 8.0),
|
|
child: widget.elementToShow),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
/*floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
|
|
floatingActionButton: Padding(
|
|
padding: const EdgeInsets.only(top: 10.0),
|
|
child: Container(
|
|
height: size.height *0.08,
|
|
width: size.width *0.08,
|
|
child: FittedBox(
|
|
child: FloatingActionButton.extended(
|
|
backgroundColor: kBackgroundColor,
|
|
focusColor: kBackgroundColor,
|
|
splashColor: kBackgroundColor,
|
|
onPressed: () {
|
|
setState(() {
|
|
sectionSelected = null;
|
|
});
|
|
},
|
|
icon: Icon(Icons.arrow_back, color: Colors.grey),
|
|
label: Text("Menu", style: TextStyle(color: Colors.black))
|
|
),
|
|
),
|
|
),
|
|
),*/
|
|
);
|
|
}
|
|
}
|
|
|
|
boxDecoration(AppContext appContext, SectionDTO section, bool isSelected) {
|
|
TabletAppContext tabletAppContext = appContext.getContext() as TabletAppContext;
|
|
return BoxDecoration(
|
|
color: !isSelected ? kBackgroundLight : section.imageSource == null ? kBackgroundLight : null,
|
|
shape: BoxShape.rectangle,
|
|
borderRadius: BorderRadius.circular(tabletAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 30.0),
|
|
image: section.imageSource != null ? new DecorationImage(
|
|
fit: BoxFit.cover,
|
|
colorFilter: !isSelected? new ColorFilter.mode(kBackgroundLight.withValues(alpha: 0.2), BlendMode.dstATop) : null,
|
|
image: ImageCustomProvider.getImageProvider(appContext, section.imageId!, section.imageSource!),
|
|
): null,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: kBackgroundSecondGrey,
|
|
spreadRadius: 0.5,
|
|
blurRadius: 5,
|
|
offset: Offset(0, 1.5), // changes position of shadow
|
|
),
|
|
],
|
|
);
|
|
} |