Compare commits

...

3 Commits

Author SHA1 Message Date
809912305a chore: add Gilroy fonts, update dependencies, add gitignore entries
- Added Gilroy font family (12 variants)
- Added geolocator, geocoding, google_maps_flutter packages
- Updated pubspec.lock and macOS plugin registrant
- Added .gitignore entry for large video assets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 08:59:15 +05:30
e0f34398c2 feat: update login, event detail, theme, and API client
- Improved event detail page with carousel, map, and layout fixes
- Updated login screen with video background and glassmorphism
- API client development mode with mock responses
- Theme manager and main app updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 08:57:25 +05:30
4acf75902c feat: responsive layout, date filtering, location search, calendar fix
- Make web responsive layout use width-based mobile detection (820px)
- Add date filter chips that actually filter events by date ranges
- Custom calendar dialog with event dots on Date chip tap
- Update location search with Kerala cities and pincode display
- Fix calendar screen overflow errors and broken event indicators
- Replace thumbnail indicators with clean colored dots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 08:55:21 +05:30
26 changed files with 1693 additions and 860 deletions

1
.gitignore vendored
View File

@@ -45,3 +45,4 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
web/assets/login-bg.mp4

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -6,6 +6,8 @@ import '../storage/token_storage.dart';
class ApiClient {
static const Duration _timeout = Duration(seconds: 30);
// Set to true to enable mock/offline development mode (useful when backend is unavailable)
static const bool _developmentMode = true;
/// POST request
///
@@ -34,6 +36,30 @@ class ApiClient {
.timeout(_timeout);
} catch (e) {
if (kDebugMode) debugPrint('ApiClient.post network error: $e');
// Development mode: return mock responses for common endpoints on network errors
if (_developmentMode) {
if (url.contains('/user/login/')) {
if (kDebugMode) debugPrint('Development mode: returning mock login response');
final email = finalBody['username'] ?? 'test@example.com';
return {
'token': 'mock_token_${DateTime.now().millisecondsSinceEpoch}',
'username': email,
'email': email,
'phone_number': '+1234567890',
};
} else if (url.contains('/user/register/')) {
if (kDebugMode) debugPrint('Development mode: returning mock register response');
final email = finalBody['email'] ?? 'test@example.com';
return {
'token': 'mock_token_${DateTime.now().millisecondsSinceEpoch}',
'username': email,
'email': email,
'phone_number': finalBody['phone_number'] ?? '+1234567890',
};
}
}
throw Exception('Network error: $e');
}

View File

@@ -10,9 +10,15 @@ class ThemeManager {
/// Call during app startup to load saved preference.
static Future<void> init() async {
try {
final prefs = await SharedPreferences.getInstance();
final isDark = prefs.getBool(_prefKey) ?? false;
themeMode.value = isDark ? ThemeMode.dark : ThemeMode.light;
} catch (e) {
// If SharedPreferences fails, default to light theme
print('Error initializing theme: $e');
themeMode.value = ThemeMode.light;
}
}
/// Set theme and persist

View File

@@ -39,9 +39,12 @@ class MyApp extends StatelessWidget {
},
);
static const String _fontFamily = 'Gilroy';
ThemeData _lightTheme() {
return ThemeData(
brightness: Brightness.light,
fontFamily: _fontFamily,
primarySwatch: primarySwatch,
scaffoldBackgroundColor: const Color(0xFFF7F5FB),
appBarTheme: const AppBarTheme(
@@ -61,9 +64,9 @@ class MyApp extends StatelessWidget {
}
ThemeData _darkTheme() {
// Basic dark theme based on your sample — tweak colors as desired
return ThemeData(
brightness: Brightness.dark,
fontFamily: _fontFamily,
primarySwatch: primarySwatch,
scaffoldBackgroundColor: const Color(0xFF0B1220),
appBarTheme: const AppBarTheme(
@@ -75,7 +78,7 @@ class MyApp extends StatelessWidget {
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0B63D6)),
),
cardColor: const Color(0xFF0E1620),
textTheme: ThemeData.dark().textTheme,
textTheme: ThemeData.dark().textTheme.apply(fontFamily: _fontFamily),
useMaterial3: false,
);
}
@@ -115,9 +118,17 @@ class _StartupScreenState extends State<StartupScreen> {
}
Future<void> _loadLoginState() async {
try {
final prefs = await SharedPreferences.getInstance();
final hasEmail = prefs.getString('email') != null && prefs.getString('email')!.isNotEmpty;
setState(() => _loggedIn = hasEmail);
} catch (e) {
// If SharedPreferences fails (common on web with plugin issues), default to not logged in
print('Error loading login state: $e');
if (mounted) {
setState(() => _loggedIn = false);
}
}
}
@override

View File

@@ -1,5 +1,4 @@
// lib/screens/calendar_screen.dart
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../features/events/services/events_service.dart';
@@ -25,8 +24,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
final Set<String> _markedDates = {};
final Map<String, int> _dateCounts = {};
final Map<String, List<String>> _dateThumbnails = {};
List<EventModel> _eventsOfDay = [];
// Scroll controller for the calendar grid
@@ -67,7 +64,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
_loadingMonth = true;
_markedDates.clear();
_dateCounts.clear();
_dateThumbnails.clear();
_eventsOfDay = [];
});
@@ -95,9 +91,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
}
}
if (_markedDates.isNotEmpty) {
await _fetchThumbnailsForDates(_markedDates.toList());
}
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
} finally {
@@ -105,34 +98,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
}
}
Future<void> _fetchThumbnailsForDates(List<String> dates) async {
for (final date in dates) {
try {
final events = await _service.getEventsForDate(date);
final thumbs = <String>[];
for (final e in events) {
String? url;
if (e.thumbImg != null && e.thumbImg!.trim().isNotEmpty) {
url = e.thumbImg!.trim();
} else if (e.images.isNotEmpty && e.images.first.image.trim().isNotEmpty) {
url = e.images.first.image.trim();
}
if (url != null && url.isNotEmpty) thumbs.add(url);
if (thumbs.length >= 3) break;
}
if (thumbs.isNotEmpty) {
if (mounted) {
setState(() => _dateThumbnails[date] = thumbs);
} else {
_dateThumbnails[date] = thumbs;
}
}
} catch (_) {
// ignore per-date errors
}
}
}
Future<void> _onSelectDate(String yyyyMMdd) async {
setState(() {
_loadingDay = true;
@@ -381,9 +346,9 @@ class _CalendarScreenState extends State<CalendarScreen> {
itemCount: totalItems,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 7,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1,
mainAxisSpacing: 4,
crossAxisSpacing: 4,
childAspectRatio: 0.78,
),
itemBuilder: (context, index) {
final cellDate = firstCellDate.add(Duration(days: index));
@@ -391,8 +356,9 @@ class _CalendarScreenState extends State<CalendarScreen> {
final dayIndex = cellDate.day;
final key = _ymKey(cellDate);
final hasEvents = _markedDates.contains(key);
final thumbnails = _dateThumbnails[key] ?? [];
final eventCount = _dateCounts[key] ?? 0;
final isSelected = selectedDate.year == cellDate.year && selectedDate.month == cellDate.month && selectedDate.day == cellDate.day;
final isToday = cellDate.year == DateTime.now().year && cellDate.month == DateTime.now().month && cellDate.day == DateTime.now().day;
final dayTextColor = inCurrentMonth
? (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black87)
@@ -408,52 +374,57 @@ class _CalendarScreenState extends State<CalendarScreen> {
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// rounded date cell
Container(
width: 36,
height: 36,
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected ? primaryColor.withOpacity(0.14) : Colors.transparent,
color: isSelected
? primaryColor
: isToday
? primaryColor.withOpacity(0.12)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
child: Text(
'$dayIndex',
style: TextStyle(
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
color: isSelected ? primaryColor : dayTextColor,
fontSize: 14,
fontWeight: (isSelected || isToday) ? FontWeight.w700 : FontWeight.w500,
color: isSelected
? Colors.white
: isToday
? primaryColor
: dayTextColor,
fontSize: 13,
),
),
),
const SizedBox(height: 6),
// small event indicators (thumbnail overlap or dot)
if (hasEvents && thumbnails.isNotEmpty)
SizedBox(
height: 14,
child: Row(
const SizedBox(height: 3),
// event indicator dots
if (hasEvents && inCurrentMonth)
Row(
mainAxisSize: MainAxisSize.min,
children: thumbnails.take(2).toList().asMap().entries.map((entry) {
final i = entry.key;
final url = entry.value;
return Transform.translate(
offset: Offset(i * -6.0, 0),
child: Container(
margin: const EdgeInsets.only(left: 4),
width: 14,
height: 14,
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Theme.of(context).cardColor, width: 1.0)),
child: ClipOval(child: Image.network(url, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(color: Theme.of(context).dividerColor))),
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
eventCount.clamp(1, 3),
(i) => Container(
width: 5,
height: 5,
margin: EdgeInsets.only(left: i > 0 ? 2 : 0),
decoration: BoxDecoration(
color: isSelected
? primaryColor
: const Color(0xFFEF4444),
shape: BoxShape.circle,
),
),
);
}).toList(),
),
)
else if (hasEvents)
Container(width: 18, height: 6, decoration: BoxDecoration(color: primaryColor, borderRadius: BorderRadius.circular(6)))
else
const SizedBox.shrink(),
const SizedBox(height: 5),
],
),
);

View File

@@ -237,8 +237,8 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
Wrap(
alignment: WrapAlignment.spaceBetween,
children: [
TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")),
TextButton(onPressed: () {}, child: const Text('Contact support'))

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
// lib/screens/learn_more_screen.dart
import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:share_plus/share_plus.dart';
@@ -222,26 +223,25 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
}
final screenHeight = MediaQuery.of(context).size.height;
final imageHeight = screenHeight * 0.50;
final overlap = 30.0;
final imageHeight = screenHeight * 0.45;
final topPadding = MediaQuery.of(context).padding.top;
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: Stack(
children: [
// ── LAYER 1: Image carousel (background) ──
_buildImageCarousel(theme, imageHeight),
// ── LAYER 2: Scrollable content with overlapping white card ──
// ── Scrollable content (carousel + card scroll together) ──
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Transparent spacer — shows the image behind
SizedBox(height: imageHeight - overlap),
// Image carousel (scrolls with content)
_buildImageCarousel(theme, imageHeight),
// White card with rounded top corners overlapping image
Container(
// Content card with rounded top corners overlapping carousel
Transform.translate(
offset: const Offset(0, -28),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
@@ -274,15 +274,33 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
],
),
),
),
],
),
),
// ── LAYER 3: Floating icon row (above scrollview so taps work) ──
// ── Fixed top bar with back/share/heart buttons ──
Positioned(
top: MediaQuery.of(context).padding.top + 10,
top: 0,
left: 0,
right: 0,
child: Container(
padding: EdgeInsets.only(
top: topPadding + 10,
bottom: 10,
left: 16,
right: 16,
),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.5),
Colors.black.withOpacity(0.0),
],
),
),
child: Row(
children: [
_squareIconButton(
@@ -325,6 +343,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
],
),
),
),
],
),
);
@@ -492,7 +511,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
);
}
/// Square icon button with rounded corners and translucent white background
/// Square icon button with rounded corners and prominent background
Widget _squareIconButton({
required IconData icon,
required VoidCallback onTap,
@@ -501,12 +520,12 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
return GestureDetector(
onTap: onTap,
child: Container(
width: 42,
height: 42,
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: Colors.black.withOpacity(0.35),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.3)),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
child: Icon(icon, color: iconColor, size: 22),
),
@@ -640,6 +659,46 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
height: 280,
child: Stack(
children: [
// Use static map image on web (Google Maps JS SDK not configured),
// native GoogleMap widget on mobile
if (kIsWeb)
GestureDetector(
onTap: _viewLargerMap,
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(20),
),
child: Stack(
children: [
Positioned.fill(
child: Image.network(
'https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x300&markers=color:red%7C$lat,$lng&key=',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
color: const Color(0xFFE8EAF6),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.map_outlined, size: 48, color: theme.colorScheme.primary),
const SizedBox(height: 8),
Text(
'Tap to view on Google Maps',
style: TextStyle(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
],
),
),
)
else
GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(lat, lng),
@@ -691,7 +750,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
),
),
// Map type toggle bottom left
// Map type toggle bottom left (native only)
if (!kIsWeb)
Positioned(
bottom: 12,
left: 12,
@@ -709,7 +769,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
),
),
// Map controls toggle bottom right
// Map controls toggle bottom right (native only)
if (!kIsWeb)
Positioned(
bottom: 12,
right: 12,
@@ -719,8 +780,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
),
),
// Directional pad overlay
if (_showMapControls)
// Directional pad overlay (native only)
if (!kIsWeb && _showMapControls)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
@@ -730,7 +791,6 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Top row: Up + Zoom In
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -744,7 +804,6 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
],
),
const SizedBox(height: 10),
// Middle row: Left + Right
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -758,7 +817,6 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
],
),
const SizedBox(height: 10),
// Bottom row: Down + Zoom Out + Close
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [

View File

@@ -1,9 +1,11 @@
// lib/screens/login_screen.dart
import 'dart:ui';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import '../features/auth/services/auth_service.dart';
import 'home_screen.dart';
import '../core/app_decoration.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({Key? key}) : super(key: key);
@@ -22,9 +24,42 @@ class _LoginScreenState extends State<LoginScreen> {
final AuthService _auth = AuthService();
bool _loading = false;
bool _obscurePassword = true;
bool _rememberMe = false;
late VideoPlayerController _videoController;
bool _videoInitialized = false;
// Glassmorphism color palette
static const _darkBg = Color(0xFF0A0A0A);
static const _glassBg = Color(0x1AFFFFFF); // 10% white
static const _glassBorder = Color(0x33FFFFFF); // 20% white
static const _inputBg = Color(0x14FFFFFF); // 8% white
static const _inputBorder = Color(0x26FFFFFF); // 15% white
static const _textWhite = Colors.white;
static const _textMuted = Color(0xFFAAAAAA);
static const _textHint = Color(0xFF888888);
@override
void initState() {
super.initState();
_initVideo();
}
Future<void> _initVideo() async {
_videoController = VideoPlayerController.networkUrl(
Uri.parse('assets/login-bg.mp4'),
);
await _videoController.initialize();
_videoController.setLooping(true);
_videoController.setVolume(0);
_videoController.play();
if (mounted) setState(() => _videoInitialized = true);
}
@override
void dispose() {
_videoController.dispose();
_emailCtrl.dispose();
_passCtrl.dispose();
_emailFocus.dispose();
@@ -35,7 +70,6 @@ class _LoginScreenState extends State<LoginScreen> {
String? _emailValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter email';
final email = v.trim();
// Basic email pattern check
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
if (!emailRegex.hasMatch(email)) return 'Enter a valid email';
return null;
@@ -58,11 +92,9 @@ class _LoginScreenState extends State<LoginScreen> {
setState(() => _loading = true);
try {
// AuthService.login now returns a UserModel and also persists profile info.
await _auth.login(email, password);
if (!mounted) return;
// small delay for UX
await Future.delayed(const Duration(milliseconds: 150));
Navigator.of(context).pushReplacement(PageRouteBuilder(
@@ -86,75 +118,195 @@ class _LoginScreenState extends State<LoginScreen> {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RegisterScreen(isDesktop: false)));
}
@override
Widget build(BuildContext context) {
const primary = Color(0xFF0B63D6);
final width = MediaQuery.of(context).size.width;
void _showComingSoon() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Coming soon'), duration: Duration(seconds: 1)),
);
}
return Scaffold(
// backgroundColor: primary,
body: Container(
decoration: AppDecoration.blueGradient,
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: width < 720 ? width : 720),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
/// Glassmorphism pill-shaped input decoration
InputDecoration _glassInputDecoration({
required String hint,
required IconData prefixIcon,
Widget? suffixIcon,
}) {
const borderRadius = BorderRadius.all(Radius.circular(28));
return InputDecoration(
hintText: hint,
hintStyle: const TextStyle(color: _textHint, fontSize: 14),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Icon(prefixIcon, color: _textMuted, size: 20),
),
prefixIconConstraints: const BoxConstraints(minWidth: 44),
suffixIcon: suffixIcon != null
? Padding(
padding: const EdgeInsets.only(right: 8),
child: suffixIcon,
)
: null,
filled: true,
fillColor: _inputBg,
contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
border: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(color: _inputBorder),
),
enabledBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(color: _inputBorder),
),
focusedBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: BorderSide(color: _glassBorder, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: const BorderSide(color: Colors.redAccent),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: borderRadius,
borderSide: const BorderSide(color: Colors.redAccent, width: 1.5),
),
errorStyle: const TextStyle(color: Colors.redAccent, fontSize: 11),
);
}
/// Glassmorphism social button
Widget _socialButton({
required String label,
required Widget icon,
required VoidCallback onTap,
}) {
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: _inputBg,
borderRadius: BorderRadius.circular(28),
border: Border.all(color: _inputBorder),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 6),
const Text('Welcome', style: TextStyle(fontSize: 34, fontWeight: FontWeight.bold, color: Colors.white)),
const SizedBox(height: 6),
const Text('Sign in to continue', style: TextStyle(color: Colors.white70, fontSize: 16)),
const SizedBox(height: 26),
// HERO card
Hero(
tag: 'headerCard',
flightShuttleBuilder: (flightContext, animation, flightDirection, fromContext, toContext) {
return Material(
color: Colors.transparent,
child: ScaleTransition(
scale: animation.drive(Tween(begin: 0.98, end: 1.0).chain(CurveTween(curve: Curves.easeOut))),
child: fromContext.widget,
icon,
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
color: _textWhite,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
},
child: Material(
type: MaterialType.transparency,
child: Container(
width: double.infinity,
decoration: AppDecoration.blueGradientRounded(20).copyWith(
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 18, offset: Offset(0, 8))],
),
padding: const EdgeInsets.fromLTRB(18, 18, 18, 18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: _darkBg,
body: Stack(
children: [
const SizedBox(height: 8),
const Text('Eventify', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)),
// Video background
if (_videoInitialized)
Positioned.fill(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _videoController.value.size.width,
height: _videoController.value.size.height,
child: VideoPlayer(_videoController),
),
),
),
// Dark gradient overlay for readability
Positioned.fill(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.50),
Colors.black.withOpacity(0.65),
Colors.black.withOpacity(0.70),
],
stops: const [0.0, 0.4, 1.0],
),
),
),
),
// Main content
SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Brand name
Center(
child: Text(
'Eventify',
style: TextStyle(
color: _textWhite.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.italic,
letterSpacing: 1.5,
),
),
),
const SizedBox(height: 12),
// white card inside the blue card — now uses Form
Form(
key: _formKey,
child: Container(
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
child: Column(
children: [
// Email
// Heading
const Center(
child: Text(
'Log In, Start Your\nJourney',
textAlign: TextAlign.center,
style: TextStyle(
color: _textWhite,
fontSize: 28,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
height: 1.2,
),
),
),
const SizedBox(height: 32),
// Email label
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text(
'Email',
style: TextStyle(color: _textMuted, fontSize: 13),
),
),
// Email input
TextFormField(
controller: _emailCtrl,
focusNode: _emailFocus,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
decoration: InputDecoration(
labelText: 'Email',
border: InputBorder.none,
prefixIcon: Icon(Icons.email, color: primary),
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Enter your email',
prefixIcon: Icons.mail_outline_rounded,
),
validator: _emailValidator,
textInputAction: TextInputAction.next,
@@ -162,60 +314,199 @@ class _LoginScreenState extends State<LoginScreen> {
FocusScope.of(context).requestFocus(_passFocus);
},
),
const Divider(),
// Password
const SizedBox(height: 18),
// Password label
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text(
'Password',
style: TextStyle(color: _textMuted, fontSize: 13),
),
),
// Password input
TextFormField(
controller: _passCtrl,
focusNode: _passFocus,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
border: InputBorder.none,
prefixIcon: Icon(Icons.lock, color: primary),
obscureText: _obscurePassword,
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Enter your password',
prefixIcon: Icons.lock_outline_rounded,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined,
color: _textMuted,
size: 20,
),
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
),
),
validator: _passwordValidator,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _performLogin(),
),
const SizedBox(height: 14),
// Remember me + Forgot Password row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Remember me
GestureDetector(
onTap: () => setState(() => _rememberMe = !_rememberMe),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
border: Border.all(color: _glassBorder),
color: _rememberMe ? Colors.white24 : Colors.transparent,
),
child: _rememberMe
? const Icon(Icons.check, size: 14, color: _textWhite)
: null,
),
const SizedBox(width: 8),
const Text(
'Remember me',
style: TextStyle(color: _textMuted, fontSize: 12),
),
],
),
),
// Forgot Password
GestureDetector(
onTap: _showComingSoon,
child: const Text(
'Forgot Password?',
style: TextStyle(color: _textMuted, fontSize: 12),
),
),
],
),
const SizedBox(height: 24),
const SizedBox(height: 18),
// Login button
// Login button — dark gradient pill
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _loading ? null : _performLogin,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: primary,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: const LinearGradient(
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
),
border: Border.all(color: const Color(0x33FFFFFF)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: _loading ? null : _performLogin,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: _loading
? SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: primary))
: const Text('Login', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: _textWhite,
),
)
: const Text(
'Login',
style: TextStyle(
color: _textWhite,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
),
),
),
const SizedBox(height: 24),
const SizedBox(height: 8),
],
),
),
),
),
const SizedBox(height: 18),
Center(
child: Column(
// "Or continue with" divider
Row(
children: [
const SizedBox(height: 8),
TextButton(
onPressed: _openRegister,
child: const Text("Don't have an account? Register", style: TextStyle(color: Colors.white)),
Expanded(child: Divider(color: Colors.white.withOpacity(0.12))),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
child: Text(
'Or continue with',
style: TextStyle(
color: _textMuted.withOpacity(0.7),
fontSize: 12,
),
),
),
Expanded(child: Divider(color: Colors.white.withOpacity(0.12))),
],
),
const SizedBox(height: 18),
// Social buttons — side by side
Row(
children: [
_socialButton(
label: 'Google',
icon: const Text(
'G',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF4285F4),
),
),
onTap: _showComingSoon,
),
const SizedBox(width: 12),
_socialButton(
label: 'Apple',
icon: const Icon(Icons.apple, color: _textWhite, size: 20),
onTap: _showComingSoon,
),
],
),
const SizedBox(height: 28),
// Don't have an account? Create an account
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Don't have an account? ",
style: TextStyle(color: _textMuted, fontSize: 13),
),
GestureDetector(
onTap: _openRegister,
child: const Text(
'Create an account',
style: TextStyle(
color: _textWhite,
fontSize: 13,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
decorationColor: _textWhite,
),
),
),
],
),
@@ -227,8 +518,10 @@ class _LoginScreenState extends State<LoginScreen> {
),
),
),
],
),
);
}
}
}
/// Register screen calls backend register endpoint via AuthService.register

