817 lines
27 KiB
Dart
817 lines
27 KiB
Dart
import 'package:flutter/material.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/GuidedPath/guided_path_content_progression_page.dart';
|
|
import 'package:mymuseum_visitapp/Screens/Sections/GuidedPath/guided_path_map_progression_page.dart';
|
|
import 'package:mymuseum_visitapp/constants.dart';
|
|
|
|
class ParcoursPage extends StatefulWidget {
|
|
final ParcoursDTO section;
|
|
final VisitAppContext visitAppContextIn;
|
|
|
|
const ParcoursPage({
|
|
Key? key,
|
|
required this.section,
|
|
required this.visitAppContextIn,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
State<ParcoursPage> createState() => _ParcoursPageState();
|
|
}
|
|
|
|
class _ParcoursPageState extends State<ParcoursPage> {
|
|
List<GuidedPathDTO> _paths = [];
|
|
bool _loading = true;
|
|
MapDTO? _baseMap;
|
|
|
|
static const _bg = Color(0xFFF6F3EE);
|
|
static const _inkDark = Color(0xFF1E2A33);
|
|
static const _inkMid = Color(0xFF46555F);
|
|
static const _inkMuted = Color(0xFF6B7B86);
|
|
static const _borderLight = Color(0xFFEFEAE2);
|
|
static const _gold = Color(0xFFD4AF37);
|
|
|
|
Color get _primary => kMainColor;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadPaths();
|
|
}
|
|
|
|
Future<void> _loadPaths() async {
|
|
if (widget.section.id == null) {
|
|
setState(() => _loading = false);
|
|
return;
|
|
}
|
|
try {
|
|
final api = widget.visitAppContextIn.clientAPI.sectionParcoursApi!;
|
|
final paths = await api.sectionParcoursGetAllGuidedPathFromSection(widget.section.id!);
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_paths = (paths ?? [])..sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0));
|
|
_loading = false;
|
|
});
|
|
if (widget.section.showMap == true && widget.section.baseSectionMapId != null) {
|
|
_loadBaseMap();
|
|
}
|
|
} catch (_) {
|
|
if (mounted) setState(() => _loading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadBaseMap() async {
|
|
try {
|
|
final rawMap = await widget.visitAppContextIn.clientAPI.sectionApi!
|
|
.sectionGetDetail(widget.section.baseSectionMapId!);
|
|
if (rawMap != null && mounted) {
|
|
setState(() => _baseMap = MapDTO.fromJson(rawMap));
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
String get _sectionTitle =>
|
|
TranslationHelper.get(widget.section.title, widget.visitAppContextIn);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: _bg,
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
_buildHero(context),
|
|
Expanded(child: _buildPathList()),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHero(BuildContext context) {
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
height: 220,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
_primary.withOpacity(0.85),
|
|
_primary,
|
|
],
|
|
),
|
|
),
|
|
child: widget.section.imageSource != null
|
|
? Image.network(
|
|
widget.section.imageSource!,
|
|
width: double.infinity,
|
|
height: 220,
|
|
fit: BoxFit.cover,
|
|
color: _primary.withOpacity(0.5),
|
|
colorBlendMode: BlendMode.darken,
|
|
errorBuilder: (_, __, ___) => const SizedBox(),
|
|
)
|
|
: null,
|
|
),
|
|
Container(
|
|
height: 220,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Colors.transparent, Colors.black.withOpacity(0.45)],
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 12,
|
|
left: 16,
|
|
child: GestureDetector(
|
|
onTap: () => Navigator.of(context).pop(),
|
|
child: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 18),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 20,
|
|
bottom: 20,
|
|
right: 20,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (_paths.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.92),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'${_paths.length} parcours',
|
|
style: TextStyle(
|
|
color: _primary,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.4,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
_sectionTitle,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 26,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.5,
|
|
height: 1.1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPathList() {
|
|
if (_loading) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (_paths.isEmpty) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.route_outlined, size: 48, color: _inkMuted),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Aucun parcours disponible',
|
|
style: TextStyle(color: _inkMuted, fontSize: 15),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 4),
|
|
child: Text(
|
|
'Choisir un parcours',
|
|
style: TextStyle(
|
|
color: _inkDark,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
|
|
itemCount: _paths.length,
|
|
itemBuilder: (_, i) => _buildPathCard(_paths[i]),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPathCard(GuidedPathDTO path) {
|
|
final title = TranslationHelper.get(path.title, widget.visitAppContextIn);
|
|
final desc = TranslationHelper.get(path.description, widget.visitAppContextIn);
|
|
final stepCount = path.steps?.length ?? 0;
|
|
final isGame = path.isGameMode == true;
|
|
final durationMin = path.estimatedDurationMinutes;
|
|
|
|
return GestureDetector(
|
|
onTap: () => _onPathTap(path),
|
|
child: Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: _borderLight),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.06),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 88,
|
|
height: 88,
|
|
decoration: BoxDecoration(
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(19),
|
|
bottomLeft: Radius.circular(19),
|
|
),
|
|
gradient: isGame
|
|
? const LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [Color(0xFF1a2436), Color(0xFF0B1018)],
|
|
)
|
|
: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
_primary.withOpacity(0.8),
|
|
_primary,
|
|
],
|
|
),
|
|
),
|
|
child: Icon(
|
|
isGame ? Icons.lock_outlined : Icons.route_outlined,
|
|
color: isGame ? _gold : Colors.white.withOpacity(0.9),
|
|
size: 32,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.fromLTRB(13, 12, 12, 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: isGame
|
|
? _gold.withOpacity(0.15)
|
|
: _primary.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(7),
|
|
),
|
|
child: Text(
|
|
isGame ? 'ESCAPE GAME' : 'DÉCOUVERTE',
|
|
style: TextStyle(
|
|
color: isGame ? _gold : _primary,
|
|
fontSize: 10.5,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.4,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
color: _inkDark,
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w600,
|
|
height: 1.15,
|
|
),
|
|
),
|
|
if (desc.isNotEmpty) ...[
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
desc,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: const TextStyle(
|
|
color: _inkMuted,
|
|
fontSize: 12.5,
|
|
height: 1.35,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
Icon(Icons.flag_outlined, size: 12, color: _inkMuted),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'$stepCount étape${stepCount > 1 ? 's' : ''}',
|
|
style: const TextStyle(
|
|
color: _inkMuted,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
if (durationMin != null) ...[
|
|
const SizedBox(width: 10),
|
|
Icon(Icons.access_time_outlined, size: 12, color: _inkMuted),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
'~$durationMin min',
|
|
style: const TextStyle(
|
|
color: _inkMuted,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 14),
|
|
child: Icon(Icons.arrow_forward_ios,
|
|
size: 14, color: _inkMuted),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onPathTap(GuidedPathDTO path) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
backgroundColor: Colors.transparent,
|
|
builder: (_) => _ParcoursPathStartSheet(
|
|
path: path,
|
|
section: widget.section,
|
|
baseMap: _baseMap,
|
|
visitAppContext: widget.visitAppContextIn,
|
|
primaryColor: _primary,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ParcoursPathStartSheet extends StatelessWidget {
|
|
final GuidedPathDTO path;
|
|
final ParcoursDTO section;
|
|
final MapDTO? baseMap;
|
|
final VisitAppContext visitAppContext;
|
|
final Color primaryColor;
|
|
|
|
static const _bg = Color(0xFFF6F3EE);
|
|
static const _inkDark = Color(0xFF1E2A33);
|
|
static const _inkMid = Color(0xFF46555F);
|
|
static const _inkMuted = Color(0xFF6B7B86);
|
|
static const _gold = Color(0xFFD4AF37);
|
|
|
|
const _ParcoursPathStartSheet({
|
|
Key? key,
|
|
required this.path,
|
|
required this.section,
|
|
required this.baseMap,
|
|
required this.visitAppContext,
|
|
required this.primaryColor,
|
|
}) : super(key: key);
|
|
|
|
bool get _isGame => path.isGameMode == true;
|
|
bool get _showMap => section.showMap == true && baseMap != null;
|
|
|
|
String get _title => TranslationHelper.get(path.title, visitAppContext);
|
|
String get _desc => TranslationHelper.get(path.description, visitAppContext);
|
|
|
|
String get _gameIntro {
|
|
final msg = path.gameMessageDebut;
|
|
if (msg == null || msg.isEmpty) return '';
|
|
final found = msg.firstWhere(
|
|
(m) => m.language == visitAppContext.language,
|
|
orElse: () => msg.first,
|
|
);
|
|
return found.value ?? '';
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return _isGame ? _buildGameStart(context) : _buildNormalStart(context);
|
|
}
|
|
|
|
Widget _buildNormalStart(BuildContext context) {
|
|
final stepCount = path.steps?.length ?? 0;
|
|
final durationMin = path.estimatedDurationMinutes;
|
|
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
color: _bg,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(26)),
|
|
),
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 5,
|
|
margin: const EdgeInsets.only(top: 11, bottom: 4),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFD8D2C8),
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
height: 180,
|
|
margin: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [primaryColor.withOpacity(0.7), primaryColor],
|
|
),
|
|
),
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: CustomPaint(
|
|
painter: _MapSketchPainter(primaryColor),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 14,
|
|
top: 12,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.92),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Text(
|
|
'DÉCOUVERTE LIBRE',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 0.4,
|
|
color: Color(0xFF264863),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 16,
|
|
right: 16,
|
|
bottom: 14,
|
|
child: Text(
|
|
_title,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: -0.3,
|
|
height: 1.1,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
|
|
child: Row(
|
|
children: [
|
|
if (durationMin != null) ...[
|
|
Icon(Icons.access_time, size: 15, color: primaryColor),
|
|
const SizedBox(width: 5),
|
|
Text('~$durationMin min',
|
|
style: TextStyle(color: const Color(0xFF54636E), fontSize: 13, fontWeight: FontWeight.w600)),
|
|
const SizedBox(width: 18),
|
|
],
|
|
Icon(Icons.map_outlined, size: 15, color: primaryColor),
|
|
const SizedBox(width: 5),
|
|
Text('$stepCount étape${stepCount > 1 ? 's' : ''}',
|
|
style: const TextStyle(color: Color(0xFF54636E), fontSize: 13, fontWeight: FontWeight.w600)),
|
|
],
|
|
),
|
|
),
|
|
if (_desc.isNotEmpty)
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
|
|
child: Text(
|
|
_desc,
|
|
style: const TextStyle(
|
|
color: _inkMid,
|
|
fontSize: 15,
|
|
height: 1.55,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_startPath(context);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primaryColor,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 17),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
|
elevation: 0,
|
|
),
|
|
child: const Text(
|
|
'Commencer le parcours',
|
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildGameStart(BuildContext context) {
|
|
final stepCount = path.steps?.length ?? 0;
|
|
final durationMin = path.estimatedDurationMinutes;
|
|
final intro = _gameIntro;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
gradient: RadialGradient(
|
|
center: const Alignment(0, -0.4),
|
|
radius: 1.2,
|
|
colors: [const Color(0xFF16202e), const Color(0xFF0B1018)],
|
|
),
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(26)),
|
|
),
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom + 34,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Center(
|
|
child: Container(
|
|
width: 40,
|
|
height: 5,
|
|
margin: const EdgeInsets.only(top: 11, bottom: 20),
|
|
decoration: BoxDecoration(
|
|
color: _gold.withOpacity(0.4),
|
|
borderRadius: BorderRadius.circular(3),
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: _gold.withOpacity(0.5), width: 1.5),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: _gold.withOpacity(0.2),
|
|
blurRadius: 30,
|
|
spreadRadius: 4,
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(Icons.lock_open_outlined, color: _gold, size: 40),
|
|
),
|
|
const SizedBox(height: 18),
|
|
const Text(
|
|
'ESCAPE GAME',
|
|
style: TextStyle(
|
|
color: _gold,
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
letterSpacing: 4,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 28),
|
|
child: Text(
|
|
_title,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
color: Color(0xFFF4ECD8),
|
|
fontSize: 28,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: -0.5,
|
|
height: 1.1,
|
|
),
|
|
),
|
|
),
|
|
if (intro.isNotEmpty) ...[
|
|
const SizedBox(height: 14),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 28),
|
|
child: Text(
|
|
'« $intro »',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: const Color(0xFFb9ac8c),
|
|
fontSize: 15,
|
|
fontStyle: FontStyle.italic,
|
|
height: 1.6,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 18),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('$stepCount énigme${stepCount > 1 ? 's' : ''}',
|
|
style: const TextStyle(color: Color(0xFF8a8068), fontSize: 13, fontWeight: FontWeight.w600)),
|
|
if (durationMin != null) ...[
|
|
const SizedBox(width: 8),
|
|
const Text('·', style: TextStyle(color: Color(0xFF8a8068))),
|
|
const SizedBox(width: 8),
|
|
Text('~$durationMin min',
|
|
style: const TextStyle(color: Color(0xFF8a8068), fontSize: 13, fontWeight: FontWeight.w600)),
|
|
],
|
|
],
|
|
),
|
|
const SizedBox(height: 26),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
_startPath(context);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _gold,
|
|
foregroundColor: const Color(0xFF1a1305),
|
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
elevation: 0,
|
|
),
|
|
child: const Text(
|
|
"Démarrer l'aventure",
|
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w800, letterSpacing: 0.3),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
if (_isGame)
|
|
const Text(
|
|
'Activez le son pour une immersion totale',
|
|
style: TextStyle(color: Color(0xFF6f6655), fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _startPath(BuildContext context) {
|
|
if (_showMap) {
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (_) => GuidedPathMapProgressionPage(
|
|
path: path,
|
|
mapDTO: baseMap!,
|
|
visitAppContext: visitAppContext,
|
|
),
|
|
));
|
|
} else {
|
|
Navigator.of(context).push(MaterialPageRoute(
|
|
builder: (_) => GuidedPathContentProgressionPage(
|
|
path: path,
|
|
visitAppContext: visitAppContext,
|
|
),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
class _MapSketchPainter extends CustomPainter {
|
|
final Color primaryColor;
|
|
_MapSketchPainter(this.primaryColor);
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final bgPaint = Paint()
|
|
..color = Colors.white.withOpacity(0.06)
|
|
..style = PaintingStyle.fill;
|
|
|
|
final roadPaint = Paint()
|
|
..color = Colors.white.withOpacity(0.12)
|
|
..strokeWidth = 8
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
final routePaint = Paint()
|
|
..color = Colors.white.withOpacity(0.7)
|
|
..strokeWidth = 2
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
final road1 = Path()
|
|
..moveTo(-10, size.height * 0.45)
|
|
..lineTo(size.width * 0.35, size.height * 0.45)
|
|
..lineTo(size.width * 0.55, size.height * 0.2)
|
|
..lineTo(size.width + 10, size.height * 0.2);
|
|
|
|
final road2 = Path()
|
|
..moveTo(size.width * 0.2, -10)
|
|
..lineTo(size.width * 0.2, size.height * 0.55)
|
|
..lineTo(size.width * 0.4, size.height + 10);
|
|
|
|
canvas.drawPath(road1, roadPaint);
|
|
canvas.drawPath(road2, roadPaint);
|
|
|
|
final route = Path()
|
|
..moveTo(size.width * 0.15, size.height * 0.65)
|
|
..lineTo(size.width * 0.35, size.height * 0.35)
|
|
..lineTo(size.width * 0.65, size.height * 0.55)
|
|
..lineTo(size.width * 0.82, size.height * 0.35);
|
|
|
|
final dashPaint = Paint()
|
|
..color = Colors.white.withOpacity(0.8)
|
|
..strokeWidth = 2
|
|
..style = PaintingStyle.stroke
|
|
..strokeCap = StrokeCap.round;
|
|
|
|
_drawDashedPath(canvas, route, dashPaint, dashLength: 4, gapLength: 8);
|
|
|
|
final dotPaint = Paint()
|
|
..color = Colors.white
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawCircle(Offset(size.width * 0.15, size.height * 0.65), 5, dotPaint);
|
|
canvas.drawCircle(Offset(size.width * 0.82, size.height * 0.35), 5,
|
|
dotPaint..color = const Color(0xFF2E9E6B));
|
|
}
|
|
|
|
void _drawDashedPath(Canvas canvas, Path path, Paint paint,
|
|
{required double dashLength, required double gapLength}) {
|
|
final metrics = path.computeMetrics();
|
|
for (final metric in metrics) {
|
|
double distance = 0;
|
|
while (distance < metric.length) {
|
|
final start = distance;
|
|
final end = (start + dashLength).clamp(0.0, metric.length);
|
|
canvas.drawPath(metric.extractPath(start, end), paint);
|
|
distance += dashLength + gapLength;
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(_MapSketchPainter old) => old.primaryColor != primaryColor;
|
|
}
|