Compare commits
3 Commits
d536d287cd
...
809912305a
| Author | SHA1 | Date | |
|---|---|---|---|
| 809912305a | |||
| e0f34398c2 | |||
| 4acf75902c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -45,3 +45,4 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
web/assets/login-bg.mp4
|
||||||
|
|||||||
BIN
assets/fonts/Gilroy-Bold.ttf
Normal file
BIN
assets/fonts/Gilroy-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-BoldItalic.ttf
Normal file
BIN
assets/fonts/Gilroy-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-ExtraBold.ttf
Normal file
BIN
assets/fonts/Gilroy-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-ExtraBoldItalic.ttf
Normal file
BIN
assets/fonts/Gilroy-ExtraBoldItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-Light.ttf
Normal file
BIN
assets/fonts/Gilroy-Light.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-LightItalic.ttf
Normal file
BIN
assets/fonts/Gilroy-LightItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-Medium.ttf
Normal file
BIN
assets/fonts/Gilroy-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-MediumItalic.ttf
Normal file
BIN
assets/fonts/Gilroy-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-Regular.ttf
Normal file
BIN
assets/fonts/Gilroy-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-RegularItalic.ttf
Normal file
BIN
assets/fonts/Gilroy-RegularItalic.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-SemiBold.ttf
Normal file
BIN
assets/fonts/Gilroy-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Gilroy-SemiBoldItalic.ttf
Normal file
BIN
assets/fonts/Gilroy-SemiBoldItalic.ttf
Normal file
Binary file not shown.
@@ -6,6 +6,8 @@ import '../storage/token_storage.dart';
|
|||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
static const Duration _timeout = Duration(seconds: 30);
|
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
|
/// POST request
|
||||||
///
|
///
|
||||||
@@ -34,6 +36,30 @@ class ApiClient {
|
|||||||
.timeout(_timeout);
|
.timeout(_timeout);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (kDebugMode) debugPrint('ApiClient.post network error: $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');
|
throw Exception('Network error: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ class ThemeManager {
|
|||||||
|
|
||||||
/// Call during app startup to load saved preference.
|
/// Call during app startup to load saved preference.
|
||||||
static Future<void> init() async {
|
static Future<void> init() async {
|
||||||
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final isDark = prefs.getBool(_prefKey) ?? false;
|
final isDark = prefs.getBool(_prefKey) ?? false;
|
||||||
themeMode.value = isDark ? ThemeMode.dark : ThemeMode.light;
|
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
|
/// Set theme and persist
|
||||||
|
|||||||
@@ -39,9 +39,12 @@ class MyApp extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static const String _fontFamily = 'Gilroy';
|
||||||
|
|
||||||
ThemeData _lightTheme() {
|
ThemeData _lightTheme() {
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
|
fontFamily: _fontFamily,
|
||||||
primarySwatch: primarySwatch,
|
primarySwatch: primarySwatch,
|
||||||
scaffoldBackgroundColor: const Color(0xFFF7F5FB),
|
scaffoldBackgroundColor: const Color(0xFFF7F5FB),
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
@@ -61,9 +64,9 @@ class MyApp extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ThemeData _darkTheme() {
|
ThemeData _darkTheme() {
|
||||||
// Basic dark theme based on your sample — tweak colors as desired
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
|
fontFamily: _fontFamily,
|
||||||
primarySwatch: primarySwatch,
|
primarySwatch: primarySwatch,
|
||||||
scaffoldBackgroundColor: const Color(0xFF0B1220),
|
scaffoldBackgroundColor: const Color(0xFF0B1220),
|
||||||
appBarTheme: const AppBarTheme(
|
appBarTheme: const AppBarTheme(
|
||||||
@@ -75,7 +78,7 @@ class MyApp extends StatelessWidget {
|
|||||||
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0B63D6)),
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF0B63D6)),
|
||||||
),
|
),
|
||||||
cardColor: const Color(0xFF0E1620),
|
cardColor: const Color(0xFF0E1620),
|
||||||
textTheme: ThemeData.dark().textTheme,
|
textTheme: ThemeData.dark().textTheme.apply(fontFamily: _fontFamily),
|
||||||
useMaterial3: false,
|
useMaterial3: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -115,9 +118,17 @@ class _StartupScreenState extends State<StartupScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadLoginState() async {
|
Future<void> _loadLoginState() async {
|
||||||
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final hasEmail = prefs.getString('email') != null && prefs.getString('email')!.isNotEmpty;
|
final hasEmail = prefs.getString('email') != null && prefs.getString('email')!.isNotEmpty;
|
||||||
setState(() => _loggedIn = hasEmail);
|
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
|
@override
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// lib/screens/calendar_screen.dart
|
// lib/screens/calendar_screen.dart
|
||||||
import 'dart:math' as math;
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../features/events/services/events_service.dart';
|
import '../features/events/services/events_service.dart';
|
||||||
@@ -25,8 +24,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
|
|
||||||
final Set<String> _markedDates = {};
|
final Set<String> _markedDates = {};
|
||||||
final Map<String, int> _dateCounts = {};
|
final Map<String, int> _dateCounts = {};
|
||||||
final Map<String, List<String>> _dateThumbnails = {};
|
|
||||||
|
|
||||||
List<EventModel> _eventsOfDay = [];
|
List<EventModel> _eventsOfDay = [];
|
||||||
|
|
||||||
// Scroll controller for the calendar grid
|
// Scroll controller for the calendar grid
|
||||||
@@ -67,7 +64,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
_loadingMonth = true;
|
_loadingMonth = true;
|
||||||
_markedDates.clear();
|
_markedDates.clear();
|
||||||
_dateCounts.clear();
|
_dateCounts.clear();
|
||||||
_dateThumbnails.clear();
|
|
||||||
_eventsOfDay = [];
|
_eventsOfDay = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,9 +91,6 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_markedDates.isNotEmpty) {
|
|
||||||
await _fetchThumbnailsForDates(_markedDates.toList());
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
} finally {
|
} 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 {
|
Future<void> _onSelectDate(String yyyyMMdd) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_loadingDay = true;
|
_loadingDay = true;
|
||||||
@@ -381,9 +346,9 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
itemCount: totalItems,
|
itemCount: totalItems,
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 7,
|
crossAxisCount: 7,
|
||||||
mainAxisSpacing: 8,
|
mainAxisSpacing: 4,
|
||||||
crossAxisSpacing: 8,
|
crossAxisSpacing: 4,
|
||||||
childAspectRatio: 1,
|
childAspectRatio: 0.78,
|
||||||
),
|
),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final cellDate = firstCellDate.add(Duration(days: index));
|
final cellDate = firstCellDate.add(Duration(days: index));
|
||||||
@@ -391,8 +356,9 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
final dayIndex = cellDate.day;
|
final dayIndex = cellDate.day;
|
||||||
final key = _ymKey(cellDate);
|
final key = _ymKey(cellDate);
|
||||||
final hasEvents = _markedDates.contains(key);
|
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 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
|
final dayTextColor = inCurrentMonth
|
||||||
? (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black87)
|
? (Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black87)
|
||||||
@@ -408,52 +374,57 @@ class _CalendarScreenState extends State<CalendarScreen> {
|
|||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// rounded date cell
|
// rounded date cell
|
||||||
Container(
|
Container(
|
||||||
width: 36,
|
width: 32,
|
||||||
height: 36,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected ? primaryColor.withOpacity(0.14) : Colors.transparent,
|
color: isSelected
|
||||||
|
? primaryColor
|
||||||
|
: isToday
|
||||||
|
? primaryColor.withOpacity(0.12)
|
||||||
|
: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$dayIndex',
|
'$dayIndex',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600,
|
fontWeight: (isSelected || isToday) ? FontWeight.w700 : FontWeight.w500,
|
||||||
color: isSelected ? primaryColor : dayTextColor,
|
color: isSelected
|
||||||
fontSize: 14,
|
? Colors.white
|
||||||
|
: isToday
|
||||||
|
? primaryColor
|
||||||
|
: dayTextColor,
|
||||||
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 3),
|
||||||
// small event indicators (thumbnail overlap or dot)
|
// event indicator dots
|
||||||
if (hasEvents && thumbnails.isNotEmpty)
|
if (hasEvents && inCurrentMonth)
|
||||||
SizedBox(
|
Row(
|
||||||
height: 14,
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: thumbnails.take(2).toList().asMap().entries.map((entry) {
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
final i = entry.key;
|
children: List.generate(
|
||||||
final url = entry.value;
|
eventCount.clamp(1, 3),
|
||||||
return Transform.translate(
|
(i) => Container(
|
||||||
offset: Offset(i * -6.0, 0),
|
width: 5,
|
||||||
child: Container(
|
height: 5,
|
||||||
margin: const EdgeInsets.only(left: 4),
|
margin: EdgeInsets.only(left: i > 0 ? 2 : 0),
|
||||||
width: 14,
|
decoration: BoxDecoration(
|
||||||
height: 14,
|
color: isSelected
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: Theme.of(context).cardColor, width: 1.0)),
|
? primaryColor
|
||||||
child: ClipOval(child: Image.network(url, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Container(color: Theme.of(context).dividerColor))),
|
: const Color(0xFFEF4444),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else if (hasEvents)
|
|
||||||
Container(width: 18, height: 6, decoration: BoxDecoration(color: primaryColor, borderRadius: BorderRadius.circular(6)))
|
|
||||||
else
|
else
|
||||||
const SizedBox.shrink(),
|
const SizedBox(height: 5),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -237,8 +237,8 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Wrap(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
alignment: WrapAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")),
|
TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")),
|
||||||
TextButton(onPressed: () {}, child: const Text('Contact support'))
|
TextButton(onPressed: () {}, child: const Text('Contact support'))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
// lib/screens/learn_more_screen.dart
|
// lib/screens/learn_more_screen.dart
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:share_plus/share_plus.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 screenHeight = MediaQuery.of(context).size.height;
|
||||||
final imageHeight = screenHeight * 0.50;
|
final imageHeight = screenHeight * 0.45;
|
||||||
final overlap = 30.0;
|
final topPadding = MediaQuery.of(context).padding.top;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// ── LAYER 1: Image carousel (background) ──
|
// ── Scrollable content (carousel + card scroll together) ──
|
||||||
_buildImageCarousel(theme, imageHeight),
|
|
||||||
|
|
||||||
// ── LAYER 2: Scrollable content with overlapping white card ──
|
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Transparent spacer — shows the image behind
|
// Image carousel (scrolls with content)
|
||||||
SizedBox(height: imageHeight - overlap),
|
_buildImageCarousel(theme, imageHeight),
|
||||||
|
|
||||||
// White card with rounded top corners overlapping image
|
// Content card with rounded top corners overlapping carousel
|
||||||
Container(
|
Transform.translate(
|
||||||
|
offset: const Offset(0, -28),
|
||||||
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.scaffoldBackgroundColor,
|
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(
|
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,
|
left: 16,
|
||||||
right: 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(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_squareIconButton(
|
_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({
|
Widget _squareIconButton({
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
@@ -501,12 +520,12 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 42,
|
width: 44,
|
||||||
height: 42,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.2),
|
color: Colors.black.withOpacity(0.35),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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),
|
child: Icon(icon, color: iconColor, size: 22),
|
||||||
),
|
),
|
||||||
@@ -640,6 +659,46 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
height: 280,
|
height: 280,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
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(
|
GoogleMap(
|
||||||
initialCameraPosition: CameraPosition(
|
initialCameraPosition: CameraPosition(
|
||||||
target: LatLng(lat, lng),
|
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(
|
Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
left: 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(
|
Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
@@ -719,8 +780,8 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Directional pad overlay
|
// Directional pad overlay (native only)
|
||||||
if (_showMapControls)
|
if (!kIsWeb && _showMapControls)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -730,7 +791,6 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Top row: Up + Zoom In
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -744,7 +804,6 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
// Middle row: Left + Right
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
@@ -758,7 +817,6 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
// Bottom row: Down + Zoom Out + Close
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// lib/screens/login_screen.dart
|
// lib/screens/login_screen.dart
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
import '../features/auth/services/auth_service.dart';
|
import '../features/auth/services/auth_service.dart';
|
||||||
import 'home_screen.dart';
|
import 'home_screen.dart';
|
||||||
import '../core/app_decoration.dart';
|
|
||||||
|
|
||||||
class LoginScreen extends StatefulWidget {
|
class LoginScreen extends StatefulWidget {
|
||||||
const LoginScreen({Key? key}) : super(key: key);
|
const LoginScreen({Key? key}) : super(key: key);
|
||||||
@@ -22,9 +24,42 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
|
|
||||||
final AuthService _auth = AuthService();
|
final AuthService _auth = AuthService();
|
||||||
bool _loading = false;
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_videoController.dispose();
|
||||||
_emailCtrl.dispose();
|
_emailCtrl.dispose();
|
||||||
_passCtrl.dispose();
|
_passCtrl.dispose();
|
||||||
_emailFocus.dispose();
|
_emailFocus.dispose();
|
||||||
@@ -35,7 +70,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
String? _emailValidator(String? v) {
|
String? _emailValidator(String? v) {
|
||||||
if (v == null || v.trim().isEmpty) return 'Enter email';
|
if (v == null || v.trim().isEmpty) return 'Enter email';
|
||||||
final email = v.trim();
|
final email = v.trim();
|
||||||
// Basic email pattern check
|
|
||||||
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
|
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
|
||||||
if (!emailRegex.hasMatch(email)) return 'Enter a valid email';
|
if (!emailRegex.hasMatch(email)) return 'Enter a valid email';
|
||||||
return null;
|
return null;
|
||||||
@@ -58,11 +92,9 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// AuthService.login now returns a UserModel and also persists profile info.
|
|
||||||
await _auth.login(email, password);
|
await _auth.login(email, password);
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
// small delay for UX
|
|
||||||
await Future.delayed(const Duration(milliseconds: 150));
|
await Future.delayed(const Duration(milliseconds: 150));
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(PageRouteBuilder(
|
Navigator.of(context).pushReplacement(PageRouteBuilder(
|
||||||
@@ -86,75 +118,195 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RegisterScreen(isDesktop: false)));
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RegisterScreen(isDesktop: false)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _showComingSoon() {
|
||||||
Widget build(BuildContext context) {
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const primary = Color(0xFF0B63D6);
|
const SnackBar(content: Text('Coming soon'), duration: Duration(seconds: 1)),
|
||||||
final width = MediaQuery.of(context).size.width;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
/// Glassmorphism pill-shaped input decoration
|
||||||
// backgroundColor: primary,
|
InputDecoration _glassInputDecoration({
|
||||||
body: Container(
|
required String hint,
|
||||||
decoration: AppDecoration.blueGradient,
|
required IconData prefixIcon,
|
||||||
child: SafeArea(
|
Widget? suffixIcon,
|
||||||
child: Center(
|
}) {
|
||||||
child: SingleChildScrollView(
|
const borderRadius = BorderRadius.all(Radius.circular(28));
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
return InputDecoration(
|
||||||
child: ConstrainedBox(
|
hintText: hint,
|
||||||
constraints: BoxConstraints(maxWidth: width < 720 ? width : 720),
|
hintStyle: const TextStyle(color: _textHint, fontSize: 14),
|
||||||
child: Column(
|
prefixIcon: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
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: [
|
children: [
|
||||||
const SizedBox(height: 6),
|
icon,
|
||||||
const Text('Welcome', style: TextStyle(fontSize: 34, fontWeight: FontWeight.bold, color: Colors.white)),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(height: 6),
|
Text(
|
||||||
const Text('Sign in to continue', style: TextStyle(color: Colors.white70, fontSize: 16)),
|
label,
|
||||||
const SizedBox(height: 26),
|
style: const TextStyle(
|
||||||
|
color: _textWhite,
|
||||||
// HERO card
|
fontSize: 13,
|
||||||
Hero(
|
fontWeight: FontWeight.w500,
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
@override
|
||||||
child: Container(
|
Widget build(BuildContext context) {
|
||||||
width: double.infinity,
|
return Scaffold(
|
||||||
decoration: AppDecoration.blueGradientRounded(20).copyWith(
|
backgroundColor: _darkBg,
|
||||||
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 18, offset: Offset(0, 8))],
|
body: Stack(
|
||||||
),
|
|
||||||
padding: const EdgeInsets.fromLTRB(18, 18, 18, 18),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
// Video background
|
||||||
const Text('Eventify', style: TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold)),
|
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),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// white card inside the blue card — now uses Form
|
// Heading
|
||||||
Form(
|
const Center(
|
||||||
key: _formKey,
|
child: Text(
|
||||||
child: Container(
|
'Log In, Start Your\nJourney',
|
||||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
textAlign: TextAlign.center,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
style: TextStyle(
|
||||||
child: Column(
|
color: _textWhite,
|
||||||
children: [
|
fontSize: 28,
|
||||||
// Email
|
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(
|
TextFormField(
|
||||||
controller: _emailCtrl,
|
controller: _emailCtrl,
|
||||||
focusNode: _emailFocus,
|
focusNode: _emailFocus,
|
||||||
keyboardType: TextInputType.emailAddress,
|
keyboardType: TextInputType.emailAddress,
|
||||||
autofillHints: const [AutofillHints.email],
|
autofillHints: const [AutofillHints.email],
|
||||||
decoration: InputDecoration(
|
style: const TextStyle(color: _textWhite, fontSize: 14),
|
||||||
labelText: 'Email',
|
cursorColor: Colors.white54,
|
||||||
border: InputBorder.none,
|
decoration: _glassInputDecoration(
|
||||||
prefixIcon: Icon(Icons.email, color: primary),
|
hint: 'Enter your email',
|
||||||
|
prefixIcon: Icons.mail_outline_rounded,
|
||||||
),
|
),
|
||||||
validator: _emailValidator,
|
validator: _emailValidator,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
@@ -162,60 +314,199 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
FocusScope.of(context).requestFocus(_passFocus);
|
FocusScope.of(context).requestFocus(_passFocus);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
const SizedBox(height: 18),
|
||||||
// Password
|
|
||||||
|
// Password label
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(left: 4, bottom: 8),
|
||||||
|
child: Text(
|
||||||
|
'Password',
|
||||||
|
style: TextStyle(color: _textMuted, fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Password input
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passCtrl,
|
controller: _passCtrl,
|
||||||
focusNode: _passFocus,
|
focusNode: _passFocus,
|
||||||
obscureText: true,
|
obscureText: _obscurePassword,
|
||||||
decoration: InputDecoration(
|
style: const TextStyle(color: _textWhite, fontSize: 14),
|
||||||
labelText: 'Password',
|
cursorColor: Colors.white54,
|
||||||
border: InputBorder.none,
|
decoration: _glassInputDecoration(
|
||||||
prefixIcon: Icon(Icons.lock, color: primary),
|
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,
|
validator: _passwordValidator,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
onFieldSubmitted: (_) => _performLogin(),
|
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 — dark gradient pill
|
||||||
|
|
||||||
// Login button
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: Container(
|
||||||
onPressed: _loading ? null : _performLogin,
|
decoration: BoxDecoration(
|
||||||
style: ElevatedButton.styleFrom(
|
borderRadius: BorderRadius.circular(28),
|
||||||
backgroundColor: Colors.white,
|
gradient: const LinearGradient(
|
||||||
foregroundColor: primary,
|
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
),
|
),
|
||||||
|
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
|
child: _loading
|
||||||
? SizedBox(height: 18, width: 18, child: CircularProgressIndicator(strokeWidth: 2, color: primary))
|
? const SizedBox(
|
||||||
: const Text('Login', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
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),
|
// "Or continue with" divider
|
||||||
],
|
Row(
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 18),
|
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 8),
|
Expanded(child: Divider(color: Colors.white.withOpacity(0.12))),
|
||||||
TextButton(
|
Padding(
|
||||||
onPressed: _openRegister,
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
child: const Text("Don't have an account? Register", style: TextStyle(color: Colors.white)),
|
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
|
/// Register screen calls backend register endpoint via AuthService.register
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ResponsiveLayout extends StatelessWidget {
|
|||||||
Key? key,
|
Key? key,
|
||||||
required this.mobile,
|
required this.mobile,
|
||||||
required this.desktop,
|
required this.desktop,
|
||||||
this.mobileBreakpoint = 700, // tune this value if you prefer different breakpoint
|
this.mobileBreakpoint = 820, // consistent with MyApp.desktopBreakpoint
|
||||||
}) : assert(mobileBreakpoint > 0),
|
}) : assert(mobileBreakpoint > 0),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
@@ -35,12 +35,18 @@ class ResponsiveLayout extends StatelessWidget {
|
|||||||
bool _chooseMobile(BuildContext context) {
|
bool _chooseMobile(BuildContext context) {
|
||||||
final width = MediaQuery.of(context).size.width;
|
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 running on Android/iOS, allow width to determine mobile vs desktop.
|
||||||
if (_isMobilePlatform()) {
|
if (_isMobilePlatform()) {
|
||||||
return width < mobileBreakpoint;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,46 +2,92 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
// Location packages (add to pubspec.yaml)
|
// Location packages
|
||||||
// geolocator -> for permission & coordinates
|
|
||||||
// geocoding -> for reverse geocoding coordinates to a placemark
|
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:geocoding/geocoding.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 {
|
class SearchScreen extends StatefulWidget {
|
||||||
const SearchScreen({Key? key}) : super(key: key);
|
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
|
@override
|
||||||
State<SearchScreen> createState() => _SearchScreenState();
|
State<SearchScreen> createState() => _SearchScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchScreenState extends State<SearchScreen> {
|
class _SearchScreenState extends State<SearchScreen> {
|
||||||
final TextEditingController _ctrl = TextEditingController();
|
final TextEditingController _ctrl = TextEditingController();
|
||||||
final List<String> _popularCities = const [
|
|
||||||
'Delhi NCR',
|
/// Popular Kerala cities shown as chips.
|
||||||
'Mumbai',
|
static const List<String> _popularCities = [
|
||||||
'Kolkata',
|
'Thiruvananthapuram',
|
||||||
'Bengaluru',
|
'Kochi',
|
||||||
'Hyderabad',
|
'Kozhikode',
|
||||||
'Chandigarh',
|
'Kollam',
|
||||||
'Pune',
|
'Thrissur',
|
||||||
'Chennai',
|
'Kannur',
|
||||||
'Ahmedabad',
|
'Alappuzha',
|
||||||
'Jaipur',
|
'Palakkad',
|
||||||
|
'Malappuram',
|
||||||
|
'Kottayam',
|
||||||
];
|
];
|
||||||
|
|
||||||
List<String> _filtered = [];
|
/// Searchable location database – Kerala towns/cities with pincodes.
|
||||||
bool _loadingLocation = false;
|
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
|
List<_LocationItem> _searchResults = [];
|
||||||
void initState() {
|
bool _showSearchResults = false;
|
||||||
super.initState();
|
bool _loadingLocation = false;
|
||||||
_filtered = List.from(_popularCities);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -53,38 +99,42 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
final ql = q.trim().toLowerCase();
|
final ql = q.trim().toLowerCase();
|
||||||
setState(() {
|
setState(() {
|
||||||
if (ql.isEmpty) {
|
if (ql.isEmpty) {
|
||||||
_filtered = List.from(_popularCities);
|
_showSearchResults = false;
|
||||||
|
_searchResults = [];
|
||||||
} else {
|
} 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) {
|
void _selectAndClose(String location) {
|
||||||
Navigator.of(context).pop(city);
|
Navigator.of(context).pop(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _useCurrentLocation() async {
|
Future<void> _useCurrentLocation() async {
|
||||||
setState(() => _loadingLocation = true);
|
setState(() => _loadingLocation = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check / request permission
|
|
||||||
LocationPermission permission = await Geolocator.checkPermission();
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) {
|
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')));
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
|
||||||
Navigator.of(context).pop('Current Location');
|
Navigator.of(context).pop('Current Location');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current position
|
|
||||||
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
|
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.best);
|
||||||
|
|
||||||
// Try reverse geocoding to get a readable place name
|
|
||||||
try {
|
try {
|
||||||
final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude);
|
final placemarks = await placemarkFromCoordinates(pos.latitude, pos.longitude);
|
||||||
if (placemarks.isNotEmpty) {
|
if (placemarks.isNotEmpty) {
|
||||||
@@ -93,21 +143,18 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
|
if ((p.subLocality ?? '').isNotEmpty) parts.add(p.subLocality!);
|
||||||
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
||||||
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
|
if ((p.subAdministrativeArea ?? '').isNotEmpty) parts.add(p.subAdministrativeArea!);
|
||||||
if ((p.administrativeArea ?? '').isNotEmpty) parts.add(p.administrativeArea!);
|
|
||||||
final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location';
|
final label = parts.isNotEmpty ? parts.join(', ') : 'Current Location';
|
||||||
Navigator.of(context).pop(label);
|
if (mounted) Navigator.of(context).pop(label);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {}
|
||||||
// ignore reverse geocode failures and fallback to coordinates or simple label
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback: return lat,lng string or simple label
|
if (mounted) Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}');
|
||||||
Navigator.of(context).pop('${pos.latitude.toStringAsFixed(5)},${pos.longitude.toStringAsFixed(5)}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If any error, fallback to simple label
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e')));
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Could not determine location: $e')));
|
||||||
Navigator.of(context).pop('Current Location');
|
Navigator.of(context).pop('Current Location');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) setState(() => _loadingLocation = false);
|
if (mounted) setState(() => _loadingLocation = false);
|
||||||
}
|
}
|
||||||
@@ -115,31 +162,22 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Full-screen transparent Scaffold so the BackdropFilter can blur underlying UI.
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
body: GestureDetector(
|
body: GestureDetector(
|
||||||
// Tap outside sheet to dismiss
|
|
||||||
onTap: () => Navigator.of(context).pop(),
|
onTap: () => Navigator.of(context).pop(),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// BackdropFilter + dim overlay
|
|
||||||
BackdropFilter(
|
BackdropFilter(
|
||||||
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
|
||||||
child: Container(color: Colors.black.withOpacity(0.16)),
|
child: Container(color: Colors.black.withOpacity(0.16)),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Align bottom: the sheet content
|
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: _SearchBottomSheet(
|
child: GestureDetector(
|
||||||
controller: _ctrl,
|
onTap: () {}, // prevent taps on sheet from closing
|
||||||
filteredCities: _filtered,
|
child: _buildSheet(context),
|
||||||
onCityTap: (city) => _selectAndClose(city),
|
|
||||||
onQueryChanged: _onQueryChanged,
|
|
||||||
onUseCurrentLocation: _useCurrentLocation,
|
|
||||||
loadingLocation: _loadingLocation,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -147,45 +185,9 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class _SearchBottomSheet extends StatelessWidget {
|
Widget _buildSheet(BuildContext context) {
|
||||||
final TextEditingController controller;
|
return Container(
|
||||||
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
|
|
||||||
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.78, minHeight: 240),
|
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.78, minHeight: 240),
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -199,109 +201,160 @@ class _SearchBottomSheet extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// center drag handle
|
// Header row
|
||||||
Center(
|
|
||||||
child: Container(width: 48, height: 6, decoration: BoxDecoration(color: Colors.grey[300], borderRadius: BorderRadius.circular(6))),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Header
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text('Set Your Location', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
const Text('Set Your Location', style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold, color: Color(0xFF1A1A2E))),
|
||||||
// Close button (inside sheet)
|
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () => Navigator.of(context).pop(),
|
onTap: () => Navigator.of(context).pop(),
|
||||||
borderRadius: BorderRadius.circular(12),
|
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(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||||
decoration: BoxDecoration(color: Colors.grey[100], borderRadius: BorderRadius.circular(12)),
|
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(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.search, color: Colors.black38),
|
Icon(Icons.search, color: Colors.grey[500]),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: _ctrl,
|
||||||
decoration: const InputDecoration(hintText: 'Search city, area or locality', border: InputBorder.none),
|
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,
|
textInputAction: TextInputAction.search,
|
||||||
onChanged: onQueryChanged,
|
onChanged: _onQueryChanged,
|
||||||
onSubmitted: (v) {
|
onSubmitted: (v) {
|
||||||
final q = v.trim();
|
final q = v.trim();
|
||||||
if (q.isEmpty) return;
|
if (q.isEmpty) return;
|
||||||
// If there's an exact/first match in filteredCities, pick it; otherwise pass the raw query.
|
if (_searchResults.isNotEmpty) {
|
||||||
final match = filteredCities.isNotEmpty ? filteredCities.first : null;
|
_selectAndClose(_searchResults.first.returnValue);
|
||||||
Navigator.of(context).pop(match ?? q);
|
} else {
|
||||||
|
_selectAndClose(q);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (controller.text.isNotEmpty)
|
if (_ctrl.text.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
controller.clear();
|
_ctrl.clear();
|
||||||
onQueryChanged('');
|
_onQueryChanged('');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Use current location button
|
// Use current location
|
||||||
ElevatedButton(
|
Material(
|
||||||
onPressed: loadingLocation ? null : () => onUseCurrentLocation(),
|
color: const Color(0xFF2563EB),
|
||||||
style: ElevatedButton.styleFrom(
|
borderRadius: BorderRadius.circular(14),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
child: InkWell(
|
||||||
backgroundColor: const Color(0xFF0B63D6),
|
onTap: _loadingLocation ? null : () => _useCurrentLocation(),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
borderRadius: BorderRadius.circular(14),
|
||||||
),
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.my_location, color: Colors.white),
|
const Icon(Icons.my_location, color: Colors.white, size: 22),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(child: Text(loadingLocation ? 'Detecting location...' : 'Use Current Location', style: const TextStyle(color: Colors.white))),
|
Expanded(
|
||||||
if (loadingLocation)
|
child: Text(
|
||||||
const SizedBox(width: 18, height: 18, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
_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
|
else
|
||||||
const Icon(Icons.chevron_right, color: Colors.white),
|
const Icon(Icons.chevron_right, color: Colors.white),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Popular cities
|
// Search results or Popular Cities
|
||||||
const Text('Popular Cities', style: TextStyle(fontWeight: FontWeight.bold)),
|
if (_showSearchResults) ...[
|
||||||
const SizedBox(height: 12),
|
if (_searchResults.isEmpty)
|
||||||
|
|
||||||
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)
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
child: Text('No suggestions', style: TextStyle(color: Colors.grey[600])),
|
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),
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import path_provider_foundation
|
|||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import video_player_avfoundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
@@ -19,4 +20,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
64
pubspec.lock
64
pubspec.lock
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -492,26 +492,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -769,10 +769,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
version: "0.7.10"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -893,6 +893,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
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:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -942,5 +982,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.10.0 <4.0.0"
|
||||||
flutter: ">=3.35.0"
|
flutter: ">=3.38.0"
|
||||||
|
|||||||
35
pubspec.yaml
35
pubspec.yaml
@@ -19,6 +19,7 @@ dependencies:
|
|||||||
google_maps_flutter: ^2.5.0
|
google_maps_flutter: ^2.5.0
|
||||||
url_launcher: ^6.2.1
|
url_launcher: ^6.2.1
|
||||||
share_plus: ^7.2.1
|
share_plus: ^7.2.1
|
||||||
|
video_player: ^2.8.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -31,7 +32,39 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/icon/hand_stop.svg
|
- 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:
|
flutter_launcher_icons:
|
||||||
|
|||||||
Reference in New Issue
Block a user