diff --git a/analyze_output.txt b/analyze_output.txt
new file mode 100644
index 0000000..3bce1be
--- /dev/null
+++ b/analyze_output.txt
@@ -0,0 +1,145 @@
+Analyzing figma_event_app...
+
+warning - The include file 'package:flutter_lints/flutter.yaml' in 'D:\projects\figma_event_app\analysis_options.yaml' can't be found when analyzing 'D:\projects\figma_event_app' - analysis_options.yaml:10:10 - include_file_not_found
+warning - Unused import: 'package:intl/intl.dart' - lib\features\events\services\events_service.dart:2:8 - unused_import
+warning - Unused import: 'dart:math' - lib\screens\calendar_screen.dart:2:8 - unused_import
+warning - The value of the field '_loadingMonth' isn't used - lib\screens\calendar_screen.dart:23:8 - unused_field
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:252:77 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:255:99 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:365:65 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:417:62 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:491:63 - deprecated_member_use
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:528:36 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:528:57 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:530:24 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\calendar_screen.dart:530:45 - unnecessary_null_comparison
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\calendar_screen.dart:530:105 - dead_null_aware_expression
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\calendar_screen.dart:548:43 - dead_null_aware_expression
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:551:111 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\calendar_screen.dart:557:111 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:135:41 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:159:79 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:175:37 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:177:56 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:209:47 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:241:31 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:243:50 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:244:53 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:259:72 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:278:71 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\contribute_screen.dart:308:51 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:389:140 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:400:88 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:553:111 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:679:55 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:774:39 - dead_null_aware_expression
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:776:33 - dead_null_aware_expression
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:795:36 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:795:57 - unnecessary_null_comparison
+warning - The '!' will have no effect because the receiver can't be null - lib\screens\home_desktop_screen.dart:796:22 - unnecessary_non_null_assertion
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:797:25 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:797:46 - unnecessary_null_comparison
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:797:107 - dead_null_aware_expression
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:806:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:807:43 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:829:42 - dead_null_aware_expression
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:844:84 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:865:84 - deprecated_member_use
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:898:36 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:898:57 - unnecessary_null_comparison
+warning - The '!' will have no effect because the receiver can't be null - lib\screens\home_desktop_screen.dart:899:22 - unnecessary_non_null_assertion
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:900:25 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_desktop_screen.dart:900:46 - unnecessary_null_comparison
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:900:107 - dead_null_aware_expression
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:910:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:911:43 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_desktop_screen.dart:932:42 - dead_null_aware_expression
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:943:84 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_desktop_screen.dart:955:84 - deprecated_member_use
+warning - The declaration '_bookEventAtIndex' isn't referenced - lib\screens\home_screen.dart:119:8 - unused_element
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:143:47 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:144:34 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:239:55 - dead_null_aware_expression
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:319:68 - dead_null_aware_expression
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:320:64 - dead_null_aware_expression
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:330:45 - unnecessary_null_comparison
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:389:56 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:462:47 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:521:35 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:541:41 - deprecated_member_use
+warning - The value of the local variable 'theme' isn't used - lib\screens\home_screen.dart:556:11 - unused_local_variable
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:578:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:580:62 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:604:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:606:62 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:677:59 - deprecated_member_use
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:712:22 - unnecessary_null_comparison
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:743:44 - dead_null_aware_expression
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:1015:18 - unnecessary_null_comparison
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:1022:58 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:1037:43 - dead_null_aware_expression
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:1076:18 - unnecessary_null_comparison
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:1126:49 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:1162:36 - dead_null_aware_expression
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\home_screen.dart:1231:18 - unnecessary_null_comparison
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\home_screen.dart:1239:48 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\home_screen.dart:1313:42 - dead_null_aware_expression
+warning - The declaration '_getShortEmailLabel' isn't referenced - lib\screens\home_screen.dart:1385:10 - unused_element
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:265:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:275:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:284:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:293:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:302:43 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:399:42 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:445:35 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:447:54 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:524:59 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:531:59 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:619:51 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:669:47 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:741:46 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:784:31 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:788:35 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:852:50 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:855:52 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:865:56 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:923:48 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\learn_more_screen.dart:926:50 - deprecated_member_use
+warning - The value of the local variable 'headingStyle' isn't used - lib\screens\privacy_policy_screen.dart:86:11 - unused_local_variable
+warning - The value of the field '_upcomingEvents' isn't used - lib\screens\profile_screen.dart:30:20 - unused_field
+warning - The declaration '_topIcon' isn't referenced - lib\screens\profile_screen.dart:290:10 - unused_element
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:298:58 - deprecated_member_use
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\profile_screen.dart:306:42 - dead_null_aware_expression
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:307:37 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:307:59 - unnecessary_null_comparison
+warning - The '!' will have no effect because the receiver can't be null - lib\screens\profile_screen.dart:308:23 - unnecessary_non_null_assertion
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:309:26 - unnecessary_null_comparison
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:309:48 - unnecessary_null_comparison
+warning - The left operand can't be null, so the right operand is never executed - lib\screens\profile_screen.dart:309:112 - dead_null_aware_expression
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:314:108 - deprecated_member_use
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:345:68 - deprecated_member_use
+warning - The operand can't be 'null', so the condition is always 'true' - lib\screens\profile_screen.dart:349:21 - unnecessary_null_comparison
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\profile_screen.dart:368:82 - deprecated_member_use
+warning - The value of the local variable 'gradient' isn't used - lib\screens\profile_screen.dart:397:11 - unused_local_variable
+ info - 'withOpacity' is deprecated and shouldn't be used. Use .withValues() to avoid precision loss - lib\screens\search_screen.dart:130:52 - deprecated_member_use
+
+flutter : 122 issues
+found. (ran in 5.2s)
+At line:1 char:1
++ flutter analyze
+2>&1 | Out-File
+-Encoding utf8 D:\pro
+jects\figma_even ...
++
+~~~~~~~~~~~~~~~~~~~~
+ + CategoryInfo
+ : NotSpe
+ cified: (122 iss
+ ues found. (ran
+in 5.2s):String)
+[], RemoteExcep
+tion
+ + FullyQualified
+ ErrorId : Native
+ CommandError
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ff0b212..74bf722 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,15 +5,19 @@
-
-
+
+
+
+
+
+
+
+
diff --git a/assets/icon/hand_stop.svg b/assets/icon/hand_stop.svg
new file mode 100644
index 0000000..0775369
--- /dev/null
+++ b/assets/icon/hand_stop.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 6266644..1a8b602 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,5 +1,6 @@
import Flutter
import UIKit
+import GoogleMaps
@main
@objc class AppDelegate: FlutterAppDelegate {
@@ -7,6 +8,7 @@ import UIKit
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
+ GMSServices.provideAPIKey("YOUR_GOOGLE_MAPS_API_KEY")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
diff --git a/lib/features/events/models/event_models.dart b/lib/features/events/models/event_models.dart
index 11de701..cf8c864 100644
--- a/lib/features/events/models/event_models.dart
+++ b/lib/features/events/models/event_models.dart
@@ -51,6 +51,14 @@ class EventModel {
final String? eventStatus;
final String? cancelledReason;
+ // Geo / location fields
+ final double? latitude;
+ final double? longitude;
+ final String? locationName;
+
+ // Structured important info list [{title, value}, ...]
+ final List> importantInfo;
+
EventModel({
required this.id,
required this.name,
@@ -70,8 +78,36 @@ class EventModel {
this.venueName,
this.eventStatus,
this.cancelledReason,
+ this.latitude,
+ this.longitude,
+ this.locationName,
+ this.importantInfo = const [],
});
+ /// Safely parse a double from backend (may arrive as String or num)
+ static double? _parseDouble(dynamic raw) {
+ if (raw == null) return null;
+ if (raw is num) return raw.toDouble();
+ if (raw is String) return double.tryParse(raw);
+ return null;
+ }
+
+ /// Safely parse important_info from backend (list of {title, value} maps)
+ static List> _parseImportantInfo(dynamic raw) {
+ if (raw is List) {
+ return raw.map>((e) {
+ if (e is Map) {
+ return {
+ 'title': (e['title'] ?? '').toString(),
+ 'value': (e['value'] ?? '').toString(),
+ };
+ }
+ return {'title': '', 'value': e.toString()};
+ }).toList();
+ }
+ return [];
+ }
+
factory EventModel.fromJson(Map j) {
final imgs = [];
if (j['images'] is List) {
@@ -99,6 +135,10 @@ class EventModel {
venueName: j['venue_name'] as String?,
eventStatus: j['event_status'] as String?,
cancelledReason: j['cancelled_reason'] as String?,
+ latitude: _parseDouble(j['latitude']),
+ longitude: _parseDouble(j['longitude']),
+ locationName: j['location_name'] as String?,
+ importantInfo: _parseImportantInfo(j['important_info']),
);
}
}
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
index 7cc73b9..105c78f 100644
--- a/lib/screens/home_screen.dart
+++ b/lib/screens/home_screen.dart
@@ -12,6 +12,7 @@ import 'contribute_screen.dart';
import 'learn_more_screen.dart';
import 'search_screen.dart';
import '../core/app_decoration.dart';
+import 'package:flutter_svg/flutter_svg.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@@ -125,46 +126,69 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
required String label,
required bool selected,
required VoidCallback onTap,
+ String? imageUrl,
IconData? icon,
}) {
final theme = Theme.of(context);
- return InkWell(
- borderRadius: BorderRadius.circular(20),
+ return GestureDetector(
onTap: onTap,
child: Container(
- height: 40,
- alignment: Alignment.center,
- padding: const EdgeInsets.symmetric(horizontal: 16),
+ width: 110,
decoration: BoxDecoration(
- color: selected ? theme.colorScheme.primary : theme.cardColor,
- borderRadius: BorderRadius.circular(20),
- border: Border.all(
- color: selected ? theme.colorScheme.primary : theme.dividerColor,
- width: 1,
- ),
- boxShadow: selected
- ? [
- BoxShadow(
- color: theme.colorScheme.primary.withOpacity(0.3),
- blurRadius: 8,
- offset: const Offset(0, 4),
- )
- ]
- : [],
+ color: selected ? theme.colorScheme.primary : Colors.white,
+ borderRadius: BorderRadius.circular(18),
+ boxShadow: [
+ BoxShadow(
+ color: selected
+ ? theme.colorScheme.primary.withOpacity(0.35)
+ : Colors.black.withOpacity(0.06),
+ blurRadius: selected ? 12 : 8,
+ offset: const Offset(0, 4),
+ ),
+ ],
),
- child: Row(
- mainAxisSize: MainAxisSize.min,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
children: [
- if (icon != null) ...[
- Icon(icon, size: 16, color: selected ? Colors.white : theme.colorScheme.primary),
- const SizedBox(width: 6),
- ],
- Text(
- label,
- style: TextStyle(
- fontSize: 14,
- fontWeight: FontWeight.w600,
- color: selected ? Colors.white : theme.textTheme.bodyLarge?.color,
+ // Image / Icon area
+ SizedBox(
+ height: 56,
+ width: 56,
+ child: imageUrl != null && imageUrl.isNotEmpty
+ ? ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: Image.network(
+ imageUrl,
+ fit: BoxFit.contain,
+ errorBuilder: (_, __, ___) => Icon(
+ icon ?? Icons.category,
+ size: 36,
+ color: selected ? Colors.white : theme.colorScheme.primary,
+ ),
+ ),
+ )
+ : Icon(
+ icon ?? Icons.category,
+ size: 36,
+ color: selected ? Colors.white : theme.colorScheme.primary,
+ ),
+ ),
+ const SizedBox(height: 10),
+ // Label
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 6),
+ child: Text(
+ label,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ fontSize: 13,
+ fontWeight: FontWeight.w700,
+ color: selected
+ ? Colors.white
+ : theme.textTheme.bodyLarge?.color ?? Colors.black87,
+ ),
),
),
],
@@ -367,39 +391,96 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
- _bottomNavItem(0, Icons.home, 'Home'),
- _bottomNavItem(1, Icons.calendar_today, 'Calendar'),
- _bottomNavItem(2, Icons.volunteer_activism, 'Contribute'),
- _bottomNavItem(3, Icons.person, 'Profile'),
+ _bottomNavItem(
+ 0,
+ Icon(
+ Icons.home,
+ color: _selectedIndex == 0
+ ? theme.colorScheme.primary
+ : theme.iconTheme.color,
+ ),
+ 'Home',
+ ),
+ _bottomNavItem(
+ 1,
+ Icon(
+ Icons.calendar_today,
+ color: _selectedIndex == 1
+ ? theme.colorScheme.primary
+ : theme.iconTheme.color,
+ ),
+ 'Calendar',
+ ),
+ _bottomNavItem(
+ 2,
+ SvgPicture.asset(
+ 'assets/icon/hand_stop.svg',
+ height: 24,
+ width: 24,
+ colorFilter: ColorFilter.mode(
+ _selectedIndex == 2
+ ? theme.colorScheme.primary
+ : theme.iconTheme.color!,
+ BlendMode.srcIn,
+ ),
+ ),
+ 'Contribute',
+ ),
+ _bottomNavItem(
+ 3,
+ Icon(
+ Icons.person,
+ color: _selectedIndex == 3
+ ? theme.colorScheme.primary
+ : theme.iconTheme.color,
+ ),
+ 'Profile',
+ ),
+
],
),
);
}
- Widget _bottomNavItem(int index, IconData icon, String label) {
+ Widget _bottomNavItem(int index, Widget icon, String label) {
final theme = Theme.of(context);
bool active = _selectedIndex == index;
+
return GestureDetector(
onTap: () {
setState(() {
_selectedIndex = index;
});
},
- child: Column(mainAxisSize: MainAxisSize.min, children: [
- Container(
- padding: const EdgeInsets.all(8),
- decoration: BoxDecoration(
- color: active ? theme.colorScheme.primary.withOpacity(0.08) : Colors.transparent,
- shape: BoxShape.circle,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: const EdgeInsets.all(8),
+ decoration: BoxDecoration(
+ color: active
+ ? theme.colorScheme.primary.withOpacity(0.08)
+ : Colors.transparent,
+ shape: BoxShape.circle,
+ ),
+ child: icon,
),
- child: Icon(icon, color: active ? theme.colorScheme.primary : theme.iconTheme.color),
- ),
- const SizedBox(height: 4),
- Text(label, style: theme.textTheme.bodySmall?.copyWith(color: active ? theme.colorScheme.primary : theme.iconTheme.color, fontSize: 12)),
- ]),
+ const SizedBox(height: 4),
+ Text(
+ label,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: active
+ ? theme.colorScheme.primary
+ : theme.iconTheme.color,
+ fontSize: 12,
+ ),
+ ),
+ ],
+ ),
);
}
+
// Get hero events (first 4 events for the carousel)
List get _heroEvents => _events.take(4).toList();
@@ -537,7 +618,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
// Hero image carousel (PageView) and fixed indicators under it.
_heroEvents.isEmpty
? SizedBox(
- height: 360,
+ height: 240,
child: Center(
child: _loading ? const CircularProgressIndicator(color: Colors.white) : const Text('No events available', style: TextStyle(color: Colors.white70)),
),
@@ -546,7 +627,7 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
children: [
// PageView with only the images/titles
SizedBox(
- height: 360,
+ height: 300,
child: PageView.builder(
controller: _heroPageController,
onPageChanged: (page) {
@@ -560,9 +641,9 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
),
// fixed indicators (outside PageView)
- const SizedBox(height: 12),
+ const SizedBox(height: 20),
SizedBox(
- height: 28,
+ height: 20,
child: Center(
child: AnimatedBuilder(
animation: _heroPageController,
@@ -575,8 +656,8 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
mainAxisSize: MainAxisSize.min,
children: List.generate(_heroEvents.length, (i) {
final dx = (i - page).abs();
- final t = 1.0 - dx.clamp(0.0, 1.0); // 1 when focused, 0 when far
- final width = 10 + (36 - 10) * t; // interpolate between 10 and 36
+ final t = 1.0 - dx.clamp(0.0, 1.0);
+ final width = 7 + (24 - 7) * t;
final opacity = 0.35 + (0.65 * t);
return GestureDetector(
onTap: () {
@@ -589,12 +670,12 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
}
},
child: Container(
- margin: const EdgeInsets.symmetric(horizontal: 8),
+ margin: const EdgeInsets.symmetric(horizontal: 5),
width: width,
- height: 10,
+ height: 7,
decoration: BoxDecoration(
color: Colors.white.withOpacity(opacity),
- borderRadius: BorderRadius.circular(6),
+ borderRadius: BorderRadius.circular(4),
),
),
);
@@ -614,9 +695,8 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
);
}
- /// Build a hero image card (image + gradient + title).
- /// If there's no image, show the AppDecoration blue gradient rounded background
- /// and a black overlay gradient for contrast.
+ /// Build a hero image card with the image only (rounded),
+ /// and the title text placed below the image.
Widget _buildHeroEventImage(EventModel event) {
String? img;
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) {
@@ -626,7 +706,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
}
final radius = 24.0;
- final startDate = event.startDate ?? '';
return GestureDetector(
onTap: () {
@@ -636,75 +715,46 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
- child: ClipRRect(
- borderRadius: BorderRadius.circular(radius),
- child: Stack(
- fit: StackFit.expand,
- children: [
- // If image available show it; otherwise use AppDecoration blue gradient.
- if (img != null && img.isNotEmpty)
- Image.network(
- img,
- fit: BoxFit.cover,
- errorBuilder: (context, error, stackTrace) => Container(decoration: AppDecoration.blueGradientRounded(radius)),
- )
- else
- Container(
- decoration: AppDecoration.blueGradientRounded(radius),
- ),
-
- // BLACK gradient overlay to darken bottom area for text (stronger to match your reference)
- Container(
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.bottomCenter,
- end: Alignment.topCenter,
- colors: [
- Colors.black.withOpacity(0.72), // strong black near bottom for contrast
- Colors.black.withOpacity(0.38),
- Colors.black.withOpacity(0.08), // subtle near top
- ],
- stops: const [0.0, 0.45, 1.0],
- ),
- ),
- ),
-
- // Title and date positioned bottom-left
- Padding(
- padding: const EdgeInsets.fromLTRB(20, 20, 20, 18),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.end,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (startDate.isNotEmpty)
- Text(
- startDate,
- style: TextStyle(
- color: Colors.white.withOpacity(0.9),
- fontSize: 14,
- fontWeight: FontWeight.w600,
+ child: Column(
+ children: [
+ // Image only (no text overlay)
+ Expanded(
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(radius),
+ child: SizedBox(
+ width: double.infinity,
+ child: img != null && img.isNotEmpty
+ ? Image.network(
+ img,
+ fit: BoxFit.cover,
+ errorBuilder: (context, error, stackTrace) =>
+ Container(decoration: AppDecoration.blueGradientRounded(radius)),
+ )
+ : Container(
+ decoration: AppDecoration.blueGradientRounded(radius),
),
- ),
- if (startDate.isNotEmpty) const SizedBox(height: 8),
- Text(
- event.title ?? event.name ?? '',
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 22,
- fontWeight: FontWeight.bold,
- height: 1.1,
- shadows: [
- Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)),
- ],
- ),
- ),
- ],
),
),
- ],
- ),
+ ),
+
+ // Title text outside the image
+ const SizedBox(height: 12),
+ Text(
+ event.title ?? event.name ?? '',
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.center,
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 22,
+ fontWeight: FontWeight.bold,
+ height: 1.2,
+ shadows: [
+ Shadow(color: Colors.black38, blurRadius: 6, offset: Offset(0, 2)),
+ ],
+ ),
+ ),
+ ],
),
),
);
@@ -769,9 +819,9 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
),
),
- // Category chips
+ // Category chips (card-style)
SizedBox(
- height: 48,
+ height: 140,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
@@ -782,15 +832,16 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
selected: _selectedTypeId == -1,
onTap: () => _onSelectType(-1),
),
- const SizedBox(width: 10),
+ const SizedBox(width: 12),
for (final t in _types) ...[
_categoryChip(
label: t.name,
+ imageUrl: t.iconUrl,
icon: _getIconForType(t.name),
selected: _selectedTypeId == t.id,
onTap: () => _onSelectType(t.id),
),
- const SizedBox(width: 10),
+ const SizedBox(width: 12),
],
],
),
@@ -798,33 +849,42 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
const SizedBox(height: 16),
- // Event cards
- if (_loading)
- const Padding(
- padding: EdgeInsets.all(40),
- child: Center(child: CircularProgressIndicator()),
- )
- else if (_events.isEmpty)
- Padding(
- padding: const EdgeInsets.all(40),
- child: Center(
- child: Text(
- 'No events found',
- style: theme.textTheme.bodyLarge?.copyWith(color: theme.hintColor),
+ // --- NEW: when All Events is active, show only "types that have events"
+ if (_selectedTypeId == -1) ...[
+ if (_loading)
+ const Padding(
+ padding: EdgeInsets.all(40),
+ child: Center(child: CircularProgressIndicator()),
+ )
+ else
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Column(
+ children: [
+ for (final t in _types)
+ if (_events.where((e) => e.eventTypeId == t.id).isNotEmpty) ...[
+ _buildTypeSection(t),
+ const SizedBox(height: 18),
+ ],
+ const SizedBox(height: 24),
+ ],
),
),
- )
- else
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- children: [
- for (int i = 0; i < _events.length; i++) ...[
- _buildEventCard(_events[i], i),
- ],
- ],
+ ] else ...[
+ // Selected a specific type -> show filtered events in vertical list (full cards)
+ if (_loading)
+ const Padding(
+ padding: EdgeInsets.all(40),
+ child: Center(child: CircularProgressIndicator()),
+ )
+ else
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ child: Column(
+ children: _events.map((e) => _buildFullWidthCard(e)).toList(),
+ ),
),
- ),
+ ],
// Bottom padding for nav bar
const SizedBox(height: 100),
@@ -832,6 +892,471 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
);
}
+ /// Build a type section that follows your requested layout rules:
+ /// - If type has <= 5 events => single horizontal row of compact cards.
+ /// - If type has >= 6 events => arrange events into column groups of 3 (so visually there are 3 rows across horizontally scrollable columns).
+ Widget _buildTypeSection(EventTypeModel type) {
+ final theme = Theme.of(context);
+ final eventsForType = _events.where((e) => e.eventTypeId == type.id).toList();
+ final n = eventsForType.length;
+
+ // Header row
+ Widget header = Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(type.name, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
+ TextButton(
+ onPressed: () {
+ _onSelectType(type.id);
+ },
+ child: Text('View All', style: TextStyle(color: theme.colorScheme.primary, fontWeight: FontWeight.w600)),
+ ),
+ ],
+ ),
+ );
+
+ // If <= 5 events: show one horizontal row using _buildHorizontalEventCard
+ if (n <= 5) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ header,
+ const SizedBox(height: 8),
+ SizedBox(
+ height: 290, // card height: image 180 + text ~110
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ itemBuilder: (ctx, idx) => _buildHorizontalEventCard(eventsForType[idx]),
+ separatorBuilder: (_, __) => const SizedBox(width: 12),
+ itemCount: eventsForType.length,
+ ),
+ ),
+ ],
+ );
+ }
+
+ // For 6+ events: arrange into columns where each column has up to 3 stacked cards.
+ final columnsCount = (n / 3).ceil();
+ final columnWidth = 260.0; // narrower so second column peeks in
+ final verticalCardHeight = 120.0; // each stacked card height matches sample
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ header,
+ const SizedBox(height: 8),
+
+ // Container height must accommodate 3 stacked cards + small gaps
+ SizedBox(
+ height: (verticalCardHeight * 3) + 16, // 3 cards + spacing
+ child: ListView.separated(
+ scrollDirection: Axis.horizontal,
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ separatorBuilder: (_, __) => const SizedBox(width: 12),
+ itemBuilder: (ctx, colIndex) {
+ // Build one column: contains up to 3 items: indices colIndex*3 + 0/1/2
+ return Container(
+ width: columnWidth,
+ child: Column(
+ children: [
+ // top card
+ if ((colIndex * 3 + 0) < n)
+ SizedBox(
+ height: verticalCardHeight,
+ child: _buildStackedCard(eventsForType[colIndex * 3 + 0]),
+ )
+ else
+ const SizedBox(height: 0),
+ const SizedBox(height: 8),
+ // middle card
+ if ((colIndex * 3 + 1) < n)
+ SizedBox(
+ height: verticalCardHeight,
+ child: _buildStackedCard(eventsForType[colIndex * 3 + 1]),
+ )
+ else
+ const SizedBox(height: 0),
+ const SizedBox(height: 8),
+ // bottom card
+ if ((colIndex * 3 + 2) < n)
+ SizedBox(
+ height: verticalCardHeight,
+ child: _buildStackedCard(eventsForType[colIndex * 3 + 2]),
+ )
+ else
+ const SizedBox(height: 0),
+ ],
+ ),
+ );
+ },
+ itemCount: columnsCount,
+ ),
+ ),
+ ],
+ );
+ }
+
+ /// A stacked card styled to match your sample (left square thumbnail, bold title).
+ /// REMOVED: price/rating row (per your request).
+ Widget _buildStackedCard(EventModel e) {
+ final theme = Theme.of(context);
+ String? img;
+ if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
+ img = e.thumbImg;
+ } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
+ img = e.images.first.image;
+ }
+
+ return GestureDetector(
+ onTap: () {
+ if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
+ },
+ child: Container(
+ margin: const EdgeInsets.symmetric(vertical: 0),
+ decoration: BoxDecoration(
+ color: theme.cardColor,
+ borderRadius: BorderRadius.circular(16),
+ boxShadow: [BoxShadow(color: theme.shadowColor.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 8))],
+ ),
+ padding: const EdgeInsets.all(12),
+ child: Row(
+ children: [
+ // thumbnail square (rounded)
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: img != null && img.isNotEmpty
+ ? Image.network(img, width: 96, height: double.infinity, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(width: 96, color: theme.dividerColor))
+ : Container(width: 96, height: double.infinity, color: theme.dividerColor),
+ ),
+ const SizedBox(width: 14),
+ Expanded(
+ child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [
+ Text(e.title ?? e.name ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 18)),
+ // removed price/rating row here per request
+ ]),
+ ),
+ // optional heart icon aligned top-right
+ Icon(Icons.favorite_border, color: theme.hintColor),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// Compact card used inside the one-row layout for small counts (<=5).
+ /// Matches Figma: vertical card with image, date badge, title, location, "Free".
+ Widget _buildHorizontalEventCard(EventModel e) {
+ final theme = Theme.of(context);
+ String? img;
+ if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
+ img = e.thumbImg;
+ } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
+ img = e.images.first.image;
+ }
+
+ // Parse day & month for the date badge
+ String day = '';
+ String month = '';
+ try {
+ final parts = e.startDate.split('-');
+ if (parts.length == 3) {
+ day = int.parse(parts[2]).toString();
+ const months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
+ month = months[int.parse(parts[1]) - 1];
+ }
+ } catch (_) {}
+
+ final venue = e.venueName ?? e.place ?? '';
+
+ return GestureDetector(
+ onTap: () {
+ if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
+ },
+ child: SizedBox(
+ width: 220,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Image with date badge
+ Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(18),
+ child: img != null && img.isNotEmpty
+ ? Image.network(
+ img,
+ width: 220,
+ height: 180,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ width: 220,
+ height: 180,
+ decoration: BoxDecoration(
+ color: theme.dividerColor,
+ borderRadius: BorderRadius.circular(18),
+ ),
+ child: Icon(Icons.image, size: 40, color: theme.hintColor),
+ ),
+ )
+ : Container(
+ width: 220,
+ height: 180,
+ decoration: BoxDecoration(
+ color: theme.dividerColor,
+ borderRadius: BorderRadius.circular(18),
+ ),
+ child: Icon(Icons.image, size: 40, color: theme.hintColor),
+ ),
+ ),
+ // Date badge
+ if (day.isNotEmpty)
+ Positioned(
+ top: 10,
+ right: 10,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.08),
+ blurRadius: 6,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ day,
+ style: const TextStyle(
+ fontSize: 16,
+ fontWeight: FontWeight.w800,
+ color: Colors.black87,
+ height: 1.1,
+ ),
+ ),
+ Text(
+ month,
+ style: const TextStyle(
+ fontSize: 11,
+ fontWeight: FontWeight.w700,
+ color: Colors.black54,
+ height: 1.2,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 10),
+ // Title
+ Text(
+ e.title ?? e.name ?? '',
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ fontSize: 16,
+ ),
+ ),
+ if (venue.isNotEmpty) ...[
+ const SizedBox(height: 4),
+ Text(
+ venue,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.hintColor,
+ fontSize: 13,
+ ),
+ ),
+ ],
+ const SizedBox(height: 4),
+ Text(
+ 'Free',
+ style: TextStyle(
+ color: theme.colorScheme.primary,
+ fontWeight: FontWeight.w700,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ /// Format a date string (YYYY-MM-DD) to short display like "4 Mar".
+ String _formatDateShort(String dateStr) {
+ try {
+ final parts = dateStr.split('-');
+ if (parts.length == 3) {
+ final day = int.parse(parts[2]);
+ const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
+ final month = months[int.parse(parts[1]) - 1];
+ return '$day $month';
+ }
+ } catch (_) {}
+ return dateStr;
+ }
+
+ /// Full width card used when a single type is selected (vertical list).
+ /// Matches Figma: large image, badge, title, date + venue.
+ Widget _buildFullWidthCard(EventModel e) {
+ final theme = Theme.of(context);
+ String? img;
+ if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
+ img = e.thumbImg;
+ } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
+ img = e.images.first.image;
+ }
+
+ // Build date range string
+ final startShort = _formatDateShort(e.startDate);
+ final endShort = _formatDateShort(e.endDate);
+ final dateRange = startShort == endShort ? startShort : '$startShort - $endShort';
+
+ final venue = e.venueName ?? e.place ?? '';
+
+ return GestureDetector(
+ onTap: () {
+ if (e.id != null) Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
+ },
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 18),
+ decoration: BoxDecoration(
+ color: theme.cardColor,
+ borderRadius: BorderRadius.circular(20),
+ boxShadow: [
+ BoxShadow(color: theme.shadowColor.withOpacity(0.10), blurRadius: 16, offset: const Offset(0, 6)),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Image with badge
+ Stack(
+ children: [
+ ClipRRect(
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ child: img != null && img.isNotEmpty
+ ? Image.network(
+ img,
+ width: double.infinity,
+ height: 200,
+ fit: BoxFit.cover,
+ errorBuilder: (_, __, ___) => Container(
+ width: double.infinity,
+ height: 200,
+ decoration: BoxDecoration(
+ color: theme.dividerColor,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Icon(Icons.image, size: 48, color: theme.hintColor),
+ ),
+ )
+ : Container(
+ width: double.infinity,
+ height: 200,
+ decoration: BoxDecoration(
+ color: theme.dividerColor,
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
+ ),
+ child: Icon(Icons.image, size: 48, color: theme.hintColor),
+ ),
+ ),
+ // "ADDED BY EVENTIFY" badge
+ Positioned(
+ top: 14,
+ left: 14,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
+ decoration: BoxDecoration(
+ color: theme.colorScheme.primary,
+ borderRadius: BorderRadius.circular(20),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: const [
+ Icon(Icons.star, color: Colors.white, size: 14),
+ SizedBox(width: 4),
+ Text(
+ 'ADDED BY EVENTIFY',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 11,
+ fontWeight: FontWeight.w700,
+ letterSpacing: 0.5,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ // Title + date/venue
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ e.title ?? e.name ?? '',
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.bold,
+ fontSize: 17,
+ ),
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ Icon(Icons.calendar_today_outlined, size: 14, color: theme.hintColor),
+ const SizedBox(width: 4),
+ Text(
+ dateRange,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.hintColor,
+ fontSize: 13,
+ ),
+ ),
+ if (venue.isNotEmpty) ...[
+ const SizedBox(width: 12),
+ Icon(Icons.location_on_outlined, size: 14, color: theme.hintColor),
+ const SizedBox(width: 3),
+ Expanded(
+ child: Text(
+ venue,
+ style: theme.textTheme.bodySmall?.copyWith(
+ color: theme.hintColor,
+ fontSize: 13,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
IconData _getIconForType(String typeName) {
final name = typeName.toLowerCase();
if (name.contains('music')) return Icons.music_note;
@@ -857,85 +1382,6 @@ class _HomeScreenState extends State with SingleTickerProviderStateM
}
}
- Widget _buildEventCard(EventModel e, int index) {
- final theme = Theme.of(context);
- String? img;
- if (e.thumbImg != null && e.thumbImg!.isNotEmpty) {
- img = e.thumbImg;
- } else if (e.images.isNotEmpty && e.images.first.image.isNotEmpty) {
- img = e.images.first.image;
- }
-
- return GestureDetector(
- onTap: () {
- if (e.id != null) {
- Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: e.id)));
- }
- },
- child: Container(
- margin: const EdgeInsets.only(bottom: 18),
- decoration: BoxDecoration(
- color: theme.cardColor,
- borderRadius: BorderRadius.circular(16),
- boxShadow: [
- BoxShadow(color: theme.shadowColor.withOpacity(0.12), blurRadius: 18, offset: const Offset(0, 8)),
- BoxShadow(color: theme.shadowColor.withOpacity(0.04), blurRadius: 6, offset: const Offset(0, 2)),
- ],
- ),
- child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- ClipRRect(
- borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
- child: img != null && img.isNotEmpty
- ? Image.network(img, fit: BoxFit.cover, width: double.infinity, height: 160)
- : Image.asset('assets/images/event1.jpg', fit: BoxFit.cover, width: double.infinity, height: 160),
- ),
- Padding(
- padding: const EdgeInsets.fromLTRB(12, 12, 12, 12),
- child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- Text(
- e.title ?? e.name ?? '',
- maxLines: 2,
- overflow: TextOverflow.ellipsis,
- style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold, fontSize: 16),
- ),
- const SizedBox(height: 8),
- Row(
- children: [
- Icon(Icons.calendar_today, size: 14, color: theme.colorScheme.primary),
- const SizedBox(width: 6),
- Flexible(
- flex: 2,
- child: Text(
- '${e.startDate ?? ''}',
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13),
- ),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: Text('•', style: TextStyle(color: theme.textTheme.bodySmall?.color?.withOpacity(0.4))),
- ),
- Icon(Icons.location_on, size: 14, color: theme.colorScheme.primary),
- const SizedBox(width: 6),
- Flexible(
- flex: 3,
- child: Text(
- e.place ?? '',
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withOpacity(0.9), fontSize: 13),
- ),
- ),
- ],
- ),
- ]),
- )
- ]),
- ),
- );
- }
-
String _getShortEmailLabel() {
try {
final parts = _username.split('@');
diff --git a/lib/screens/learn_more_screen.dart b/lib/screens/learn_more_screen.dart
index a8835ad..13ef221 100644
--- a/lib/screens/learn_more_screen.dart
+++ b/lib/screens/learn_more_screen.dart
@@ -1,9 +1,13 @@
// 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';
-import 'booking_screen.dart';
class LearnMoreScreen extends StatefulWidget {
final int eventId;
@@ -20,22 +24,50 @@ class _LearnMoreScreenState extends State {
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 _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());
@@ -44,131 +76,1030 @@ class _LearnMoreScreenState extends State {
}
}
- Widget _buildImageCarousel() {
- final imgs = _event?.images ?? [];
- final thumb = _event?.thumbImg;
+ // ---------------------------------------------------------------------------
+ // Carousel helpers
+ // ---------------------------------------------------------------------------
+
+ List get _imageUrls {
final list = [];
-
+ if (_event == null) return list;
+ final thumb = _event!.thumbImg;
if (thumb != null && thumb.isNotEmpty) list.add(thumb);
- for (final i in imgs) {
- if (i.image.isNotEmpty && !list.contains(i.image)) list.add(i.image);
+ for (final img in _event!.images) {
+ if (img.image.isNotEmpty && !list.contains(img.image)) list.add(img.image);
}
-
- if (list.isEmpty) {
- return Container(
- height: 220,
- color: Colors.grey.shade200,
- child: const Center(child: Icon(Icons.event, size: 80, color: Colors.grey)),
- );
- }
-
- return SizedBox(
- height: 220,
- child: PageView.builder(
- itemCount: list.length,
- itemBuilder: (context, i) => Image.network(list[i], fit: BoxFit.cover, width: double.infinity),
- ),
- );
+ 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 _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 _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(
- appBar: AppBar(
- title: const Text('Event Details'),
- ),
- body: _loading
- ? const Center(child: CircularProgressIndicator())
- : _error != null
- ? Center(child: Text('Error: $_error'))
- : _event == null
- ? const Center(child: Text('Event not found'))
- : SingleChildScrollView(
- child: DefaultTextStyle.merge(
- // force child Text widgets to use theme-aware foreground color (works in light/dark)
- style: TextStyle(color: theme.colorScheme.onSurface, height: 1.45),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- _buildImageCarousel(),
- Padding(
- padding: const EdgeInsets.all(16.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // Title — use theme typography
- Text(
- _event!.title ?? _event!.name ?? '',
- style: theme.textTheme.headlineSmall?.copyWith(fontSize: 22, fontWeight: FontWeight.bold),
- ),
- const SizedBox(height: 8),
+ backgroundColor: theme.scaffoldBackgroundColor,
+ body: Stack(
+ children: [
+ // ── LAYER 1: Image carousel (background) ──
+ _buildImageCarousel(theme, imageHeight),
- // Meta row (date, location) — icons will use theme icon color
- Row(
- children: [
- Icon(Icons.calendar_today, size: 16, color: theme.iconTheme.color),
- const SizedBox(width: 6),
- Text(
- '${_event!.startDate}${_event!.startTime != null ? ' • ${_event!.startTime}' : ''}',
- style: theme.textTheme.bodyMedium,
- ),
- const SizedBox(width: 12),
- Icon(Icons.location_on, size: 16, color: theme.iconTheme.color),
- const SizedBox(width: 6),
- Flexible(child: Text(_event!.place ?? _event!.venueName ?? '', style: theme.textTheme.bodyMedium)),
- ],
- ),
- const SizedBox(height: 12),
+ // ── 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),
- // Description — themed body text (no hardcoded black)
- Text(
- _event!.description ?? '',
- style: theme.textTheme.bodyMedium,
- ),
- const SizedBox(height: 16),
+ // 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),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
- // Important section (if present)
- if ((_event!.importantInformation ?? '').isNotEmpty) ...[
- Text('Important', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold)),
- const SizedBox(height: 6),
- Text(_event!.importantInformation!, style: theme.textTheme.bodyMedium),
- const SizedBox(height: 12),
- ],
-
- // Book button
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- ElevatedButton(
- onPressed: () {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (_) => BookingScreen(
- onBook: () {
- ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Booked (demo)')));
- },
- image: _event!.thumbImg ?? '',
- ),
- ),
- );
- },
- child: const Padding(
- padding: EdgeInsets.symmetric(horizontal: 14, vertical: 12),
- child: Text('Book Tickets'),
- ),
- ),
- ],
- ),
- ],
+ // ── 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> _parseHtmlImportantInfo(String raw) {
+ // Strip HTML tags, preserving as a newline separator first
+ var text = raw
+ .replaceAll(RegExp(r' ', 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 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 = >[];
+ 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,
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
);
}
}
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index 64a0ece..7299b5c 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -7,9 +7,13 @@
#include "generated_plugin_registrant.h"
#include
+#include
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
+ g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+ fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+ url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 2db3c22..786ff5c 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
+ url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 90d7d0f..bb0698f 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -7,10 +7,16 @@ import Foundation
import file_selector_macos
import geolocator_apple
+import path_provider_foundation
+import share_plus
import shared_preferences_foundation
+import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
+ PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
diff --git a/pubspec.lock b/pubspec.lock
index 27e303b..1f9710c 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -206,6 +206,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.30"
+ flutter_svg:
+ dependency: "direct main"
+ description:
+ name: flutter_svg
+ sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.3"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -296,6 +304,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.3"
+ google_maps:
+ dependency: transitive
+ description:
+ name: google_maps
+ sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468"
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.2.0"
+ google_maps_flutter:
+ dependency: "direct main"
+ description:
+ name: google_maps_flutter
+ sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.14.2"
+ google_maps_flutter_android:
+ dependency: transitive
+ description:
+ name: google_maps_flutter_android
+ sha256: "98d7f5354f770f3e993db09fc798d40aeb6a254f04c1c468a94818ec2086e83e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.18.12"
+ google_maps_flutter_ios:
+ dependency: transitive
+ description:
+ name: google_maps_flutter_ios
+ sha256: "38f0a9ee858b0de3a5105e7efe200f154eea8397eb0c36bea6b3810429fbc0e4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.17.3"
+ google_maps_flutter_platform_interface:
+ dependency: transitive
+ description:
+ name: google_maps_flutter_platform_interface
+ sha256: e8b1232419fcdd35c1fdafff96843f5a40238480365599d8ca661dde96d283dd
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.14.1"
+ google_maps_flutter_web:
+ dependency: transitive
+ description:
+ name: google_maps_flutter_web
+ sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.14+3"
html:
dependency: transitive
description:
@@ -472,6 +528,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
+ path_parsing:
+ dependency: transitive
+ description:
+ name: path_parsing
+ sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ path_provider:
+ dependency: transitive
+ description:
+ name: path_provider
+ sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.5"
+ path_provider_android:
+ dependency: transitive
+ description:
+ name: path_provider_android
+ sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.22"
+ path_provider_foundation:
+ dependency: transitive
+ description:
+ name: path_provider_foundation
+ sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.1"
path_provider_linux:
dependency: transitive
description:
@@ -528,6 +616,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.3"
+ sanitize_html:
+ dependency: transitive
+ description:
+ name: sanitize_html
+ sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ share_plus:
+ dependency: "direct main"
+ description:
+ name: share_plus
+ sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.2.2"
+ share_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: share_plus_platform_interface
+ sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.4.0"
shared_preferences:
dependency: "direct main"
description:
@@ -621,6 +733,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -669,6 +789,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.2"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.28"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.6"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.2"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.5"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.5"
uuid:
dependency: transitive
description:
@@ -677,6 +861,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.2"
+ vector_graphics:
+ dependency: transitive
+ description:
+ name: vector_graphics
+ sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.19"
+ vector_graphics_codec:
+ dependency: transitive
+ description:
+ name: vector_graphics_codec
+ sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.13"
+ vector_graphics_compiler:
+ dependency: transitive
+ description:
+ name: vector_graphics_compiler
+ sha256: "201e876b5d52753626af64b6359cd13ac6011b80728731428fd34bc840f71c9b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.20"
vector_math:
dependency: transitive
description:
@@ -701,6 +909,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.15.0"
xdg_directories:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index a4b52f8..e2199ae 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -15,6 +15,10 @@ dependencies:
table_calendar: ^3.2.0
geolocator: ^9.0.2
geocoding: ^2.0.5
+ flutter_svg: ^2.0.9
+ google_maps_flutter: ^2.5.0
+ url_launcher: ^6.2.1
+ share_plus: ^7.2.1
dev_dependencies:
flutter_test:
@@ -26,6 +30,7 @@ flutter:
uses-material-design: true
assets:
- assets/images/
+ - assets/icon/hand_stop.svg
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index f35b3a6..58f82d3 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,10 +8,16 @@
#include
#include
+#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
+ SharePlusWindowsPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
+ UrlLauncherWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 389222b..8f2921f 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,6 +5,8 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
geolocator_windows
+ share_plus
+ url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST