import 'package:flutter/material.dart'; import 'package:wisata_app/models/destination.dart'; import '../services/destination_service.dart'; import '../services/maps_launcher_service.dart'; import '../widgets/destination_image.dart'; import '../widgets/primary_button.dart'; import 'ar_view_screen.dart'; import 'video_page.dart'; class DetailDestinationScreen extends StatefulWidget { const DetailDestinationScreen({super.key}); static const routeName = '/detail'; @override State createState() => _DetailDestinationScreenState(); } class _DetailDestinationScreenState extends State { static const MapsLauncherService _mapsLauncher = MapsLauncherService(); static const DestinationService _destinationService = DestinationService(); Future? _destinationFuture; @override void didChangeDependencies() { super.didChangeDependencies(); _destinationFuture ??= _resolveDestination(); } Future _resolveDestination() async { final argument = ModalRoute.of(context)?.settings.arguments; if (argument is Destination) return argument; if (argument == null) return null; return _destinationService.getDestinationDetail(argument.toString()); } @override Widget build(BuildContext context) { return FutureBuilder( future: _destinationFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Scaffold( backgroundColor: Color(0xFFF4F8FB), body: Center(child: CircularProgressIndicator()), ); } final destination = snapshot.data; if (snapshot.hasError || destination == null) { return Scaffold( backgroundColor: const Color(0xFFF4F8FB), appBar: AppBar(), body: Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( snapshot.hasError ? 'Detail wisata gagal dimuat.' : 'Destinasi tidak ditemukan.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, ), ), ), ), ); } return _DetailContent( destination: destination, mapsLauncher: _mapsLauncher, ); }, ); } } class _DetailContent extends StatelessWidget { const _DetailContent({ required this.destination, required this.mapsLauncher, }); final Destination destination; final MapsLauncherService mapsLauncher; @override Widget build(BuildContext context) { // ignore: avoid_print print(destination.imagePath); return Scaffold( backgroundColor: const Color(0xFFF4F8FB), body: Stack( children: [ CustomScrollView( slivers: [ _Header(destination: destination), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(20, 22, 20, 116), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( destination.title, style: Theme.of(context) .textTheme .headlineMedium ?.copyWith( fontWeight: FontWeight.w900, color: const Color(0xFF0B1F33), ), ), const SizedBox(height: 8), Row( children: [ const Icon(Icons.place_rounded, size: 20, color: Color(0xFF4B6475)), const SizedBox(width: 6), Expanded( child: Text( destination.displayLocation, style: Theme.of(context) .textTheme .bodyLarge ?.copyWith( color: const Color(0xFF4B6475), fontWeight: FontWeight.w700, ), ), ), ], ), const SizedBox(height: 22), Column( children: [ _DetailInfoCard( icon: Icons.star_rounded, label: 'Rating', value: destination.rating.toStringAsFixed(1), accentColor: const Color(0xFFD97706), ), const SizedBox(height: 10), _DetailInfoCard( icon: Icons.height_rounded, label: 'Ketinggian', value: destination.elevation, ), const SizedBox(height: 10), _DetailInfoCard( icon: Icons.confirmation_number, label: 'Tiket & Parkir', value: destination.tiketParkir, ), const SizedBox(height: 10), _DetailInfoCard( icon: Icons.access_time, label: 'Jam Operasional', value: destination.jamOperasional, ), ], ), const SizedBox(height: 28), _SectionTitle(title: 'Deskripsi'), const SizedBox(height: 10), Text( destination.overview, style: Theme.of(context).textTheme.bodyLarge?.copyWith( height: 1.65, color: const Color(0xFF405466), ), ), const SizedBox(height: 28), _SectionTitle(title: 'Lokasi'), const SizedBox(height: 12), _MapPreview(destination: destination), const SizedBox(height: 14), OutlinedButton.icon( onPressed: destination.hasCoordinates ? () => _openGoogleMaps( context, destination, ) : null, icon: const Icon(Icons.map_outlined), label: const Text('Buka Google Maps'), style: OutlinedButton.styleFrom( minimumSize: const Size.fromHeight(52), foregroundColor: const Color(0xFF0F4C81), side: const BorderSide(color: Color(0xFF0F4C81)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), ), ), ], ), ), ), ], ), Positioned( left: 20, right: 20, bottom: 22, child: SafeArea( child: PrimaryButton( label: destination.isVideo && destination.videoPath.isNotEmpty ? 'Lihat Video Wisata' : 'Lihat AR', icon: destination.isVideo && destination.videoPath.isNotEmpty ? Icons.play_circle_fill : Icons.view_in_ar_rounded, onPressed: () { if (destination.isVideo && destination.videoPath.isNotEmpty) { Navigator.push( context, MaterialPageRoute( builder: (_) => VideoPage( title: destination.title, videoPath: destination.videoPath, ), ), ); return; } final modelPath = destination.modelPath; if (modelPath.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Model AR untuk ${destination.title} belum tersedia.', ), ), ); return; } Navigator.pushNamed( context, ArViewScreen.routeName, arguments: destination, ); }, ), ), ), ], ), ); } Future _openGoogleMaps( BuildContext context, Destination destination, ) async { try { await mapsLauncher.openDestination(destination); } on MapsLauncherException catch (exception) { if (!context.mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar(SnackBar(content: Text(exception.message))); } catch (_) { if (!context.mounted) return; ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( const SnackBar( content: Text('Lokasi belum bisa dibuka. Silakan coba lagi.'), ), ); } } } class _Header extends StatelessWidget { const _Header({required this.destination}); final Destination destination; @override Widget build(BuildContext context) { return SliverAppBar( expandedHeight: 390, pinned: true, backgroundColor: const Color(0xFF083A63), leading: Padding( padding: const EdgeInsets.only(left: 12), child: IconButton.filledTonal( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back_rounded), ), ), flexibleSpace: FlexibleSpaceBar( background: Stack( fit: StackFit.expand, children: [ Hero( tag: 'destination-${destination.id}', child: DestinationImage( path: destination.imagePath, ), ), const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color(0x66000000), Color(0x11000000), Color(0xEE083A63) ], ), ), ), Positioned( left: 20, bottom: 28, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 9), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(999), border: Border.all(color: Colors.white.withValues(alpha: 0.32)), ), child: Row( children: [ const Icon(Icons.star_rounded, color: Color(0xFFFFD36A), size: 18), const SizedBox(width: 6), Text( destination.rating.toStringAsFixed(1), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800), ), ], ), ), ), ], ), ), ); } } class _SectionTitle extends StatelessWidget { const _SectionTitle({required this.title}); final String title; @override Widget build(BuildContext context) { return Text( title, style: Theme.of(context) .textTheme .titleLarge ?.copyWith(fontWeight: FontWeight.w900), ); } } class _DetailInfoCard extends StatelessWidget { const _DetailInfoCard({ required this.icon, required this.label, required this.value, this.accentColor, }); final IconData icon; final String label; final String value; final Color? accentColor; @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Container( width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: colors.surfaceContainerHighest.withValues(alpha: 0.72), borderRadius: BorderRadius.circular(22), border: Border.all(color: colors.outlineVariant.withValues(alpha: 0.45)), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: accentColor ?? colors.primary), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of(context).textTheme.labelLarge?.copyWith( color: colors.onSurfaceVariant, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 6), Text( value, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: const Color(0xFF0B1F33), fontWeight: FontWeight.w800, height: 1.45, ), ), ], ), ), ], ), ); } } class _MapPreview extends StatelessWidget { const _MapPreview({required this.destination}); final Destination destination; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(26), child: Container( height: 170, decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFFD9EEF8), Color(0xFFB7D8EA)], ), ), child: Stack( children: [ Positioned.fill( child: CustomPaint(painter: _MapPatternPainter()), ), Center( child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.86), shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.14), blurRadius: 24, offset: const Offset(0, 14), ), ], ), child: const Icon(Icons.location_on_rounded, color: Color(0xFFE04F3F), size: 36), ), ), Positioned( left: 18, bottom: 16, child: Text( destination.displayLocation, style: const TextStyle( fontWeight: FontWeight.w900, color: Color(0xFF0B1F33)), ), ), ], ), ), ); } } class _MapPatternPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final pathPaint = Paint() ..color = Colors.white.withValues(alpha: 0.42) ..style = PaintingStyle.stroke ..strokeWidth = 6 ..strokeCap = StrokeCap.round; final route = Path() ..moveTo(-20, size.height * 0.72) ..quadraticBezierTo(size.width * 0.24, size.height * 0.32, size.width * 0.46, size.height * 0.56) ..quadraticBezierTo(size.width * 0.68, size.height * 0.8, size.width + 24, size.height * 0.28); canvas.drawPath(route, pathPaint); final contourPaint = Paint() ..color = const Color(0xFF2B6E99).withValues(alpha: 0.28) ..style = PaintingStyle.stroke ..strokeWidth = 2; for (var i = 0; i < 5; i++) { canvas.drawOval( Rect.fromCenter( center: Offset(size.width * (0.18 + i * 0.18), size.height * (0.25 + (i.isEven ? 0.08 : 0.2))), width: 120 - i * 8, height: 52 + i * 8, ), contourPaint, ); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }