Compare commits

..

No commits in common. "6efd67611f08b387fe18323b4fa179fc1657c63f" and "9f582dd8a7bc60efa9e95ccfead3dbfd9a818d3b" have entirely different histories.

6 changed files with 42 additions and 732 deletions

View File

@ -16,8 +16,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Voir les réponses",
"quiz.restart": "Recommencer",
"map.search": "Rechercher",
"back": "Retour",
"event.live": "En cours"
"back": "Retour"
}),
Translation(language: "EN", data: {
"weather.hourly": "Hourly",
@ -34,8 +33,7 @@ List<Translation> translations = [
"quiz.seeResponses": "See the answers",
"quiz.restart": "Restart",
"map.search": "Search",
"back": "Back",
"event.live": "Now on"
"back": "Back"
}),
Translation(language: "DE", data: {
"weather.hourly": "Nächste Stunden",
@ -52,8 +50,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Sehen Sie sich die Antworten an",
"quiz.restart": "Neu starten",
"map.search": "Suchen",
"back": "Zurück",
"event.live": "Läuft"
"back": "Zurück"
}),
Translation(language: "NL", data: {
"weather.hourly": "Volgende uren",
@ -70,8 +67,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Zie de antwoorden",
"quiz.restart": "Herstarten",
"map.search": "Zoeken",
"back": "Rug",
"event.live": "Nu bezig"
"back": "Rug"
}),
Translation(language: "IT", data: {
"weather.hourly": "Le prossime ore",
@ -88,8 +84,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Vedi le risposte",
"quiz.restart": "Ricomincia",
"map.search": "Cerca",
"back": "Indietro",
"event.live": "In corso"
"back": "Indietro"
}),
Translation(language: "ES", data: {
"weather.hourly": "Próximas horas",
@ -106,8 +101,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Ver las respuestas",
"quiz.restart": "Reanudar",
"map.search": "Buscar",
"back": "Atrás",
"event.live": "En curso"
"back": "Atrás"
}),
Translation(language: "PL", data: {
"weather.hourly": "Następne godziny",
@ -124,8 +118,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Zobacz odpowiedzi",
"quiz.restart": "Uruchom ponownie",
"map.search": "Szukaj",
"back": "Z powrotem",
"event.live": "Trwa"
"back": "Z powrotem"
}),
Translation(language: "CN", data: {
"weather.hourly": "接下来的几个小时",
@ -142,8 +135,7 @@ List<Translation> translations = [
"quiz.seeResponses": "查看答案",
"quiz.restart": "重新开始",
"map.search": "搜索",
"back": "后退",
"event.live": "进行中"
"back": "后退"
}),
Translation(language: "UK", data: {
"weather.hourly": "Наступні години",
@ -160,8 +152,7 @@ List<Translation> translations = [
"quiz.seeResponses": "Подивіться відповіді",
"quiz.restart": "Перезапустіть",
"map.search": "Шукати",
"back": "Назад",
"event.live": "Зараз"
"back": "Назад"
}),
Translation(language: "AR", data: {
"weather.hourly": "الساعات القادمة",
@ -178,7 +169,6 @@ List<Translation> translations = [
"quiz.seeResponses": "انظر الإجابات",
"quiz.restart": "إعادة تشغيل",
"map.search": "بحث",
"back": "خلف",
"event.live": "جارٍ الآن"
"back": "خلف"
}),
];

View File

@ -1,227 +1,47 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
//import 'dart:html';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart';
import 'package:tablet_app/Components/cached_custom_resource.dart';
import 'package:tablet_app/Helpers/ImageCustomProvider.dart';
import 'package:tablet_app/Helpers/translationHelper.dart';
import 'package:tablet_app/Models/tabletContext.dart';
import 'package:tablet_app/app_context.dart';
import 'package:tablet_app/constants.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tablet_app/Components/loading_common.dart';
/// Largeur de lecture. Une borne fait 1280 px de large : étirer le texte sur
/// toute la dalle le rend illisible, l'œil perd la ligne au retour.
const double kArticleMeasure = 720.0;
const double kMediaRailFlex = 38.0;
const double kArticleTextFlex = 62.0;
class ArticleView extends StatefulWidget {
final ArticleDTO section;
const ArticleView({Key? key, required this.section}) : super(key: key);
final SectionDTO? section;
ArticleView({this.section});
@override
State<ArticleView> createState() => _ArticleViewState();
_ArticleView createState() => _ArticleView();
}
class _ArticleViewState extends State<ArticleView> {
String _audioId = "";
ResourceDTO? _audioResource;
class _ArticleView extends State<ArticleView> {
ArticleDTO articleDTO = ArticleDTO();
@override
void initState() {
/*print(widget.section!.data);
agendaDTO = AgendaDTO.fromJson(jsonDecode(widget.section!.data!))!;
print(agendaDTO);*/
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _resolveAudio());
}
/// L'audio est porté par langue : `audioIds` est une List<TranslationDTO>.
/// Pas d'entrée pour la langue choisie = pas de lecteur, sans repli sur une
/// autre langue servir l'audio français à un néerlandophone est pire que
/// le silence.
Future<void> _resolveAudio() async {
final tabletAppContext =
Provider.of<AppContext>(context, listen: false).getContext() as TabletAppContext;
final audioId = TranslationHelper.get(widget.section.audioIds, tabletAppContext);
if (audioId.isEmpty) return;
final embedded = widget.section.contents
?.where((content) => content.resourceId == audioId && content.resource != null)
.firstOrNull;
if (embedded != null) {
if (mounted) {
setState(() {
_audioId = audioId;
_audioResource = embedded.resource;
});
}
return;
}
try {
final resource = await tabletAppContext.clientAPI!.resourceApi!.resourceGetDetail(audioId);
if (resource != null && mounted) {
setState(() {
_audioId = audioId;
_audioResource = resource;
});
}
} catch (e) {
print("ERROR _resolveAudio ArticleView");
print(e);
}
}
List<ContentDTO> _medias() {
final contents = widget.section.contents
?.where((content) => content.resource != null && content.resourceId != _audioId)
.toList() ??
[];
contents.sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0));
return contents;
}
@override
void dispose() {
//_webView = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final tabletAppContext = appContext.getContext() as TabletAppContext;
final medias = _medias();
if (medias.isEmpty) {
return _buildTextOnly(appContext, tabletAppContext);
}
final rail = Expanded(
flex: kMediaRailFlex.toInt(),
child: _buildMediaRail(appContext, tabletAppContext, medias),
);
final text = Expanded(
flex: kArticleTextFlex.toInt(),
child: _buildTextColumn(tabletAppContext, showAudio: false),
);
// isContentTop veut dire « le texte avant le média ». En portrait c'était
// au-dessus ; en paysage il n'y a plus de dessus, le drapeau devient le côté.
final contentFirst = widget.section.isContentTop ?? false;
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: contentFirst ? [text, rail] : [rail, text],
return new Center(
child: Text("TODO Article"),
);
}
Widget _buildTextOnly(AppContext appContext, TabletAppContext tabletAppContext) {
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kArticleMeasure),
child: _buildTextColumn(tabletAppContext, showAudio: true),
),
);
}
Widget _buildMediaRail(
AppContext appContext,
TabletAppContext tabletAppContext,
List<ContentDTO> medias,
) {
final main = medias.first;
final thumbs = medias.skip(1).toList();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: CachedCustomResource(
resourceDTO: main.resource!,
isAuto: false,
webView: false,
fit: BoxFit.cover,
),
),
),
if (thumbs.isNotEmpty) ...[
const SizedBox(height: 12.0),
SizedBox(
height: 90.0,
child: Row(
children: thumbs
.map((content) => Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Image(
image: ImageCustomProvider.getImageProvider(
appContext,
content.resourceId,
content.resource!.url ?? "",
),
fit: BoxFit.cover,
),
),
),
))
.toList(),
),
),
],
if (_audioResource != null) ...[
const SizedBox(height: 12.0),
_buildAudio(),
],
],
),
);
}
Widget _buildTextColumn(TabletAppContext tabletAppContext, {required bool showAudio}) {
final title = TranslationHelper.get(widget.section.title, tabletAppContext);
final content = TranslationHelper.get(widget.section.content, tabletAppContext);
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 32.0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: kArticleMeasure),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isNotEmpty)
Text(
title,
style: const TextStyle(
fontSize: kSectionTitleDetailSize,
fontWeight: FontWeight.bold,
color: kMainGrey,
),
),
if (showAudio && _audioResource != null) ...[
const SizedBox(height: 16.0),
_buildAudio(),
],
const SizedBox(height: 16.0),
HtmlWidget(
content,
textStyle: const TextStyle(
fontSize: kSectionDescriptionDetailSize,
color: kSecondGrey,
),
),
],
),
),
);
}
Widget _buildAudio() {
return CachedCustomResource(
resourceDTO: _audioResource!,
isAuto: widget.section.isReadAudioAuto ?? false,
webView: false,
);
}
}
} //_webView

