759 lines
27 KiB
Dart
759 lines
27 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_map/flutter_map.dart';
|
||
import 'package:latlong2/latlong.dart';
|
||
import 'package:manager_api_new/api.dart';
|
||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||
import 'package:mymuseum_visitapp/Screens/Sections/Event/event_map_full_page.dart';
|
||
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_map_progression_page.dart';
|
||
import 'package:mymuseum_visitapp/constants.dart';
|
||
|
||
class EventPage extends StatefulWidget {
|
||
const EventPage({
|
||
Key? key,
|
||
required this.section,
|
||
required this.visitAppContextIn,
|
||
}) : super(key: key);
|
||
|
||
final SectionEventDTO section;
|
||
final VisitAppContext visitAppContextIn;
|
||
|
||
@override
|
||
State<EventPage> createState() => _EventPageState();
|
||
}
|
||
|
||
class _EventPageState extends State<EventPage> {
|
||
List<GuidedPathDTO> _paths = [];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadPaths();
|
||
}
|
||
|
||
Future<void> _loadPaths() async {
|
||
if (widget.section.id == null) return;
|
||
if (widget.section.parcoursIds?.isEmpty ?? true) return;
|
||
try {
|
||
final paths = await widget.visitAppContextIn.clientAPI.sectionMapApi!
|
||
.sectionMapGetAllGuidedPathFromSection(widget.section.id!);
|
||
if (mounted) setState(() => _paths = paths ?? []);
|
||
} catch (_) {}
|
||
}
|
||
|
||
ProgrammeBlock? get _activeBlock {
|
||
final now = DateTime.now();
|
||
return widget.section.programme
|
||
?.where((b) =>
|
||
b.startTime != null &&
|
||
b.endTime != null &&
|
||
!now.isBefore(b.startTime!) &&
|
||
!now.isAfter(b.endTime!))
|
||
.firstOrNull;
|
||
}
|
||
|
||
String _formatTime(DateTime? dt) {
|
||
if (dt == null) return '';
|
||
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||
}
|
||
|
||
String _formatDate(DateTime dt) =>
|
||
'${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}';
|
||
|
||
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)}';
|
||
}
|
||
|
||
MapDTO _minimalMapDTO() => MapDTO(
|
||
latitude: widget.section.latitude,
|
||
longitude: widget.section.longitude,
|
||
zoom: 14,
|
||
);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final hasProgramme = widget.section.programme?.isNotEmpty ?? false;
|
||
final annotations = widget.section.globalMapAnnotations ?? [];
|
||
final hasMapSection = annotations.isNotEmpty ||
|
||
widget.section.baseSectionMapId != null ||
|
||
widget.section.latitude != null ||
|
||
_paths.isNotEmpty;
|
||
final hasDescription = widget.section.description?.isNotEmpty ?? false;
|
||
|
||
return Scaffold(
|
||
backgroundColor: const Color(0xFF111111),
|
||
body: CustomScrollView(
|
||
slivers: [
|
||
_buildHero(context),
|
||
SliverToBoxAdapter(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
if (hasProgramme) _buildProgrammeSection(),
|
||
if (hasMapSection) _buildMapParcoursSection(context, annotations),
|
||
if (hasDescription) _buildDescriptionSection(),
|
||
const SizedBox(height: 48),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ─── Hero ──────────────────────────────────────────────────────────────────
|
||
|
||
Widget _buildHero(BuildContext context) {
|
||
final title = _safeTranslate(widget.section.title);
|
||
final dateRange = _formatDateRange();
|
||
final expandedHeight = MediaQuery.of(context).size.height * 0.52;
|
||
|
||
return SliverAppBar(
|
||
automaticallyImplyLeading: false,
|
||
expandedHeight: expandedHeight,
|
||
pinned: true,
|
||
backgroundColor: kMainColor,
|
||
title: Text(
|
||
title,
|
||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
leading: Padding(
|
||
padding: const EdgeInsets.all(8),
|
||
child: GestureDetector(
|
||
onTap: () => Navigator.of(context).pop(),
|
||
child: Container(
|
||
decoration: const BoxDecoration(color: kMainColor, shape: BoxShape.circle),
|
||
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||
),
|
||
),
|
||
),
|
||
flexibleSpace: FlexibleSpaceBar(
|
||
collapseMode: CollapseMode.parallax,
|
||
background: Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
if (widget.section.imageSource != null)
|
||
Image.network(
|
||
widget.section.imageSource!,
|
||
fit: BoxFit.cover,
|
||
errorBuilder: (_, __, ___) => _gradientBackground(),
|
||
)
|
||
else
|
||
_gradientBackground(),
|
||
const DecoratedBox(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [Colors.transparent, Colors.black87],
|
||
stops: [0.35, 1.0],
|
||
),
|
||
),
|
||
),
|
||
Positioned(
|
||
bottom: 20,
|
||
left: 16,
|
||
right: 16,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 26,
|
||
fontWeight: FontWeight.bold,
|
||
height: 1.2,
|
||
),
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
if (dateRange.isNotEmpty) ...[
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: kMainColor.withOpacity(0.85),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Text(
|
||
dateRange,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _gradientBackground() => Container(
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [kMainColor, kSecondColor],
|
||
),
|
||
),
|
||
);
|
||
|
||
// ─── Programme ─────────────────────────────────────────────────────────────
|
||
|
||
Widget _buildProgrammeSection() {
|
||
final blocks = widget.section.programme!;
|
||
final active = _activeBlock;
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 28),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_sectionHeader(Icons.schedule, 'Programme'),
|
||
const SizedBox(height: 8),
|
||
...blocks.asMap().entries.map(
|
||
(e) => _buildProgrammeBlock(e.value, e.value == active, e.key == blocks.length - 1),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildProgrammeBlock(ProgrammeBlock block, bool isActive, bool isLast) {
|
||
final title = _safeTranslate(block.title);
|
||
final startStr = _formatTime(block.startTime);
|
||
final endStr = _formatTime(block.endTime);
|
||
|
||
return IntrinsicHeight(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
width: 56,
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(top: 14),
|
||
child: Text(
|
||
startStr,
|
||
style: TextStyle(
|
||
color: isActive ? kMainColor : Colors.grey[500],
|
||
fontSize: 12,
|
||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
),
|
||
Column(
|
||
children: [
|
||
const SizedBox(height: 16),
|
||
Container(
|
||
width: 12,
|
||
height: 12,
|
||
decoration: BoxDecoration(
|
||
color: isActive ? kMainColor : Colors.grey[700],
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
if (!isLast)
|
||
Expanded(
|
||
child: Container(width: 2, color: Colors.grey[800]),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () => _showBlockDetail(block),
|
||
child: Container(
|
||
margin: EdgeInsets.only(bottom: isLast ? 0 : 12, right: 16, top: 4),
|
||
padding: const EdgeInsets.all(14),
|
||
decoration: BoxDecoration(
|
||
color: isActive ? kMainColor : const Color(0xFF1e1e1e),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: isActive ? null : Border.all(color: Colors.grey[800]!, width: 1),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
title,
|
||
style: TextStyle(
|
||
color: isActive ? Colors.white : Colors.white.withOpacity(0.9),
|
||
fontWeight: FontWeight.w600,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
if (startStr.isNotEmpty && endStr.isNotEmpty) ...[
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
'$startStr – $endStr',
|
||
style: TextStyle(
|
||
color: isActive ? Colors.white70 : Colors.grey[600],
|
||
fontSize: 12,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
if (isActive)
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.25),
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: const Text(
|
||
'En cours',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
)
|
||
else
|
||
Icon(Icons.chevron_right, color: Colors.grey[700], size: 18),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showBlockDetail(ProgrammeBlock block) {
|
||
final title = _safeTranslate(block.title);
|
||
final desc = _safeTranslate(block.description);
|
||
final annotations = block.mapAnnotations ?? [];
|
||
|
||
showModalBottomSheet(
|
||
context: context,
|
||
backgroundColor: const Color(0xFF1e1e1e),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
),
|
||
isScrollControlled: true,
|
||
builder: (_) => Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 40),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 40,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey[700],
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
title,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
if (desc.isNotEmpty) ...[
|
||
const SizedBox(height: 10),
|
||
Text(
|
||
desc,
|
||
style: TextStyle(color: Colors.grey[400], fontSize: 14, height: 1.6),
|
||
),
|
||
],
|
||
if (annotations.isNotEmpty) ...[
|
||
const SizedBox(height: 16),
|
||
Divider(color: Colors.grey[800]),
|
||
const SizedBox(height: 8),
|
||
...annotations.map((a) {
|
||
final label = _safeTranslate(a.label);
|
||
if (label.isEmpty) return const SizedBox.shrink();
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 8),
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.place, color: kMainColor, size: 16),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
label,
|
||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ─── Carte & Parcours ──────────────────────────────────────────────────────
|
||
|
||
Widget _buildMapParcoursSection(BuildContext context, List<MapAnnotationDTO> annotations) {
|
||
final lat = double.tryParse(widget.section.latitude ?? '');
|
||
final lng = double.tryParse(widget.section.longitude ?? '');
|
||
final center = (lat != null && lng != null) ? LatLng(lat, lng) : const LatLng(50.85, 4.35);
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 28),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_sectionHeader(Icons.map_outlined, 'Carte & Parcours'),
|
||
const SizedBox(height: 12),
|
||
GestureDetector(
|
||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||
builder: (_) => EventMapFullPage(
|
||
section: widget.section,
|
||
visitAppContextIn: widget.visitAppContextIn,
|
||
),
|
||
)),
|
||
child: Container(
|
||
height: 220,
|
||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||
clipBehavior: Clip.antiAlias,
|
||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(16)),
|
||
child: Stack(
|
||
children: [
|
||
FlutterMap(
|
||
options: MapOptions(
|
||
initialCenter: center,
|
||
initialZoom: 14,
|
||
interactionOptions:
|
||
const InteractionOptions(flags: InteractiveFlag.none),
|
||
),
|
||
children: [
|
||
TileLayer(
|
||
urlTemplate:
|
||
'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}',
|
||
userAgentPackageName: 'be.unov.myinfomate.visitapp',
|
||
),
|
||
if (annotations.isNotEmpty) ...[
|
||
MarkerLayer(markers: _buildMarkers(annotations)),
|
||
PolylineLayer(polylines: _buildPolylines(annotations)),
|
||
],
|
||
],
|
||
),
|
||
Positioned(
|
||
bottom: 8,
|
||
right: 8,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||
decoration: BoxDecoration(
|
||
color: Colors.black54,
|
||
borderRadius: BorderRadius.circular(12),
|
||
),
|
||
child: const Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.open_in_full, color: Colors.white, size: 12),
|
||
SizedBox(width: 4),
|
||
Text(
|
||
'Agrandir',
|
||
style: TextStyle(color: Colors.white, fontSize: 11),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
if (_paths.isNotEmpty) ...[
|
||
const SizedBox(height: 16),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Text(
|
||
'${_paths.length} parcours disponible${_paths.length > 1 ? 's' : ''}',
|
||
style: TextStyle(color: Colors.grey[400], fontSize: 13),
|
||
),
|
||
GestureDetector(
|
||
onTap: () => _showParcoursList(context),
|
||
child: Text(
|
||
'Voir tout',
|
||
style: TextStyle(
|
||
color: kMainColor,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
SizedBox(
|
||
height: 84,
|
||
child: ListView.separated(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
scrollDirection: Axis.horizontal,
|
||
itemCount: _paths.length,
|
||
separatorBuilder: (_, __) => const SizedBox(width: 10),
|
||
itemBuilder: (_, i) => _buildParcoursChip(context, _paths[i]),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
List<Marker> _buildMarkers(List<MapAnnotationDTO> annotations) {
|
||
final markers = <Marker>[];
|
||
for (final ann in annotations) {
|
||
if (ann.geometryType?.value != 0) continue;
|
||
final coords = ann.geometry?.coordinates;
|
||
if (coords is! List || coords.length < 2) continue;
|
||
final lat = (coords[1] as num).toDouble();
|
||
final lng = (coords[0] as num).toDouble();
|
||
markers.add(Marker(
|
||
point: LatLng(lat, lng),
|
||
width: 30,
|
||
height: 30,
|
||
child: Icon(Icons.place, color: kMainColor, size: 30),
|
||
));
|
||
}
|
||
return markers;
|
||
}
|
||
|
||
List<Polyline> _buildPolylines(List<MapAnnotationDTO> annotations) {
|
||
final lines = <Polyline>[];
|
||
for (final ann in annotations) {
|
||
if (ann.geometryType?.value != 1) continue;
|
||
final coords = ann.geometry?.coordinates;
|
||
if (coords is! List) continue;
|
||
final points = <LatLng>[];
|
||
for (final c in coords) {
|
||
if (c is List && c.length >= 2) {
|
||
points.add(LatLng((c[1] as num).toDouble(), (c[0] as num).toDouble()));
|
||
}
|
||
}
|
||
if (points.length < 2) continue;
|
||
final color = ann.polyColor != null ? _hexColor(ann.polyColor!) : kMainColor;
|
||
lines.add(Polyline(points: points, color: color, strokeWidth: 3.5));
|
||
}
|
||
return lines;
|
||
}
|
||
|
||
Color _hexColor(String hex) {
|
||
final v = int.tryParse('FF${hex.replaceAll('#', '')}', radix: 16);
|
||
return v != null ? Color(v) : kMainColor;
|
||
}
|
||
|
||
Widget _buildParcoursChip(BuildContext context, GuidedPathDTO path) {
|
||
final title = _safeTranslate(path.title);
|
||
final stepCount = path.steps?.length ?? 0;
|
||
|
||
return GestureDetector(
|
||
onTap: () => Navigator.of(context).push(MaterialPageRoute(
|
||
builder: (_) => GuidedPathMapProgressionPage(
|
||
path: path,
|
||
mapDTO: _minimalMapDTO(),
|
||
visitAppContext: widget.visitAppContextIn,
|
||
),
|
||
)),
|
||
child: Container(
|
||
width: 160,
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF1e1e1e),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Colors.grey[800]!, width: 1),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(Icons.route, color: kMainColor, size: 15),
|
||
const SizedBox(width: 5),
|
||
Text(
|
||
'$stepCount étape${stepCount > 1 ? 's' : ''}',
|
||
style: TextStyle(color: Colors.grey[500], fontSize: 11),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
title,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _showParcoursList(BuildContext context) {
|
||
showModalBottomSheet(
|
||
context: context,
|
||
backgroundColor: const Color(0xFF1e1e1e),
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
),
|
||
builder: (_) => Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Center(
|
||
child: Container(
|
||
width: 40,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey[700],
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 16),
|
||
Row(
|
||
children: [
|
||
Icon(Icons.route, size: 20, color: kMainColor),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'Parcours disponibles',
|
||
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
..._paths.map((path) {
|
||
final title = _safeTranslate(path.title);
|
||
final desc = _safeTranslate(path.description);
|
||
final stepCount = path.steps?.length ?? 0;
|
||
return ListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
leading: Container(
|
||
width: 40,
|
||
height: 40,
|
||
decoration: BoxDecoration(
|
||
color: kMainColor.withOpacity(0.15),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(Icons.map_outlined, color: kMainColor, size: 20),
|
||
),
|
||
title: Text(
|
||
title,
|
||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500),
|
||
),
|
||
subtitle: Text(
|
||
desc.isNotEmpty ? desc : '$stepCount étape${stepCount > 1 ? 's' : ''}',
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(color: Colors.grey[500]),
|
||
),
|
||
trailing: Icon(Icons.arrow_forward_ios, size: 14, color: Colors.grey[600]),
|
||
onTap: () {
|
||
Navigator.of(context).pop();
|
||
Navigator.of(context).push(MaterialPageRoute(
|
||
builder: (_) => GuidedPathMapProgressionPage(
|
||
path: path,
|
||
mapDTO: _minimalMapDTO(),
|
||
visitAppContext: widget.visitAppContextIn,
|
||
),
|
||
));
|
||
},
|
||
);
|
||
}),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ─── Description ───────────────────────────────────────────────────────────
|
||
|
||
Widget _buildDescriptionSection() {
|
||
final desc = _safeTranslate(widget.section.description);
|
||
if (desc.isEmpty) return const SizedBox.shrink();
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 28, 16, 0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
const Icon(Icons.info_outline, color: kMainColor, size: 20),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'À propos',
|
||
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Text(desc, style: TextStyle(color: Colors.grey[300], fontSize: 14, height: 1.6)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ─── Helpers ───────────────────────────────────────────────────────────────
|
||
|
||
Widget _sectionHeader(IconData icon, String label) => Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
child: Row(
|
||
children: [
|
||
Icon(icon, color: kMainColor, size: 20),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
label,
|
||
style: const TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.bold),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
String _safeTranslate(List<TranslationDTO>? list) {
|
||
if (list == null || list.isEmpty) return '';
|
||
try {
|
||
return TranslationHelper.get(list, widget.visitAppContextIn);
|
||
} catch (_) {
|
||
return list.first.value ?? '';
|
||
}
|
||
}
|
||
}
|