1106 lines
38 KiB
Dart
1106 lines
38 KiB
Dart
// lib/screens/learn_more_screen.dart
|
||
import 'dart:async';
|
||
import 'dart:ui';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||
import 'package:share_plus/share_plus.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
import '../features/events/models/event_models.dart';
|
||
import '../features/events/services/events_service.dart';
|
||
|
||
class LearnMoreScreen extends StatefulWidget {
|
||
final int eventId;
|
||
const LearnMoreScreen({Key? key, required this.eventId}) : super(key: key);
|
||
|
||
@override
|
||
State<LearnMoreScreen> createState() => _LearnMoreScreenState();
|
||
}
|
||
|
||
class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||
final EventsService _service = EventsService();
|
||
|
||
bool _loading = true;
|
||
EventModel? _event;
|
||
String? _error;
|
||
|
||
// Carousel
|
||
final PageController _pageController = PageController();
|
||
int _currentPage = 0;
|
||
Timer? _autoScrollTimer;
|
||
|
||
// About section
|
||
bool _aboutExpanded = false;
|
||
|
||
// Wishlist (UI-only)
|
||
bool _wishlisted = false;
|
||
|
||
// Google Map
|
||
GoogleMapController? _mapController;
|
||
MapType _mapType = MapType.normal;
|
||
bool _showMapControls = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_loadEvent();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_autoScrollTimer?.cancel();
|
||
_pageController.dispose();
|
||
_mapController?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Data loading
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Future<void> _loadEvent() async {
|
||
setState(() {
|
||
_loading = true;
|
||
_error = null;
|
||
});
|
||
try {
|
||
final ev = await _service.getEventDetails(widget.eventId);
|
||
if (!mounted) return;
|
||
setState(() => _event = ev);
|
||
_startAutoScroll();
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
setState(() => _error = e.toString());
|
||
} finally {
|
||
if (mounted) setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Carousel helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
List<String> get _imageUrls {
|
||
final list = <String>[];
|
||
if (_event == null) return list;
|
||
final thumb = _event!.thumbImg;
|
||
if (thumb != null && thumb.isNotEmpty) list.add(thumb);
|
||
for (final img in _event!.images) {
|
||
if (img.image.isNotEmpty && !list.contains(img.image)) list.add(img.image);
|
||
}
|
||
return list;
|
||
}
|
||
|
||
void _startAutoScroll() {
|
||
_autoScrollTimer?.cancel();
|
||
final count = _imageUrls.length;
|
||
if (count <= 1) return;
|
||
_autoScrollTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||
if (!_pageController.hasClients) return;
|
||
final next = (_currentPage + 1) % count;
|
||
_pageController.animateToPage(next,
|
||
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
||
});
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Date formatting
|
||
// ---------------------------------------------------------------------------
|
||
|
||
String _formattedDateRange() {
|
||
if (_event == null) return '';
|
||
try {
|
||
final s = DateTime.parse(_event!.startDate);
|
||
final e = DateTime.parse(_event!.endDate);
|
||
const months = [
|
||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||
];
|
||
if (s.year == e.year && s.month == e.month && s.day == e.day) {
|
||
return '${s.day} ${months[s.month - 1]}';
|
||
}
|
||
if (s.month == e.month && s.year == e.year) {
|
||
return '${s.day} - ${e.day} ${months[s.month - 1]}';
|
||
}
|
||
return '${s.day} ${months[s.month - 1]} - ${e.day} ${months[e.month - 1]}';
|
||
} catch (_) {
|
||
return '${_event!.startDate} – ${_event!.endDate}';
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Actions
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Future<void> _shareEvent() async {
|
||
final title = _event?.title ?? _event?.name ?? 'Check out this event';
|
||
final url =
|
||
'https://uat.eventifyplus.com/events/${widget.eventId}';
|
||
await Share.share('$title\n$url', subject: title);
|
||
}
|
||
|
||
Future<void> _openUrl(String url) async {
|
||
final uri = Uri.parse(url);
|
||
if (await canLaunchUrl(uri)) {
|
||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||
}
|
||
}
|
||
|
||
void _viewLargerMap() {
|
||
if (_event?.latitude == null || _event?.longitude == null) return;
|
||
_openUrl(
|
||
'https://www.google.com/maps/search/?api=1&query=${_event!.latitude},${_event!.longitude}');
|
||
}
|
||
|
||
void _getDirections() {
|
||
if (_event?.latitude == null || _event?.longitude == null) return;
|
||
_openUrl(
|
||
'https://www.google.com/maps/dir/?api=1&destination=${_event!.latitude},${_event!.longitude}');
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Map camera helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
void _moveCamera(double latDelta, double lngDelta) {
|
||
_mapController?.animateCamera(CameraUpdate.scrollBy(lngDelta * 80, -latDelta * 80));
|
||
}
|
||
|
||
void _zoom(double amount) {
|
||
_mapController?.animateCamera(CameraUpdate.zoomBy(amount));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// BUILD
|
||
// ---------------------------------------------------------------------------
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final theme = Theme.of(context);
|
||
|
||
if (_loading) {
|
||
return Scaffold(
|
||
backgroundColor: theme.scaffoldBackgroundColor,
|
||
body: _buildLoadingShimmer(theme),
|
||
);
|
||
}
|
||
|
||
if (_error != null) {
|
||
return Scaffold(
|
||
backgroundColor: theme.scaffoldBackgroundColor,
|
||
body: Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(32),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.error_outline, size: 56, color: theme.colorScheme.error),
|
||
const SizedBox(height: 16),
|
||
Text('Something went wrong',
|
||
style: theme.textTheme.titleMedium
|
||
?.copyWith(fontWeight: FontWeight.bold)),
|
||
const SizedBox(height: 8),
|
||
Text(_error!, textAlign: TextAlign.center, style: theme.textTheme.bodyMedium),
|
||
const SizedBox(height: 24),
|
||
ElevatedButton.icon(
|
||
onPressed: _loadEvent,
|
||
icon: const Icon(Icons.refresh),
|
||
label: const Text('Retry'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
if (_event == null) {
|
||
return Scaffold(
|
||
backgroundColor: theme.scaffoldBackgroundColor,
|
||
body: const Center(child: Text('Event not found')),
|
||
);
|
||
}
|
||
|
||
final screenHeight = MediaQuery.of(context).size.height;
|
||
final imageHeight = screenHeight * 0.50;
|
||
final overlap = 30.0;
|
||
|
||
return Scaffold(
|
||
backgroundColor: theme.scaffoldBackgroundColor,
|
||
body: Stack(
|
||
children: [
|
||
// ── LAYER 1: Image carousel (background) ──
|
||
_buildImageCarousel(theme, imageHeight),
|
||
|
||
// ── LAYER 2: Scrollable content with overlapping white card ──
|
||
SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Transparent spacer — shows the image behind
|
||
SizedBox(height: imageHeight - overlap),
|
||
|
||
// White card with rounded top corners overlapping image
|
||
Container(
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
color: theme.scaffoldBackgroundColor,
|
||
borderRadius: const BorderRadius.vertical(
|
||
top: Radius.circular(28),
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.08),
|
||
blurRadius: 20,
|
||
offset: const Offset(0, -6),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildTitleSection(theme),
|
||
_buildAboutSection(theme),
|
||
if (_event!.latitude != null && _event!.longitude != null) ...[
|
||
_buildVenueSection(theme),
|
||
_buildGetDirectionsButton(theme),
|
||
],
|
||
if (_event!.importantInfo.isNotEmpty)
|
||
_buildImportantInfoSection(theme),
|
||
if (_event!.importantInfo.isEmpty &&
|
||
(_event!.importantInformation ?? '').isNotEmpty)
|
||
_buildImportantInfoFallback(theme),
|
||
const SizedBox(height: 100),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// ── LAYER 3: Floating icon row (above scrollview so taps work) ──
|
||
Positioned(
|
||
top: MediaQuery.of(context).padding.top + 10,
|
||
left: 16,
|
||
right: 16,
|
||
child: Row(
|
||
children: [
|
||
_squareIconButton(
|
||
icon: Icons.arrow_back,
|
||
onTap: () => Navigator.pop(context),
|
||
),
|
||
// Pill-shaped page indicators (centered)
|
||
Expanded(
|
||
child: _imageUrls.length > 1
|
||
? Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: List.generate(_imageUrls.length, (i) {
|
||
final active = i == _currentPage;
|
||
return AnimatedContainer(
|
||
duration: const Duration(milliseconds: 300),
|
||
margin: const EdgeInsets.symmetric(horizontal: 3),
|
||
width: active ? 18 : 8,
|
||
height: 6,
|
||
decoration: BoxDecoration(
|
||
color: active
|
||
? Colors.white
|
||
: Colors.white.withOpacity(0.45),
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
);
|
||
}),
|
||
)
|
||
: const SizedBox.shrink(),
|
||
),
|
||
_squareIconButton(
|
||
icon: Icons.ios_share_outlined,
|
||
onTap: _shareEvent,
|
||
),
|
||
const SizedBox(width: 10),
|
||
_squareIconButton(
|
||
icon: _wishlisted ? Icons.favorite : Icons.favorite_border,
|
||
iconColor: _wishlisted ? Colors.redAccent : Colors.white,
|
||
onTap: () => setState(() => _wishlisted = !_wishlisted),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 1. LOADING SHIMMER
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildLoadingShimmer(ThemeData theme) {
|
||
return SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(20),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// Placeholder image
|
||
Container(
|
||
height: MediaQuery.of(context).size.height * 0.42,
|
||
decoration: BoxDecoration(
|
||
color: theme.dividerColor.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(28),
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
// Placeholder title
|
||
Container(
|
||
height: 28,
|
||
width: 220,
|
||
decoration: BoxDecoration(
|
||
color: theme.dividerColor.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
),
|
||
const SizedBox(height: 12),
|
||
Container(
|
||
height: 16,
|
||
width: 140,
|
||
decoration: BoxDecoration(
|
||
color: theme.dividerColor.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
),
|
||
const SizedBox(height: 20),
|
||
Container(
|
||
height: 16,
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
color: theme.dividerColor.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Container(
|
||
height: 16,
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
color: theme.dividerColor.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 2. IMAGE CAROUSEL WITH BLURRED BACKGROUND
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildImageCarousel(ThemeData theme, double carouselHeight) {
|
||
final images = _imageUrls;
|
||
final topPad = MediaQuery.of(context).padding.top;
|
||
|
||
return SizedBox(
|
||
height: carouselHeight,
|
||
child: Stack(
|
||
children: [
|
||
// ---- Blurred background (image or blue gradient) ----
|
||
Positioned.fill(
|
||
child: images.isNotEmpty
|
||
? ClipRect(
|
||
child: Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
Image.network(
|
||
images[_currentPage],
|
||
fit: BoxFit.cover,
|
||
errorBuilder: (_, __, ___) => Container(
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
BackdropFilter(
|
||
filter: ImageFilter.blur(sigmaX: 25, sigmaY: 25),
|
||
child: Container(
|
||
color: Colors.black.withOpacity(0.15),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: Container(
|
||
decoration: const BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [Color(0xFF1A56DB), Color(0xFF3B82F6)],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// ---- Foreground image with rounded corners ----
|
||
if (images.isNotEmpty)
|
||
Positioned(
|
||
top: topPad + 56, // below the icon row
|
||
left: 20,
|
||
right: 20,
|
||
bottom: 16,
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(20),
|
||
child: PageView.builder(
|
||
controller: _pageController,
|
||
onPageChanged: (i) => setState(() => _currentPage = i),
|
||
itemCount: images.length,
|
||
itemBuilder: (_, i) => Image.network(
|
||
images[i],
|
||
fit: BoxFit.cover,
|
||
width: double.infinity,
|
||
errorBuilder: (_, __, ___) => Container(
|
||
color: theme.dividerColor,
|
||
child: Icon(Icons.broken_image, size: 48, color: theme.hintColor),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// ---- No-image placeholder ----
|
||
if (images.isEmpty)
|
||
Positioned(
|
||
top: topPad + 56,
|
||
left: 20,
|
||
right: 20,
|
||
bottom: 16,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.15),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: const Center(
|
||
child: Icon(Icons.event, size: 80, color: Colors.white70),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Square icon button with rounded corners and translucent white background
|
||
Widget _squareIconButton({
|
||
required IconData icon,
|
||
required VoidCallback onTap,
|
||
Color iconColor = Colors.white,
|
||
}) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: 42,
|
||
height: 42,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.2),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: Colors.white.withOpacity(0.3)),
|
||
),
|
||
child: Icon(icon, color: iconColor, size: 22),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 3. TITLE & DATE
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildTitleSection(ThemeData theme) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
_event!.title ?? _event!.name,
|
||
style: theme.textTheme.headlineSmall?.copyWith(
|
||
fontWeight: FontWeight.w800,
|
||
fontSize: 26,
|
||
height: 1.25,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Icon(Icons.calendar_today_outlined,
|
||
size: 16, color: theme.hintColor),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
_formattedDateRange(),
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: theme.hintColor,
|
||
fontSize: 15,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 4. ABOUT THE EVENT
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildAboutSection(ThemeData theme) {
|
||
final desc = _event!.description ?? '';
|
||
if (desc.isEmpty) return const SizedBox.shrink();
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'About the Event',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w800,
|
||
fontSize: 20,
|
||
),
|
||
),
|
||
const SizedBox(height: 10),
|
||
AnimatedCrossFade(
|
||
firstChild: Text(
|
||
desc,
|
||
maxLines: 4,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
height: 1.55,
|
||
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75),
|
||
),
|
||
),
|
||
secondChild: Text(
|
||
desc,
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
height: 1.55,
|
||
color: theme.textTheme.bodyMedium?.color?.withOpacity(0.75),
|
||
),
|
||
),
|
||
crossFadeState:
|
||
_aboutExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
|
||
duration: const Duration(milliseconds: 300),
|
||
),
|
||
const SizedBox(height: 6),
|
||
GestureDetector(
|
||
onTap: () => setState(() => _aboutExpanded = !_aboutExpanded),
|
||
child: Text(
|
||
_aboutExpanded ? 'Read Less' : 'Read More',
|
||
style: TextStyle(
|
||
color: theme.colorScheme.primary,
|
||
fontWeight: FontWeight.w700,
|
||
fontSize: 15,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 5. VENUE LOCATION (Google Map)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildVenueSection(ThemeData theme) {
|
||
final lat = _event!.latitude!;
|
||
final lng = _event!.longitude!;
|
||
final venueLabel = _event!.locationName ?? _event!.venueName ?? _event!.place ?? '';
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Venue Location',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w800,
|
||
fontSize: 20,
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
|
||
// Map container
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(20),
|
||
child: SizedBox(
|
||
height: 280,
|
||
child: Stack(
|
||
children: [
|
||
GoogleMap(
|
||
initialCameraPosition: CameraPosition(
|
||
target: LatLng(lat, lng),
|
||
zoom: 15,
|
||
),
|
||
mapType: _mapType,
|
||
markers: {
|
||
Marker(
|
||
markerId: const MarkerId('event'),
|
||
position: LatLng(lat, lng),
|
||
infoWindow: InfoWindow(title: venueLabel),
|
||
),
|
||
},
|
||
myLocationButtonEnabled: false,
|
||
zoomControlsEnabled: false,
|
||
scrollGesturesEnabled: true,
|
||
rotateGesturesEnabled: false,
|
||
tiltGesturesEnabled: false,
|
||
onMapCreated: (c) => _mapController = c,
|
||
),
|
||
|
||
// "View larger map" – top left
|
||
Positioned(
|
||
top: 10,
|
||
left: 10,
|
||
child: GestureDetector(
|
||
onTap: _viewLargerMap,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(8),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.12),
|
||
blurRadius: 6,
|
||
),
|
||
],
|
||
),
|
||
child: Text(
|
||
'View larger map',
|
||
style: TextStyle(
|
||
color: theme.colorScheme.primary,
|
||
fontWeight: FontWeight.w600,
|
||
fontSize: 13,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// Map type toggle – bottom left
|
||
Positioned(
|
||
bottom: 12,
|
||
left: 12,
|
||
child: _mapControlButton(
|
||
icon: _mapType == MapType.normal
|
||
? Icons.satellite_alt
|
||
: Icons.map_outlined,
|
||
onTap: () {
|
||
setState(() {
|
||
_mapType = _mapType == MapType.normal
|
||
? MapType.satellite
|
||
: MapType.normal;
|
||
});
|
||
},
|
||
),
|
||
),
|
||
|
||
// Map controls toggle – bottom right
|
||
Positioned(
|
||
bottom: 12,
|
||
right: 12,
|
||
child: _mapControlButton(
|
||
icon: Icons.open_with_rounded,
|
||
onTap: () => setState(() => _showMapControls = !_showMapControls),
|
||
),
|
||
),
|
||
|
||
// Directional pad overlay
|
||
if (_showMapControls)
|
||
Positioned.fill(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.black.withOpacity(0.25),
|
||
borderRadius: BorderRadius.circular(20),
|
||
),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
// Top row: Up + Zoom In
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
_mapControlButton(
|
||
icon: Icons.keyboard_arrow_up,
|
||
onTap: () => _moveCamera(1, 0)),
|
||
const SizedBox(width: 16),
|
||
_mapControlButton(
|
||
icon: Icons.add,
|
||
onTap: () => _zoom(1)),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
// Middle row: Left + Right
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
_mapControlButton(
|
||
icon: Icons.keyboard_arrow_left,
|
||
onTap: () => _moveCamera(0, -1)),
|
||
const SizedBox(width: 60),
|
||
_mapControlButton(
|
||
icon: Icons.keyboard_arrow_right,
|
||
onTap: () => _moveCamera(0, 1)),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
// Bottom row: Down + Zoom Out + Close
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
_mapControlButton(
|
||
icon: Icons.keyboard_arrow_down,
|
||
onTap: () => _moveCamera(-1, 0)),
|
||
const SizedBox(width: 16),
|
||
_mapControlButton(
|
||
icon: Icons.remove,
|
||
onTap: () => _zoom(-1)),
|
||
const SizedBox(width: 16),
|
||
_mapControlButton(
|
||
icon: Icons.close,
|
||
onTap: () =>
|
||
setState(() => _showMapControls = false)),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// Venue name card
|
||
if (venueLabel.isNotEmpty)
|
||
Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.only(top: 14),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: theme.cardColor,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: theme.shadowColor.withOpacity(0.06),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
venueLabel,
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
if (_event!.place != null && _event!.place != venueLabel)
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 4),
|
||
child: Text(
|
||
_event!.place!,
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: theme.hintColor,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _mapControlButton({
|
||
required IconData icon,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return GestureDetector(
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: 44,
|
||
height: 44,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withOpacity(0.92),
|
||
shape: BoxShape.circle,
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.15),
|
||
blurRadius: 6,
|
||
),
|
||
],
|
||
),
|
||
child: Icon(icon, color: Colors.grey.shade700, size: 22),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 6. GET DIRECTIONS BUTTON
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildGetDirectionsButton(ThemeData theme) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 0),
|
||
child: SizedBox(
|
||
width: double.infinity,
|
||
height: 54,
|
||
child: ElevatedButton.icon(
|
||
onPressed: _getDirections,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: theme.colorScheme.primary,
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
elevation: 2,
|
||
),
|
||
icon: const Icon(Icons.directions, size: 22),
|
||
label: const Text(
|
||
'Get Directions',
|
||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 7. IMPORTANT INFORMATION (structured list)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
Widget _buildImportantInfoSection(ThemeData theme) {
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Important Information',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w800,
|
||
fontSize: 20,
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
for (final info in _event!.importantInfo)
|
||
Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.primary.withOpacity(0.05),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: theme.colorScheme.primary.withOpacity(0.12),
|
||
),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.primary.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Icon(Icons.info_outline,
|
||
size: 20, color: theme.colorScheme.primary),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
info['title'] ?? '',
|
||
style: theme.textTheme.bodyLarge?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
info['value'] ?? '',
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: theme.hintColor,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// 7b. IMPORTANT INFO FALLBACK (parse HTML string into cards)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Strip HTML tags and decode common HTML entities
|
||
String _stripHtml(String html) {
|
||
// Remove all HTML tags
|
||
var text = html.replaceAll(RegExp(r'<[^>]*>'), '');
|
||
// Decode common HTML entities
|
||
text = text
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll(''', "'")
|
||
.replaceAll(' ', ' ');
|
||
return text.trim();
|
||
}
|
||
|
||
/// Parse an HTML important_information string into a list of {title, value} maps
|
||
List<Map<String, String>> _parseHtmlImportantInfo(String raw) {
|
||
// Strip HTML tags, preserving <br> as a newline separator first
|
||
var text = raw
|
||
.replaceAll(RegExp(r'<br\s*/?>', caseSensitive: false), '\n')
|
||
.replaceAll(RegExp(r'<[^>]*>'), '');
|
||
// Decode entities
|
||
text = text
|
||
.replaceAll('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll(''', "'")
|
||
.replaceAll(' ', ' ');
|
||
|
||
// Split by newlines first
|
||
var lines = text
|
||
.split('\n')
|
||
.map((l) => l.trim())
|
||
.where((l) => l.isNotEmpty)
|
||
.toList();
|
||
|
||
// If we only have 1 line, items might be separated by emoji characters
|
||
// (some categories don't use <br> between items, e.g. "...etc.🚌 Bus:")
|
||
if (lines.length <= 1 && text.trim().isNotEmpty) {
|
||
final parts = text.trim().split(
|
||
RegExp(r'(?=[\u2600-\u27BF]|[\u{1F300}-\u{1FFFF}])', unicode: true),
|
||
);
|
||
final emojiLines = parts
|
||
.map((l) => l.trim())
|
||
.where((l) => l.isNotEmpty)
|
||
.toList();
|
||
if (emojiLines.length > 1) {
|
||
lines = emojiLines;
|
||
}
|
||
}
|
||
|
||
final items = <Map<String, String>>[];
|
||
for (final line in lines) {
|
||
// Split on first colon to get title:value
|
||
final colonIdx = line.indexOf(':');
|
||
if (colonIdx > 0 && colonIdx < line.length - 1) {
|
||
items.add({
|
||
'title': line.substring(0, colonIdx + 1).trim(),
|
||
'value': line.substring(colonIdx + 1).trim(),
|
||
});
|
||
} else {
|
||
items.add({'title': line, 'value': ''});
|
||
}
|
||
}
|
||
return items;
|
||
}
|
||
|
||
Widget _buildImportantInfoFallback(ThemeData theme) {
|
||
final parsed = _parseHtmlImportantInfo(_event!.importantInformation!);
|
||
|
||
if (parsed.isEmpty) return const SizedBox.shrink();
|
||
|
||
return Padding(
|
||
padding: const EdgeInsets.fromLTRB(20, 28, 20, 0),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
'Important Information',
|
||
style: theme.textTheme.titleMedium?.copyWith(
|
||
fontWeight: FontWeight.w800,
|
||
fontSize: 20,
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
for (final info in parsed)
|
||
Container(
|
||
width: double.infinity,
|
||
margin: const EdgeInsets.only(bottom: 12),
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.primary.withOpacity(0.05),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: theme.colorScheme.primary.withOpacity(0.12),
|
||
),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
color: theme.colorScheme.primary.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(10),
|
||
),
|
||
child: Icon(Icons.info_outline,
|
||
size: 20, color: theme.colorScheme.primary),
|
||
),
|
||
const SizedBox(width: 14),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
info['title'] ?? '',
|
||
style: theme.textTheme.bodyLarge?.copyWith(
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
if ((info['value'] ?? '').isNotEmpty) ...[
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
info['value']!,
|
||
style: theme.textTheme.bodyMedium?.copyWith(
|
||
color: theme.hintColor,
|
||
height: 1.4,
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|