View File

@ -1,465 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart' as ll;
import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart';
import 'package:tablet_app/Helpers/ImageCustomProvider.dart';
import 'package:tablet_app/Helpers/translationHelper.dart';
import 'package:tablet_app/Models/tabletContext.dart';
import 'package:tablet_app/app_context.dart';
import 'package:tablet_app/constants.dart';
/// Bande héros : 26 % de la hauteur. Sur mobile elle en prend 52 % et le
/// parallaxe récompense le défilement ; debout devant une borne, personne ne
/// défile pour découvrir qu'il y a un programme dessous.
const double kEventHeroRatio = 0.26;
const int kEventProgrammeFlex = 44;
const int kEventMapFlex = 56;
/// Couleur du bloc en cours et de ses annotations. Sémantique, distincte de
/// l'accent : c'est ce qui permet de relier une ligne du programme à un point
/// de la carte sans légende.
const Color kEventLiveColor = Color(0xFFC4622D);
/// Les coordonnées portées par `geometry.coordinates` sont en **[lng, lat]**
/// (GeoJSON). Celles de `GuidedStep` sont en [lat, lng] les deux ordres
/// coexistent en production et une confusion ne lève aucune erreur, elle
/// déplace les points. Ne pas réutiliser un helper venu d'un autre contexte.
class _Annotation {
final GeometryType? geometryType;
final Object? coordinates;
final String? polyColor;
_Annotation(this.geometryType, this.coordinates, this.polyColor);
static _Annotation fromDto(MapAnnotationDTO a) =>
_Annotation(a.geometryType, a.geometry?.coordinates, a.polyColor);
static _Annotation fromBlock(MapAnnotation a) =>
_Annotation(a.geometryType, a.geometry?.coordinates, a.polyColor);
}
class EventView extends StatefulWidget {
final SectionEventDTO section;
const EventView({Key? key, required this.section}) : super(key: key);
@override
State<EventView> createState() => _EventViewState();
}
class _EventViewState extends State<EventView> {
ProgrammeBlock? _selectedBlock;
ProgrammeBlock? get _activeBlock {
final now = DateTime.now();
return widget.section.programme
?.where((block) =>
block.startTime != null &&
block.endTime != null &&
!now.isBefore(block.startTime!) &&
!now.isAfter(block.endTime!))
.firstOrNull;
}
Color _primaryColor(TabletAppContext tabletAppContext) {
try {
return Color(int.parse(
tabletAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0],
radix: 16));
} catch (_) {
return kTestSecondColor;
}
}
String _formatTime(DateTime? date) {
if (date == null) return "";
return "${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}";
}
String _formatDate(DateTime date) =>
"${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}";
/// Les dates sont numériques, donc lisibles dans toutes les langues sans
/// traduction. C'est ce qui porte l'information « avant / pendant / après »
/// quand l'événement n'a pas lieu aujourd'hui.
String _formatDateRange() {
final start = widget.section.startDate;
final end = widget.section.endDate;
if (start == null && end == null) return "";
if (start == null) return _formatDate(end!);
if (end == null) return _formatDate(start);
return "${_formatDate(start)}${_formatDate(end)}";
}
@override
Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context);
final tabletAppContext = appContext.getContext() as TabletAppContext;
final primaryColor = _primaryColor(tabletAppContext);
final size = MediaQuery.of(context).size;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(
height: size.height * kEventHeroRatio,
child: _buildHero(appContext, tabletAppContext, primaryColor),
),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: kEventProgrammeFlex,
child: _buildProgramme(tabletAppContext, primaryColor),
),
Expanded(
flex: kEventMapFlex,
child: _buildMapColumn(tabletAppContext, primaryColor),
),
],
),
),
],
);
}
Widget _buildHero(
AppContext appContext,
TabletAppContext tabletAppContext,
Color primaryColor,
) {
final title = TranslationHelper.get(widget.section.title, tabletAppContext);
final dateRange = _formatDateRange();
return Stack(
fit: StackFit.expand,
children: [
if (widget.section.imageSource != null)
Image(
image: ImageCustomProvider.getImageProvider(
appContext,
widget.section.imageId,
widget.section.imageSource!,
),
fit: BoxFit.cover,
)
else
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
primaryColor,
Color.lerp(primaryColor, Colors.white, 0.45) ?? primaryColor,
],
),
),
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black54],
stops: [0.4, 1.0],
),
),
),
Positioned(
left: 24.0,
right: 24.0,
bottom: 18.0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: kTitleSize,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (dateRange.isNotEmpty)
Container(
margin: const EdgeInsets.only(left: 16.0),
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 6.0),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(20.0),
),
child: Text(
dateRange,
style: TextStyle(
color: primaryColor,
fontSize: 16.0,
fontWeight: FontWeight.w600,
),
),
),
],
),
),
],
);
}
Widget _buildProgramme(TabletAppContext tabletAppContext, Color primaryColor) {
final blocks = widget.section.programme ?? [];
if (blocks.isEmpty) return const SizedBox.shrink();
final active = _activeBlock;
return ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: blocks.length,
itemBuilder: (context, index) {
final block = blocks[index];
return _buildBlock(
tabletAppContext,
primaryColor,
block,
isActive: block == active,
isSelected: block == _selectedBlock,
);
},
);
}
Widget _buildBlock(
TabletAppContext tabletAppContext,
Color primaryColor,
ProgrammeBlock block, {
required bool isActive,
required bool isSelected,
}) {
final title = TranslationHelper.get(block.title, tabletAppContext);
final start = _formatTime(block.startTime);
final end = _formatTime(block.endTime);
final liveLabel = TranslationHelper.getFromLocale("event.live", tabletAppContext);
return InkWell(
onTap: () => setState(() => _selectedBlock = isSelected ? null : block),
child: Container(
margin: const EdgeInsets.only(bottom: 10.0),
padding: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 14.0),
decoration: BoxDecoration(
color: isActive ? kEventLiveColor.withValues(alpha: 0.12) : Colors.white,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(
color: isActive
? kEventLiveColor
: (isSelected ? primaryColor : kBackgroundGrey.withValues(alpha: 0.5)),
width: isSelected && !isActive ? 2.0 : 1.0,
),
),
child: Row(
children: [
SizedBox(
width: 62.0,
child: Text(
start,
style: TextStyle(
fontSize: 16.0,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
color: isActive ? kEventLiveColor : kSecondGrey,
),
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 18.0,
fontWeight: isActive ? FontWeight.bold : FontWeight.w500,
color: kMainGrey,
),
),
if (start.isNotEmpty && end.isNotEmpty)
Text(
"$start $end",
style: const TextStyle(fontSize: 14.0, color: kSecondGrey),
),
],
),
),
if (isActive && liveLabel.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0),
decoration: BoxDecoration(
color: kEventLiveColor,
borderRadius: BorderRadius.circular(12.0),
),
child: Text(
liveLabel,
style: const TextStyle(
color: Colors.white,
fontSize: 12.0,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
Widget _buildMapColumn(TabletAppContext tabletAppContext, Color primaryColor) {
final detail = _selectedBlock;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: _buildMap(primaryColor)),
if (detail != null) _buildBlockDetail(tabletAppContext, primaryColor, detail),
],
);
}
Widget _buildMap(Color primaryColor) {
final latitude = double.tryParse(widget.section.latitude ?? "");
final longitude = double.tryParse(widget.section.longitude ?? "");
final center = latitude != null && longitude != null
? ll.LatLng(latitude, longitude)
: const ll.LatLng(50.465503, 4.865105);
final global = (widget.section.globalMapAnnotations ?? []).map(_Annotation.fromDto).toList();
final blockAnnotations =
(_selectedBlock ?? _activeBlock)?.mapAnnotations?.map(_Annotation.fromBlock).toList() ?? [];
return FlutterMap(
options: MapOptions(initialCenter: center, initialZoom: 14.0),
children: [
TileLayer(
urlTemplate: "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
userAgentPackageName: "be.unov.tabletapp",
),
PolylineLayer(polylines: _polylines(global, primaryColor)),
MarkerLayer(markers: _markers(global, primaryColor)),
PolylineLayer(polylines: _polylines(blockAnnotations, kEventLiveColor)),
MarkerLayer(markers: _markers(blockAnnotations, kEventLiveColor)),
],
);
}
List<Marker> _markers(List<_Annotation> annotations, Color color) {
final markers = <Marker>[];
for (final annotation in annotations) {
if (annotation.geometryType?.value != 0) continue;
final coordinates = annotation.coordinates;
if (coordinates is! List || coordinates.length < 2) continue;
markers.add(Marker(
point: ll.LatLng(
(coordinates[1] as num).toDouble(),
(coordinates[0] as num).toDouble(),
),
width: 34.0,
height: 34.0,
child: Icon(Icons.place, color: color, size: 34.0),
));
}
return markers;
}
List<Polyline> _polylines(List<_Annotation> annotations, Color fallback) {
final polylines = <Polyline>[];
for (final annotation in annotations) {
if (annotation.geometryType?.value != 1) continue;
final coordinates = annotation.coordinates;
if (coordinates is! List) continue;
final points = <ll.LatLng>[];
for (final point in coordinates) {
if (point is List && point.length >= 2) {
points.add(ll.LatLng(
(point[1] as num).toDouble(),
(point[0] as num).toDouble(),
));
}
}
if (points.length < 2) continue;
polylines.add(Polyline(
points: points,
color: _polyColor(annotation.polyColor, fallback),
strokeWidth: 4.0,
));
}
return polylines;
}
Color _polyColor(String? hex, Color fallback) {
if (hex == null) return fallback;
final value = int.tryParse("FF${hex.replaceAll('#', '')}", radix: 16);
return value != null ? Color(value) : fallback;
}
/// Sur mobile ce détail s'ouvre en `showModalBottomSheet` : une réponse à
/// l'étroitesse. Sur une borne il y a la place à côté, donc il s'affiche sous
/// la carte sans masquer ce qu'on regardait.
Widget _buildBlockDetail(
TabletAppContext tabletAppContext,
Color primaryColor,
ProgrammeBlock block,
) {
final title = TranslationHelper.get(block.title, tabletAppContext);
final description = TranslationHelper.get(block.description, tabletAppContext);
return Container(
constraints: const BoxConstraints(maxHeight: 220.0),
padding: const EdgeInsets.all(18.0),
decoration: BoxDecoration(
color: kBackgroundLight,
border: Border(top: BorderSide(color: kBackgroundGrey.withValues(alpha: 0.5))),
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.bold,
color: kMainGrey,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: kSecondGrey),
onPressed: () => setState(() => _selectedBlock = null),
),
],
),
Text(
"${_formatTime(block.startTime)} ${_formatTime(block.endTime)}",
style: TextStyle(fontSize: 15.0, color: primaryColor, fontWeight: FontWeight.w600),
),
if (description.isNotEmpty) ...[
const SizedBox(height: 10.0),
Text(
description,
style: const TextStyle(fontSize: 16.0, color: kSecondGrey),
),
],
],
),
),
);
}
}