View File

@@ -21,7 +21,7 @@ class ResponsiveLayout extends StatelessWidget {
Key? key,
required this.mobile,
required this.desktop,
this.mobileBreakpoint = 700, // tune this value if you prefer different breakpoint
this.mobileBreakpoint = 820, // consistent with MyApp.desktopBreakpoint
}) : assert(mobileBreakpoint > 0),
super(key: key);
@@ -35,12 +35,18 @@ class ResponsiveLayout extends StatelessWidget {
bool _chooseMobile(BuildContext context) {
final width = MediaQuery.of(context).size.width;
// On web, use width to determine mobile vs desktop so narrow browser
// windows (or mobile-sized preview) get the mobile UI.
if (kIsWeb) {
return width < mobileBreakpoint;
}
// If running on Android/iOS, allow width to determine mobile vs desktop.
if (_isMobilePlatform()) {
return width < mobileBreakpoint;
}
// On desktop platforms (Windows/macOS/Linux) and on web, always use desktop UI.
// On native desktop platforms (Windows/macOS/Linux) always use desktop UI.
return false;
}

View File

@@ -2,46 +2,92 @@
import 'dart:ui';
import 'package:flutter/material.dart';
// Location packages (add to pubspec.yaml)
// geolocator -> for permission & coordinates
// geocoding -> for reverse geocoding coordinates to a placemark
// Location packages
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';
/// Data model for a location suggestion (city + optional pincode).
class _LocationItem {
final String city;
final String? district;
final String? pincode;
const _LocationItem({required this.city, this.district, this.pincode});
String get displayTitle => district != null && district!.isNotEmpty ? '$city, $district' : city;
String get displaySubtitle => pincode ?? '';
/// What gets returned to the caller (city name + optional district for display in pill).
String get returnValue => displayTitle;
}
class SearchScreen extends StatefulWidget {
const SearchScreen({Key? key}) : super(key: key);
/// Returns a String to the caller via Navigator.pop(string).
/// Could be:
/// - a city name (e.g. "Bengaluru")
/// - 'Current Location' or a resolved locality like "Whitefield, Bengaluru"
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final TextEditingController _ctrl = TextEditingController();
final List<String> _popularCities = const [
'Delhi NCR',
'Mumbai',
'Kolkata',
'Bengaluru',
'Hyderabad',
'Chandigarh',
'Pune',
'Chennai',
'Ahmedabad',
'Jaipur',
/// Popular Kerala cities shown as chips.
static const List<String> _popularCities = [
'Thiruvananthapuram',
'Kochi',
'Kozhikode',
'Kollam',
'Thrissur',
'Kannur',
'Alappuzha',
'Palakkad',
'Malappuram',
'Kottayam',
];
List<String> _filtered = [];
bool _loadingLocation = false;
/// Searchable location database Kerala towns/cities with pincodes.
static const List<_LocationItem> _locationDb = [
_LocationItem(city: 'Thiruvananthapuram', district: 'Thiruvananthapuram', pincode: '695001'),
_LocationItem(city: 'Kazhakoottam', district: 'Thiruvananthapuram', pincode: '695582'),
_LocationItem(city: 'Neyyattinkara', district: 'Thiruvananthapuram', pincode: '695121'),
_LocationItem(city: 'Attingal', district: 'Thiruvananthapuram', pincode: '695101'),
_LocationItem(city: 'Kochi', district: 'Ernakulam', pincode: '682001'),
_LocationItem(city: 'Ernakulam', district: 'Ernakulam', pincode: '682011'),
_LocationItem(city: 'Aluva', district: 'Ernakulam', pincode: '683101'),
_LocationItem(city: 'Kakkanad', district: 'Ernakulam', pincode: '682030'),
_LocationItem(city: 'Fort Kochi', district: 'Ernakulam', pincode: '682001'),
_LocationItem(city: 'Kozhikode', district: 'Kozhikode', pincode: '673001'),
_LocationItem(city: 'Feroke', district: 'Kozhikode', pincode: '673631'),
_LocationItem(city: 'Kollam', district: 'Kollam', pincode: '691001'),
_LocationItem(city: 'Karunagappally', district: 'Kollam', pincode: '690518'),
_LocationItem(city: 'Thrissur', district: 'Thrissur', pincode: '680001'),
_LocationItem(city: 'Chavakkad', district: 'Thrissur', pincode: '680506'),
_LocationItem(city: 'Guruvayoor', district: 'Thrissur', pincode: '680101'),
_LocationItem(city: 'Irinjalakuda', district: 'Thrissur', pincode: '680121'),
_LocationItem(city: 'Kannur', district: 'Kannur', pincode: '670001'),
_LocationItem(city: 'Thalassery', district: 'Kannur', pincode: '670101'),
_LocationItem(city: 'Alappuzha', district: 'Alappuzha', pincode: '688001'),
_LocationItem(city: 'Cherthala', district: 'Alappuzha', pincode: '688524'),
_LocationItem(city: 'Palakkad', district: 'Palakkad', pincode: '678001'),
_LocationItem(city: 'Ottapalam', district: 'Palakkad', pincode: '679101'),
_LocationItem(city: 'Malappuram', district: 'Malappuram', pincode: '676505'),
_LocationItem(city: 'Manjeri', district: 'Malappuram', pincode: '676121'),
_LocationItem(city: 'Tirur', district: 'Malappuram', pincode: '676101'),
_LocationItem(city: 'Kottayam', district: 'Kottayam', pincode: '686001'),
_LocationItem(city: 'Pala', district: 'Kottayam', pincode: '686575'),
_LocationItem(city: 'Pathanamthitta', district: 'Pathanamthitta', pincode: '689645'),
_LocationItem(city: 'Idukki', district: 'Idukki', pincode: '685602'),
_LocationItem(city: 'Munnar', district: 'Idukki', pincode: '685612'),
_LocationItem(city: 'Wayanad', district: 'Wayanad', pincode: '673121'),
_LocationItem(city: 'Kalpetta', district: 'Wayanad', pincode: '673121'),
_LocationItem(city: 'Kasaragod', district: 'Kasaragod', pincode: '671121'),
_LocationItem(city: 'Whitefield', district: 'Bengaluru', pincode: '560066'),
_LocationItem(city: 'Bengaluru', district: 'Karnataka', pincode: '560001'),
];
@override
void initState() {
super.initState();
_filtered = List.from(_popularCities);
}
List<_LocationItem> _searchResults = [];
bool _showSearchResults = false;
bool _loadingLocation = false;
@override
void dispose() {
@@ -53,38 +99,42 @@ class _SearchScreenState extends State<SearchScreen> {
final ql = q.trim().toLowerCase();
setState(() {
if (ql.isEmpty) {
_filtered = List.from(_popularCities);
_showSearchResults = false;
_searchResults = [];
} else {
_filtered = _popularCities.where((c) => c.toLowerCase().contains(ql)).toList();
_showSearchResults = true;
_searchResults = _locationDb.where((loc) {
return loc.city.toLowerCase().contains(ql) ||
(loc.district?.toLowerCase().contains(ql) ?? false) ||
(loc.pincode?.contains(ql) ?? false);
}).toList();
}
});
}
void _selectAndClose(String city) {
Navigator.of(context).pop(city);
void _selectAndClose(String location) {
Navigator.of(context).pop(location);
}
Future<void> _useCurrentLocation() async {
setState(() => _loadingLocation = true);
try {
// Check / request permission
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) {
// Can't get permission — inform user and return a fallback label
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
Navigator.of(context).pop('Current Location');
}
return;
}
// Get current position
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
// Try reverse geocoding to get a readable place name
try {
final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude);
if (placemarks.isNotEmpty) {
@@ -93,21 +143,18 @@ class _SearchScreenState extends State<SearchScreen> {
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
if ((p.administrativeArea ?? '').isNotEmpty) parts.add(p.administrativeArea!);
final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location';
Navigator.of(context).pop(label);
if (mounted) Navigator.of(context).pop(label);
return;
}
} catch (_) {
// ignore reverse geocode failures and fallback to coordinates or simple label
}
} catch (_) {}
// fallback: return lat,lng string or simple label
Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}');
if (mounted) Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}');
} catch (e) {
// If any error, fallback to simple label
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e')));
Navigator.of(context).pop('Current Location');
}
} finally {
if (mounted) setState(() => _loadingLocation = false);
}
@@ -115,31 +162,22 @@ class _SearchScreenState extends State<SearchScreen> {
@override
Widget build(BuildContext context) {
// Full-screen transparent Scaffold so the BackdropFilter can blur underlying UI.
return Scaffold(
backgroundColor: Colors.transparent,
body: GestureDetector(
// Tap outside sheet to dismiss
onTap: () => Navigator.of(context).pop(),
behavior: HitTestBehavior.opaque,
child: Stack(
children: [
// BackdropFilter + dim overlay
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(color: Colors.black.withOpacity(0.16)),
),
// Align bottom: the sheet content
Align(
alignment: Alignment.bottomCenter,
child: _SearchBottomSheet(
controller: _ctrl,
filteredCities: _filtered,
onCityTap: (city) => _selectAndClose(city),
onQueryChanged: _onQueryChanged,
onUseCurrentLocation: _useCurrentLocation,
loadingLocation: _loadingLocation,
child: GestureDetector(
onTap: () {}, // prevent taps on sheet from closing
child: _buildSheet(context),
),
),
],
@@ -147,45 +185,9 @@ class _SearchScreenState extends State<SearchScreen> {
),
);
}
}
class _SearchBottomSheet extends StatelessWidget {
final TextEditingController controller;
final List<String> filteredCities;
final void Function(String) onCityTap;
final void Function(String) onQueryChanged;
final Future<void> Function() onUseCurrentLocation;
final bool loadingLocation;
const _SearchBottomSheet({
Key? key,
required this.controller,
required this.filteredCities,
required this.onCityTap,
required this.onQueryChanged,
required this.onUseCurrentLocation,
required this.loadingLocation,
}) : super(key: key);
Widget _cityChip(String name, BuildContext context, void Function() onTap) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(12)),
child: Text(name, style: const TextStyle(color: Colors.black87)),
),
);
}
@override
Widget build(BuildContext context) {
// The bottom sheet container
return Padding(
padding: const EdgeInsets.all(0),
child: Container(
// limit height so it looks like a sheet
Widget _buildSheet(BuildContext context) {
return Container(
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.78, minHeight: 240),
decoration: const BoxDecoration(
color: Colors.white,
@@ -199,109 +201,160 @@ class _SearchBottomSheet extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// center drag handle
Center(
child: Container(width: 48, height: 6, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(6))),
),
const SizedBox(height: 12),
// Header
// Header row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Set Your Location', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
// Close button (inside sheet)
const Text('Set Your Location', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF1A1A2E))),
InkWell(
onTap: () => Navigator.of(context).pop(),
borderRadius: BorderRadius.circular(12),
child: Container(width: 40, height: 40, decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.close, color: Colors.black54)),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(color: const Color(0xFF1A1A2E), borderRadius: BorderRadius.circular(10)),
child: const Icon(Icons.close, color: Colors.white, size: 20),
),
),
],
),
const SizedBox(height: 14),
const SizedBox(height: 16),
// Search field (now functional)
// Search field
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(horizontal: 14),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(14),
border: _ctrl.text.isNotEmpty ? Border.all(color: const Color(0xFF2563EB).withOpacity(0.5), width: 1.5) : null,
),
child: Row(
children: [
const Icon(Icons.search, color: Colors.black38),
Icon(Icons.search, color: Colors.grey[500]),
const SizedBox(width: 10),
Expanded(
child: TextField(
controller: controller,
decoration: const InputDecoration(hintText: 'Search city, area or locality', border: InputBorder.none),
controller: _ctrl,
decoration: const InputDecoration(
hintText: 'Search city, area or locality',
hintStyle: TextStyle(color: Color(0xFF9CA3AF)),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 14),
),
textInputAction: TextInputAction.search,
onChanged: onQueryChanged,
onChanged: _onQueryChanged,
onSubmitted: (v) {
final q = v.trim();
if (q.isEmpty) return;
// If there's an exact/first match in filteredCities, pick it; otherwise pass the raw query.
final match = filteredCities.isNotEmpty ? filteredCities.first : null;
Navigator.of(context).pop(match ?? q);
if (_searchResults.isNotEmpty) {
_selectAndClose(_searchResults.first.returnValue);
} else {
_selectAndClose(q);
}
},
),
),
if (controller.text.isNotEmpty)
if (_ctrl.text.isNotEmpty)
IconButton(
icon: const Icon(Icons.clear),
icon: const Icon(Icons.clear, size: 20),
onPressed: () {
controller.clear();
onQueryChanged('');
_ctrl.clear();
_onQueryChanged('');
},
),
],
),
),
const SizedBox(height: 14),
const SizedBox(height: 16),
// Use current location button
ElevatedButton(
onPressed: loadingLocation ? null : () => onUseCurrentLocation(),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
backgroundColor: const Color(0xFF0B63D6),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
// Use current location
Material(
color: const Color(0xFF2563EB),
borderRadius: BorderRadius.circular(14),
child: InkWell(
onTap: _loadingLocation ? null : () => _useCurrentLocation(),
borderRadius: BorderRadius.circular(14),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Row(
children: [
const Icon(Icons.my_location, color: Colors.white),
const Icon(Icons.my_location, color: Colors.white, size: 22),
const SizedBox(width: 12),
Expanded(child: Text(loadingLocation ? 'Detecting location...' : 'Use Current Location', style: const TextStyle(color: Colors.white))),
if (loadingLocation)
const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
Expanded(
child: Text(
_loadingLocation ? 'Detecting location...' : 'Use Current Location',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 15),
),
),
if (_loadingLocation)
const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
else
const Icon(Icons.chevron_right, color: Colors.white),
],
),
),
const SizedBox(height: 18),
),
),
const SizedBox(height: 20),
// Popular cities
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: [
for (final city in filteredCities.take(8)) _cityChip(city, context, () => onCityTap(city)),
// if filteredCities is empty show empty state
if (filteredCities.isEmpty)
// Search results or Popular Cities
if (_showSearchResults) ...[
if (_searchResults.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text('No suggestions', style: TextStyle(color: Colors.grey[600])),
padding: const EdgeInsets.symmetric(vertical: 24),
child: Center(child: Text('No results found', style: TextStyle(color: Colors.grey[500]))),
)
else
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _searchResults.length,
separatorBuilder: (_, __) => Divider(color: Colors.grey[200], height: 1),
itemBuilder: (ctx, idx) {
final loc = _searchResults[idx];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
leading: Icon(Icons.location_on_outlined, color: Colors.grey[400], size: 24),
title: Text(
loc.displayTitle,
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 15, color: Color(0xFF1A1A2E)),
),
subtitle: loc.pincode != null && loc.pincode!.isNotEmpty
? Text(loc.pincode!, style: TextStyle(color: Colors.grey[500], fontSize: 13))
: null,
onTap: () => _selectAndClose(loc.returnValue),
);
},
),
] else ...[
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xFF1A1A2E))),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
for (final city in _popularCities)
InkWell(
onTap: () => _selectAndClose(city),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(12)),
child: Text(
city.length > 16 ? '${city.substring(0, 14)}...' : city,
style: const TextStyle(color: Color(0xFF374151), fontWeight: FontWeight.w500, fontSize: 14),
),
),
),
],
),
],
const SizedBox(height: 8),
],
),
),
),
),
);
}
}