View File

@ -23,7 +23,6 @@ import 'package:tablet_app/Helpers/MQTTHelper.dart';
import 'package:tablet_app/Helpers/translationHelper.dart';
import 'package:tablet_app/Screens/Agenda/agenda_view.dart';
import 'package:tablet_app/Screens/Article/article_view.dart';
import 'package:tablet_app/Screens/Event/event_view.dart';
import 'package:tablet_app/Screens/Configuration/config_view.dart';
import 'package:tablet_app/Screens/MainView/section_page_detail.dart';
import 'package:tablet_app/Screens/Weather/weather_view.dart';
@ -651,12 +650,9 @@ Widget getContent(TabletAppContext tabletAppContext, SectionDTO sectionSelected,
case SectionType.Agenda :
AgendaDTO agendaDTO = AgendaDTO.fromJson(rawSectionData)!;
return AgendaView(section: agendaDTO);
case SectionType.Article :
ArticleDTO articleDTO = ArticleDTO.fromJson(rawSectionData)!;
return ArticleView(section: articleDTO);
case SectionType.Event :
SectionEventDTO eventDTO = SectionEventDTO.fromJson(rawSectionData)!;
return EventView(section: eventDTO);
/*case SectionType.article : // TODO
elementToShow = ArticleView(section: sectionSelected);
break;*/
case SectionType.Weather :
WeatherDTO weatherDTO = WeatherDTO.fromJson(rawSectionData)!;
return WeatherView(section: weatherDTO);

View File

@ -49,13 +49,11 @@ 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;
@ -66,25 +64,8 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
});
}
/// 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;
@ -104,14 +85,7 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
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(
body: SingleChildScrollView(
child: Container(
height: size.height,
width: size.width,
@ -263,7 +237,6 @@ class _SectionPageDetailState extends State<SectionPageDetail> {
),
),
),
),
/*floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
floatingActionButton: Padding(
padding: const EdgeInsets.only(top: 10.0),

View File

@ -17,10 +17,6 @@ const kBackgroundLight = Color(0xfff3f3f3);
const List<String> languages = ["FR", "NL", "EN", "DE", "IT", "ES", "PL", "CN", "AR", "UK"]; // hmmmm depends on config..
/// Délai sans interaction au bout duquel la borne revient à l'accueil, pour que
/// le visiteur suivant ne trouve pas le contenu du précédent.
const Duration kKioskIdleTimeout = Duration(minutes: 5);
const kTitleSize = 32.0;
const kDescriptionSize = 22.0;