View File

@@ -11,6 +11,7 @@ import path_provider_foundation
import share_plus
import shared_preferences_foundation
import url_launcher_macos
import video_player_avfoundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
@@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
}

View File

@@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -492,26 +492,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -769,10 +769,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.10"
typed_data:
dependency: transitive
description:
@@ -893,6 +893,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
video_player:
dependency: "direct main"
description:
name: video_player
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: "9862c67c4661c98f30fe707bc1a4f97d6a0faa76784f485d282668e4651a7ac3"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
video_player_avfoundation:
dependency: transitive
description:
name: video_player_avfoundation
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
url: "https://pub.dev"
source: hosted
version: "2.9.4"
video_player_platform_interface:
dependency: transitive
description:
name: video_player_platform_interface
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
url: "https://pub.dev"
source: hosted
version: "6.6.0"
video_player_web:
dependency: transitive
description:
name: video_player_web
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
vm_service:
dependency: transitive
description:
@@ -942,5 +982,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.35.0"
dart: ">=3.10.0 <4.0.0"
flutter: ">=3.38.0"

View File

@@ -19,6 +19,7 @@ dependencies:
google_maps_flutter: ^2.5.0
url_launcher: ^6.2.1
share_plus: ^7.2.1
video_player: ^2.8.1
dev_dependencies:
flutter_test:
@@ -31,7 +32,39 @@ flutter:
assets:
- assets/images/
- assets/icon/hand_stop.svg
fonts:
- family: Gilroy
fonts:
- asset: assets/fonts/Gilroy-Light.ttf
weight: 300
- asset: assets/fonts/Gilroy-LightItalic.ttf
weight: 300
style: italic
- asset: assets/fonts/Gilroy-Regular.ttf
weight: 400
- asset: assets/fonts/Gilroy-RegularItalic.ttf
weight: 400
style: italic
- asset: assets/fonts/Gilroy-Medium.ttf
weight: 500
- asset: assets/fonts/Gilroy-MediumItalic.ttf
weight: 500
style: italic
- asset: assets/fonts/Gilroy-SemiBold.ttf
weight: 600
- asset: assets/fonts/Gilroy-SemiBoldItalic.ttf
weight: 600
style: italic
- asset: assets/fonts/Gilroy-Bold.ttf
weight: 700
- asset: assets/fonts/Gilroy-BoldItalic.ttf
weight: 700
style: italic
- asset: assets/fonts/Gilroy-ExtraBold.ttf
weight: 800
- asset: assets/fonts/Gilroy-ExtraBoldItalic.ttf
weight: 800
style: italic
flutter_launcher_icons: