Update default location to Thrissur and remove Whitefield, Bengaluru
This commit is contained in:
@@ -150,8 +150,8 @@ class ApiClient {
|
|||||||
'end_date': '2026-04-16',
|
'end_date': '2026-04-16',
|
||||||
'start_time': '09:00',
|
'start_time': '09:00',
|
||||||
'end_time': '18:00',
|
'end_time': '18:00',
|
||||||
'pincode': '560001',
|
'pincode': '680001',
|
||||||
'place': 'Bengaluru International Exhibition Centre',
|
'place': 'Thekkinkadu Maidanam',
|
||||||
'is_bookable': true,
|
'is_bookable': true,
|
||||||
'event_type': 5,
|
'event_type': 5,
|
||||||
'thumb_img': 'https://picsum.photos/seed/event1/600/400',
|
'thumb_img': 'https://picsum.photos/seed/event1/600/400',
|
||||||
@@ -160,11 +160,11 @@ class ApiClient {
|
|||||||
{'is_primary': false, 'image': 'https://picsum.photos/seed/event1b/800/500'},
|
{'is_primary': false, 'image': 'https://picsum.photos/seed/event1b/800/500'},
|
||||||
],
|
],
|
||||||
'important_information': 'Please carry a valid photo ID for entry.',
|
'important_information': 'Please carry a valid photo ID for entry.',
|
||||||
'venue_name': 'BIEC Hall 2',
|
'venue_name': 'Maidanam Grounds',
|
||||||
'event_status': 'active',
|
'event_status': 'active',
|
||||||
'latitude': 13.0147,
|
'latitude': 10.5276,
|
||||||
'longitude': 77.5636,
|
'longitude': 76.2144,
|
||||||
'location_name': 'Bengaluru',
|
'location_name': 'Thrissur',
|
||||||
'important_info': [
|
'important_info': [
|
||||||
{'title': 'Entry', 'value': 'Free with registration'},
|
{'title': 'Entry', 'value': 'Free with registration'},
|
||||||
{'title': 'Parking', 'value': 'Available on-site'},
|
{'title': 'Parking', 'value': 'Available on-site'},
|
||||||
|
|||||||
@@ -58,5 +58,5 @@ class ApiEndpoints {
|
|||||||
// Notifications
|
// Notifications
|
||||||
static const String notificationList = "$baseUrl/notifications/list/";
|
static const String notificationList = "$baseUrl/notifications/list/";
|
||||||
static const String notificationMarkRead = "$baseUrl/notifications/mark-read/";
|
static const String notificationMarkRead = "$baseUrl/notifications/mark-read/";
|
||||||
static const String notificationCount = "$baseUrl/notifications/count/";
|
static const String notificationCount = "$baseUrl/notifications/count";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,30 +109,24 @@ class EventsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get events by GPS coordinates using haversine distance filtering.
|
/// Get events by GPS coordinates using haversine distance filtering.
|
||||||
/// Automatically expands radius (10 → 25 → 50 → 100 km) until ≥ 6 events found.
|
Future<List<EventModel>> getEventsByLocation(double lat, double lng, {double radiusKm = 10.0}) async {
|
||||||
Future<List<EventModel>> getEventsByLocation(double lat, double lng, {double initialRadiusKm = 10}) async {
|
final body = {
|
||||||
const radii = [10.0, 25.0, 50.0, 100.0];
|
'latitude': lat,
|
||||||
for (final radius in radii) {
|
'longitude': lng,
|
||||||
if (radius < initialRadiusKm) continue;
|
'radius_km': radiusKm,
|
||||||
final body = {
|
'page': 1,
|
||||||
'latitude': lat,
|
'page_size': 50,
|
||||||
'longitude': lng,
|
'per_type': 5,
|
||||||
'radius_km': radius,
|
};
|
||||||
'page': 1,
|
final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false);
|
||||||
'page_size': 50,
|
final list = <EventModel>[];
|
||||||
'per_type': 5,
|
final events = res['events'] ?? res['data'] ?? [];
|
||||||
};
|
if (events is List) {
|
||||||
final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false);
|
for (final e in events) {
|
||||||
final list = <EventModel>[];
|
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
|
||||||
final events = res['events'] ?? res['data'] ?? [];
|
|
||||||
if (events is List) {
|
|
||||||
for (final e in events) {
|
|
||||||
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (list.length >= 6 || radius >= 100) return list;
|
|
||||||
}
|
}
|
||||||
return [];
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
UserGamificationProfile? profile;
|
UserGamificationProfile? profile;
|
||||||
List<LeaderboardEntry> leaderboard = [];
|
List<LeaderboardEntry> leaderboard = [];
|
||||||
List<ShopItem> shopItems = [];
|
List<ShopItem> shopItems = [];
|
||||||
List<AchievementBadge> achievements = [];
|
List<AchievementBadge> achievements = GamificationService.defaultBadges; // Initialize with defaults
|
||||||
List<SubmissionModel> submissions = [];
|
List<SubmissionModel> submissions = [];
|
||||||
CurrentUserStats? currentUserStats;
|
CurrentUserStats? currentUserStats;
|
||||||
int totalParticipants = 0;
|
int totalParticipants = 0;
|
||||||
@@ -60,10 +60,16 @@ class GamificationProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
shopItems = results[2] as List<ShopItem>;
|
shopItems = results[2] as List<ShopItem>;
|
||||||
|
|
||||||
// Prefer achievements from dashboard API; fall back to getAchievements()
|
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
|
||||||
final dashAchievements = dashboard.achievements;
|
final dashAchievements = dashboard.achievements;
|
||||||
final fetchedAchievements = results[3] as List<AchievementBadge>;
|
final fetchedAchievements = results[3] as List<AchievementBadge>;
|
||||||
achievements = dashAchievements.isNotEmpty ? dashAchievements : fetchedAchievements;
|
|
||||||
|
if (dashAchievements.isNotEmpty) {
|
||||||
|
achievements = dashAchievements;
|
||||||
|
} else if (fetchedAchievements.isNotEmpty) {
|
||||||
|
achievements = fetchedAchievements;
|
||||||
|
}
|
||||||
|
// Otherwise, keep current defaults
|
||||||
|
|
||||||
_lastLoadTime = DateTime.now();
|
_lastLoadTime = DateTime.now();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class GamificationService {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Future<DashboardResponse> getDashboard() async {
|
Future<DashboardResponse> getDashboard() async {
|
||||||
final email = await _getUserEmail();
|
final email = await _getUserEmail();
|
||||||
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$email';
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=${Uri.encodeComponent(email)}';
|
||||||
final res = await _api.get(url, requiresAuth: false);
|
final res = await _api.get(url, requiresAuth: false);
|
||||||
|
|
||||||
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
||||||
@@ -50,7 +50,7 @@ class GamificationService {
|
|||||||
// GET /v1/gamification/dashboard?user_id={userId}
|
// GET /v1/gamification/dashboard?user_id={userId}
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
Future<DashboardResponse> getDashboardForUser(String userId) async {
|
Future<DashboardResponse> getDashboardForUser(String userId) async {
|
||||||
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$userId';
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=${Uri.encodeComponent(userId)}';
|
||||||
final res = await _api.get(url, requiresAuth: false);
|
final res = await _api.get(url, requiresAuth: false);
|
||||||
|
|
||||||
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
||||||
@@ -175,20 +175,17 @@ class GamificationService {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Fall through to defaults
|
// Fall through to defaults
|
||||||
}
|
}
|
||||||
return _defaultBadges;
|
return defaultBadges;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const _defaultBadges = [
|
static const defaultBadges = [
|
||||||
AchievementBadge(id: 'badge-01', title: 'First Step', description: 'Submit your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-01', title: 'Newcomer', description: 'First Event Posted', iconName: 'star', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Submit 5 events.', iconName: 'trending_up', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-02', title: 'Contributor', description: '10th Event Posted within a month', iconName: 'crown', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Submit 15 events.', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-03', title: 'On Fire!', description: '3 Day Streak of logging in', iconName: 'fire', isUnlocked: false, progress: 0.67),
|
||||||
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Reach top 10 on leaderboard.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-04', title: 'Verified', description: 'Identity Verified successfully', iconName: 'verified', isUnlocked: true, progress: 1.0),
|
||||||
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Upload 10 event images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-05', title: 'Quality', description: '5 Star Event Rating received', iconName: 'star', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first contributors.', iconName: 'rocket_launch', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-06', title: 'Community', description: 'Referred 5 Friends to the platform', iconName: 'community', isUnlocked: false, progress: 0.4),
|
||||||
AchievementBadge(id: 'badge-07', title: 'Community Star', description: 'Get 10 events approved.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-07', title: 'Expert', description: 'Level 10 Reached in 3 months', iconName: 'expert', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-08', title: 'Event Hunter', description: 'Submit 25 events.', iconName: 'event_hunter', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-08', title: 'Precision', description: '100% Data Accuracy on all events', iconName: 'precision', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-09', title: 'District Champion', description: 'Rank #1 in your district.', iconName: 'location_on', isUnlocked: false, progress: 0.0),
|
|
||||||
AchievementBadge(id: 'badge-10', title: 'Platinum Achiever', description: 'Earn 1500 EP.', iconName: 'diamond', isUnlocked: false, progress: 0.0),
|
|
||||||
AchievementBadge(id: 'badge-11', title: 'Diamond Legend', description: 'Earn 5000 EP.', iconName: 'workspace_premium', isUnlocked: false, progress: 0.0),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,26 +121,56 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
// Build
|
// Build
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
|
int _mainTab = 0; // 0: Contribute, 1: Leaderboard, 2: Achievements
|
||||||
|
@override
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<GamificationProvider>(
|
return Consumer<GamificationProvider>(
|
||||||
builder: (context, provider, _) {
|
builder: (context, provider, _) {
|
||||||
if (provider.isLoading && provider.profile == null) {
|
if (provider.isLoading && provider.profile == null) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
backgroundColor: _pageBg,
|
backgroundColor: _blue,
|
||||||
body: Center(child: BouncingLoader(color: _blue)),
|
body: Center(child: BouncingLoader(color: Colors.white)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: _pageBg,
|
backgroundColor: Colors.white, // Changed from _blue
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
bottom: false,
|
||||||
children: [
|
child: SingleChildScrollView(
|
||||||
_buildStatsBar(provider),
|
child: Column(
|
||||||
_buildTierRoadmap(provider),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
_buildTabBar(),
|
children: [
|
||||||
Expanded(child: _buildTabContent(provider)),
|
_buildHeader(provider),
|
||||||
],
|
Transform.translate(
|
||||||
|
offset: const Offset(0, -24),
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (_mainTab == 0) ...[
|
||||||
|
_buildStatsBar(provider),
|
||||||
|
_buildTierRoadmap(provider),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildTabBar(),
|
||||||
|
_buildTabContent(provider),
|
||||||
|
] else if (_mainTab == 1) ...[
|
||||||
|
_buildLeaderboardTab(provider),
|
||||||
|
] else if (_mainTab == 2) ...[
|
||||||
|
_buildAchievementsTab(provider),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -148,6 +178,534 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// LEADERBOARD TAB
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildLeaderboardTab(GamificationProvider provider) {
|
||||||
|
final leaderboard = provider.leaderboard;
|
||||||
|
final currentPeriod = provider.leaderboardTimePeriod;
|
||||||
|
final currentDistrict = provider.leaderboardDistrict;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Time Period Toggle
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
height: 48,
|
||||||
|
width: 300,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF1F5F9),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
AnimatedAlign(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
alignment: currentPeriod == 'all_time' ? Alignment.centerLeft : Alignment.centerRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: Container(
|
||||||
|
width: 144,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _blue,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => provider.setTimePeriod('all_time'),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'All Time',
|
||||||
|
style: TextStyle(
|
||||||
|
color: currentPeriod == 'all_time' ? Colors.white : _subText,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => provider.setTimePeriod('this_month'),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'This Month',
|
||||||
|
style: TextStyle(
|
||||||
|
color: currentPeriod == 'this_month' ? Colors.white : _subText,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// District Chips
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildDistrictChip(provider, 'Overall Kerala'),
|
||||||
|
..._districts.where((d) => d != 'Other').map((d) => _buildDistrictChip(provider, d)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Leaderboard List
|
||||||
|
if (provider.isLoading && leaderboard.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 60),
|
||||||
|
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
)
|
||||||
|
else if (leaderboard.isEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 60),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.emoji_events_outlined, size: 48, color: Colors.grey.shade300),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text('No rankings available for this area.', style: TextStyle(color: Colors.grey.shade400)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 20),
|
||||||
|
itemCount: leaderboard.length,
|
||||||
|
separatorBuilder: (_, __) => Divider(height: 1, color: _border.withValues(alpha: 0.5)),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final entry = leaderboard[index];
|
||||||
|
return _buildLeaderboardTile(entry);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 100), // Bottom padding
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDistrictChip(GamificationProvider provider, String district) {
|
||||||
|
final isSelected = provider.leaderboardDistrict == district;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => provider.setDistrict(district),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected ? _blue : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: isSelected ? _blue : _border),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
district,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected ? Colors.white : _darkText,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLeaderboardTile(LeaderboardEntry entry) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 32,
|
||||||
|
child: Text(
|
||||||
|
'${entry.rank}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: entry.rank <= 3 ? _blue : _subText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 20,
|
||||||
|
backgroundColor: _lightBlueBg,
|
||||||
|
backgroundImage: entry.avatarUrl != null ? NetworkImage(entry.avatarUrl!) : null,
|
||||||
|
child: entry.avatarUrl == null
|
||||||
|
? const Icon(Icons.person_outline, color: _blue, size: 20)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
entry.username,
|
||||||
|
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.normal, color: _darkText),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${entry.lifetimeEp} pts',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF10B981), // Emerald green
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ACHIEVEMENTS TAB
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildAchievementsTab(GamificationProvider provider) {
|
||||||
|
final achievements = provider.achievements;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (achievements.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 60),
|
||||||
|
child: Center(
|
||||||
|
child: Text('No achievements found.', style: TextStyle(color: _subText)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Column(
|
||||||
|
children: achievements.map((badge) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: _buildAchievementCard(badge),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 100),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAchievementCard(AchievementBadge badge) {
|
||||||
|
final bool isLocked = !badge.isUnlocked;
|
||||||
|
final Color iconColor = _getAchievementColor(badge.iconName);
|
||||||
|
final IconData iconData = _getAchievementIcon(badge.iconName);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: _border.withValues(alpha: 0.8)),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.02),
|
||||||
|
blurRadius: 12,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Large Icon Container
|
||||||
|
Container(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isLocked ? Colors.grey.shade100 : iconColor.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isLocked ? Icons.lock_outline : iconData,
|
||||||
|
color: isLocked ? Colors.grey.shade400 : iconColor,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Title with Lock Icon if needed
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
badge.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: isLocked ? Colors.grey : _darkText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isLocked) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(Icons.lock_outline, size: 18, color: Colors.grey.shade300),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Description
|
||||||
|
Text(
|
||||||
|
badge.description,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: isLocked ? Colors.grey.shade400 : _subText,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Progress Section
|
||||||
|
if (!badge.isUnlocked && badge.progress > 0) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 6,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF1F5F9),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: badge.progress,
|
||||||
|
child: Container(
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _blue,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: Text(
|
||||||
|
'${(badge.progress * 100).toInt()}%',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black26,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getAchievementColor(String iconName) {
|
||||||
|
switch (iconName.toLowerCase()) {
|
||||||
|
case 'star': return const Color(0xFF3B82F6); // Blue
|
||||||
|
case 'crown': return const Color(0xFFF59E0B); // Amber
|
||||||
|
case 'fire': return const Color(0xFFEF4444); // Red
|
||||||
|
case 'verified': return const Color(0xFF10B981); // Emerald
|
||||||
|
case 'community': return const Color(0xFF8B5CF6); // Purple
|
||||||
|
case 'expert': return const Color(0xFF6366F1); // Indigo
|
||||||
|
default: return _blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getAchievementIcon(String iconName) {
|
||||||
|
switch (iconName.toLowerCase()) {
|
||||||
|
case 'star': return Icons.star_rounded;
|
||||||
|
case 'crown': return Icons.emoji_events_rounded;
|
||||||
|
case 'fire': return Icons.local_fire_department_rounded;
|
||||||
|
case 'verified': return Icons.verified_rounded;
|
||||||
|
case 'community': return Icons.people_alt_rounded;
|
||||||
|
case 'expert': return Icons.workspace_premium_rounded;
|
||||||
|
case 'precision': return Icons.gps_fixed_rounded;
|
||||||
|
default: return Icons.stars_rounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// NEW BLUE HEADER DESIGN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildHeader(GamificationProvider provider) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
color: _blue,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), // Increased bottom padding
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Contributor Dashboard',
|
||||||
|
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
const Text(
|
||||||
|
'Track your impact, earn rewards, and climb\nthe ranks!',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 14, height: 1.4),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_buildMainTabGlider(),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildContributorLevelCard(provider),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMainTabGlider() {
|
||||||
|
const labels = ['Contribute', 'Leaderboard', 'Achievements'];
|
||||||
|
return Container(
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.15),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final tabWidth = constraints.maxWidth / 3;
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
AnimatedPositioned(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
left: tabWidth * _mainTab,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Container(
|
||||||
|
width: tabWidth,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: List.generate(3, (i) {
|
||||||
|
final active = _mainTab == i;
|
||||||
|
return Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => setState(() => _mainTab = i),
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (i == 0) ...[
|
||||||
|
Icon(Icons.edit_square, size: 16, color: active ? _blue : Colors.white),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
labels[i],
|
||||||
|
style: TextStyle(
|
||||||
|
color: active ? _blue : Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContributorLevelCard(GamificationProvider provider) {
|
||||||
|
final profile = provider.profile;
|
||||||
|
final tier = profile?.tier ?? ContributorTier.BRONZE;
|
||||||
|
final currentEp = profile?.lifetimeEp ?? 0;
|
||||||
|
int nextThreshold = _tierThresholds.last;
|
||||||
|
String nextTierLabel = 'Max';
|
||||||
|
for (int i = 0; i < ContributorTier.values.length; i++) {
|
||||||
|
if (currentEp < _tierThresholds[i]) {
|
||||||
|
nextThreshold = _tierThresholds[i];
|
||||||
|
nextTierLabel = tierLabel(ContributorTier.values[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double progress = (currentEp / nextThreshold).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.12),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text('Contributor Level', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.normal)),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
|
decoration: BoxDecoration(color: Colors.white.withOpacity(0.25), borderRadius: BorderRadius.circular(20)),
|
||||||
|
child: Text(tierLabel(tier), style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('Start earning rewards by\ncontributing!', style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14)),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('$currentEp pts', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.normal, fontSize: 13)),
|
||||||
|
Text('Next: $nextTierLabel ($nextThreshold pts)', style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Colors.white.withOpacity(0.2),
|
||||||
|
valueColor: const AlwaysStoppedAnimation(Colors.white),
|
||||||
|
minHeight: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 1. COMPACT STATS BAR
|
// 1. COMPACT STATS BAR
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -158,65 +716,65 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
final tierIcon = _tierIcons[tier] ?? Icons.shield_outlined;
|
final tierIcon = _tierIcons[tier] ?? Icons.shield_outlined;
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
padding: const EdgeInsets.fromLTRB(20, 24, 20, 8),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Tier pill
|
Row(
|
||||||
Container(
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||||
color: _blue,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(20),
|
color: _blue,
|
||||||
),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Icon(tierIcon, color: tierColor, size: 16),
|
children: [
|
||||||
const SizedBox(width: 6),
|
Icon(tierIcon, color: Colors.white, size: 14),
|
||||||
Text(
|
const SizedBox(width: 6),
|
||||||
tierLabel(tier),
|
Text(
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w700, fontSize: 13),
|
tierLabel(tier).toUpperCase(),
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.normal, fontSize: 13, letterSpacing: 0.5),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
|
|
||||||
// Liquid EP
|
|
||||||
Icon(Icons.bolt, color: _blue, size: 18),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${profile?.currentEp ?? 0}',
|
|
||||||
style: const TextStyle(color: _darkText, fontWeight: FontWeight.w700, fontSize: 15),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
const Text('EP', style: TextStyle(color: _subText, fontSize: 12)),
|
|
||||||
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
|
|
||||||
// RP
|
|
||||||
Icon(Icons.card_giftcard, color: _rpOrange, size: 18),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${profile?.currentRp ?? 0}',
|
|
||||||
style: TextStyle(color: _rpOrange, fontWeight: FontWeight.w700, fontSize: 15),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
const Text('RP', style: TextStyle(color: _subText, fontSize: 12)),
|
|
||||||
|
|
||||||
const Spacer(),
|
|
||||||
|
|
||||||
// Share button
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _shareRank(provider),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFF1F5F9),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.share_outlined, color: _subText, size: 18),
|
const SizedBox(width: 16),
|
||||||
),
|
// Liquid EP
|
||||||
|
Icon(Icons.bolt_outlined, color: _blue, size: 18),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('${profile?.currentEp ?? 0}', style: const TextStyle(color: _darkText, fontWeight: FontWeight.normal, fontSize: 15)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Text('Liquid EP', style: TextStyle(color: _subText, fontSize: 12)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// RP
|
||||||
|
Icon(Icons.card_giftcard_outlined, color: _rpOrange, size: 18),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text('${profile?.currentRp ?? 0}', style: TextStyle(color: _rpOrange, fontWeight: FontWeight.normal, fontSize: 15)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Text('RP', style: TextStyle(color: _subText, fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Share Rank button
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _shareRank(provider),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: _border),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.ios_share_outlined, color: _blue, size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text('Share Rank', style: TextStyle(color: _blue, fontWeight: FontWeight.normal, fontSize: 13)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -337,7 +895,7 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
left: tabWidth * _activeTab + 4,
|
left: tabWidth * _activeTab + 4,
|
||||||
top: 4,
|
top: 4,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: tabWidth - 8,
|
width: tabWidth > 8 ? tabWidth - 8 : 0,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _blue,
|
color: _blue,
|
||||||
@@ -363,16 +921,20 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
_tabIcons[i],
|
_tabIcons[i],
|
||||||
size: 18,
|
size: 16, // Slightly smaller icon
|
||||||
color: isActive ? Colors.white : const Color(0xFF64748B),
|
color: isActive ? Colors.white : const Color(0xFF64748B),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 4), // Tighter spacing
|
||||||
Text(
|
Flexible(
|
||||||
_tabLabels[i],
|
child: Text(
|
||||||
style: TextStyle(
|
_tabLabels[i],
|
||||||
fontSize: 13,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.w600,
|
maxLines: 1,
|
||||||
color: isActive ? Colors.white : const Color(0xFF64748B),
|
style: TextStyle(
|
||||||
|
fontSize: 11, // Smaller font for better fit
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isActive ? Colors.white : const Color(0xFF64748B),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -470,6 +1032,8 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
|
|
||||||
return ListView.separated(
|
return ListView.separated(
|
||||||
key: const ValueKey('list'),
|
key: const ValueKey('list'),
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: submissions.length,
|
itemCount: submissions.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
||||||
@@ -757,7 +1321,8 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
Widget _dropdown(String value, List<String> items, ValueChanged<String?> onChanged) {
|
Widget _dropdown(String value, List<String> items, ValueChanged<String?> onChanged) {
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
value: value,
|
value: value,
|
||||||
items: items.map((e) => DropdownMenuItem(value: e, child: Text(e, style: const TextStyle(fontSize: 14)))).toList(),
|
isExpanded: true,
|
||||||
|
items: items.map((e) => DropdownMenuItem(value: e, child: Text(e, style: const TextStyle(fontSize: 14), overflow: TextOverflow.ellipsis))).toList(),
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? '';
|
_username = prefs.getString('display_name') ?? prefs.getString('username') ?? '';
|
||||||
final storedLocation = prefs.getString('location') ?? 'Whitefield, Bengaluru';
|
final storedLocation = prefs.getString('location') ?? 'Thrissur';
|
||||||
// Fix legacy lat,lng strings saved before the reverse-geocoding fix
|
// Fix legacy lat,lng strings saved before the reverse-geocoding fix
|
||||||
final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation);
|
final coordMatch = RegExp(r'^(-?\d+\.?\d*),\s*(-?\d+\.?\d*)$').firstMatch(storedLocation);
|
||||||
if (coordMatch != null) {
|
if (coordMatch != null) {
|
||||||
@@ -467,7 +467,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
if (ev.id != null) {
|
if (ev.id != null) {
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev)));
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev, heroTag: 'event-hero-${ev.id}-search')));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -919,7 +919,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
if (ev.id != null) {
|
if (ev.id != null) {
|
||||||
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev)));
|
Navigator.of(context).push(MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: ev.id, initialEvent: ev, heroTag: 'event-hero-${ev.id}-sheet')));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -1197,129 +1197,181 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the image URL for a given event (for blurred bg).
|
||||||
|
String? _getEventImageUrl(EventModel event) {
|
||||||
|
if (event.thumbImg != null && event.thumbImg!.isNotEmpty) return event.thumbImg;
|
||||||
|
if (event.images.isNotEmpty && event.images.first.image.isNotEmpty) return event.images.first.image;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildHeroSection() {
|
Widget _buildHeroSection() {
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Column(
|
child: ValueListenableBuilder<int>(
|
||||||
mainAxisSize: MainAxisSize.min,
|
valueListenable: _heroPageNotifier,
|
||||||
children: [
|
builder: (context, currentPage, _) {
|
||||||
// Top bar: location pill + search button
|
final currentImg = _heroEvents.isNotEmpty ? _getEventImageUrl(_heroEvents[currentPage.clamp(0, _heroEvents.length - 1)]) : null;
|
||||||
Padding(
|
return Stack(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
children: [
|
||||||
child: Row(
|
// ── Blurred background image layer ──
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
if (currentImg != null && currentImg.isNotEmpty)
|
||||||
children: [
|
Positioned.fill(
|
||||||
GestureDetector(
|
child: ClipRect(
|
||||||
onTap: _openLocationSearch,
|
child: AnimatedSwitcher(
|
||||||
child: Container(
|
duration: const Duration(milliseconds: 500),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
child: CachedNetworkImage(
|
||||||
decoration: BoxDecoration(
|
key: ValueKey(currentImg),
|
||||||
color: Colors.white.withOpacity(0.15),
|
imageUrl: currentImg,
|
||||||
borderRadius: BorderRadius.circular(25),
|
memCacheWidth: 200,
|
||||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
memCacheHeight: 200,
|
||||||
),
|
fit: BoxFit.cover,
|
||||||
child: Row(
|
placeholder: (_, __) => const SizedBox.shrink(),
|
||||||
mainAxisSize: MainAxisSize.min,
|
errorWidget: (_, __, ___) => const SizedBox.shrink(),
|
||||||
children: [
|
imageBuilder: (context, imageProvider) => Stack(
|
||||||
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
|
fit: StackFit.expand,
|
||||||
const SizedBox(width: 6),
|
children: [
|
||||||
Text(
|
ImageFiltered(
|
||||||
_location.length > 20 ? '${_location.substring(0, 20)}...' : _location,
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
|
child: Image(
|
||||||
|
image: imageProvider,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
color: Colors.black.withOpacity(0.35),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
),
|
||||||
const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const NotificationBell(),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _openEventSearch,
|
|
||||||
child: Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.15),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.search, color: Colors.white, size: 24),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Featured carousel
|
// ── Foreground content ──
|
||||||
_heroEvents.isEmpty
|
Column(
|
||||||
? _loading
|
mainAxisSize: MainAxisSize.min,
|
||||||
? const Padding(
|
children: [
|
||||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
// Top bar: location pill + search button
|
||||||
child: SizedBox(
|
Padding(
|
||||||
height: 320,
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||||
child: _HeroShimmer(),
|
child: Row(
|
||||||
),
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
)
|
children: [
|
||||||
: const SizedBox(
|
GestureDetector(
|
||||||
height: 280,
|
onTap: _openLocationSearch,
|
||||||
child: Center(
|
child: Container(
|
||||||
child: Text('No events available',
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
style: TextStyle(color: Colors.white70)),
|
decoration: BoxDecoration(
|
||||||
),
|
color: Colors.white.withOpacity(0.15),
|
||||||
)
|
borderRadius: BorderRadius.circular(25),
|
||||||
: Column(
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||||
children: [
|
),
|
||||||
RepaintBoundary(
|
child: Row(
|
||||||
child: GestureDetector(
|
mainAxisSize: MainAxisSize.min,
|
||||||
behavior: HitTestBehavior.translucent,
|
children: [
|
||||||
onPanDown: (_) => _autoScrollTimer?.cancel(),
|
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
|
||||||
onPanEnd: (_) => _startAutoScroll(delay: const Duration(seconds: 3)),
|
const SizedBox(width: 6),
|
||||||
onPanCancel: () => _startAutoScroll(delay: const Duration(seconds: 3)),
|
Text(
|
||||||
child: SizedBox(
|
_location.length > 20 ? '${_location.substring(0, 20)}...' : _location,
|
||||||
height: 320,
|
style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
child: PageView.builder(
|
),
|
||||||
controller: _heroPageController,
|
const SizedBox(width: 4),
|
||||||
onPageChanged: (page) {
|
const Icon(Icons.keyboard_arrow_down, color: Colors.white, size: 18),
|
||||||
_heroPageNotifier.value = page;
|
],
|
||||||
// 8s delay after manual swipe for full read time
|
),
|
||||||
_startAutoScroll(delay: const Duration(seconds: 8));
|
|
||||||
},
|
|
||||||
itemCount: _heroEvents.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
// Scale animation: active card = 1.0, adjacent = 0.94
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _heroPageController,
|
|
||||||
builder: (context, child) {
|
|
||||||
double scale = index == _heroPageNotifier.value ? 1.0 : 0.94;
|
|
||||||
if (_heroPageController.position.haveDimensions) {
|
|
||||||
scale = (1.0 -
|
|
||||||
(_heroPageController.page! - index).abs() * 0.06)
|
|
||||||
.clamp(0.94, 1.0);
|
|
||||||
}
|
|
||||||
return Transform.scale(scale: scale, child: child);
|
|
||||||
},
|
|
||||||
child: _buildHeroEventImage(_heroEvents[index]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const NotificationBell(),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _openEventSearch,
|
||||||
|
child: Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.15),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.search, color: Colors.white, size: 24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
// Pagination dots
|
const SizedBox(height: 24),
|
||||||
_buildCarouselDots(),
|
|
||||||
],
|
// Featured carousel
|
||||||
),
|
_heroEvents.isEmpty
|
||||||
const SizedBox(height: 24),
|
? _loading
|
||||||
],
|
? const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 320,
|
||||||
|
child: _HeroShimmer(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
height: 280,
|
||||||
|
child: Center(
|
||||||
|
child: Text('No events available',
|
||||||
|
style: TextStyle(color: Colors.white70)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
RepaintBoundary(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onPanDown: (_) => _autoScrollTimer?.cancel(),
|
||||||
|
onPanEnd: (_) => _startAutoScroll(delay: const Duration(seconds: 3)),
|
||||||
|
onPanCancel: () => _startAutoScroll(delay: const Duration(seconds: 3)),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 320,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _heroPageController,
|
||||||
|
onPageChanged: (page) {
|
||||||
|
_heroPageNotifier.value = page;
|
||||||
|
// 8s delay after manual swipe for full read time
|
||||||
|
_startAutoScroll(delay: const Duration(seconds: 8));
|
||||||
|
},
|
||||||
|
itemCount: _heroEvents.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
// Scale animation: active card = 1.0, adjacent = 0.94
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _heroPageController,
|
||||||
|
builder: (context, child) {
|
||||||
|
double scale = index == _heroPageNotifier.value ? 1.0 : 0.94;
|
||||||
|
if (_heroPageController.position.haveDimensions) {
|
||||||
|
scale = (1.0 -
|
||||||
|
(_heroPageController.page! - index).abs() * 0.06)
|
||||||
|
.clamp(0.94, 1.0);
|
||||||
|
}
|
||||||
|
return Transform.scale(scale: scale, child: child);
|
||||||
|
},
|
||||||
|
child: _buildHeroEventImage(_heroEvents[index]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Pagination dots
|
||||||
|
_buildCarouselDots(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1390,13 +1442,13 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
'source': 'hero_carousel',
|
'source': 'hero_carousel',
|
||||||
});
|
});
|
||||||
Navigator.push(context,
|
Navigator.push(context,
|
||||||
MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event)));
|
MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event, heroTag: 'event-hero-${event.id}-carousel')));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: 'event-hero-${event.id}',
|
tag: 'event-hero-${event.id}-carousel',
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(radius),
|
borderRadius: BorderRadius.circular(radius),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
@@ -1815,11 +1867,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (event.id != null) {
|
if (event.id != null) {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event)));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => LearnMoreScreen(eventId: event.id, initialEvent: event, heroTag: 'event-hero-${event.id}-top')));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: 'event-hero-${event.id}',
|
tag: 'event-hero-${event.id}-top',
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 150,
|
width: 150,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import '../core/analytics/posthog_service.dart';
|
|||||||
class LearnMoreScreen extends StatefulWidget {
|
class LearnMoreScreen extends StatefulWidget {
|
||||||
final int eventId;
|
final int eventId;
|
||||||
final EventModel? initialEvent;
|
final EventModel? initialEvent;
|
||||||
const LearnMoreScreen({Key? key, required this.eventId, this.initialEvent}) : super(key: key);
|
final String? heroTag;
|
||||||
|
const LearnMoreScreen({Key? key, required this.eventId, this.initialEvent, this.heroTag}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<LearnMoreScreen> createState() => _LearnMoreScreenState();
|
State<LearnMoreScreen> createState() => _LearnMoreScreenState();
|
||||||
@@ -301,7 +302,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
|
|||||||
final mediaQuery = MediaQuery.of(context);
|
final mediaQuery = MediaQuery.of(context);
|
||||||
final screenWidth = mediaQuery.size.width;
|
final screenWidth = mediaQuery.size.width;
|
||||||
final screenHeight = mediaQuery.size.height;
|
final screenHeight = mediaQuery.size.height;
|
||||||
final imageHeight = screenHeight * 0.45;
|
final imageHeight = screenHeight * 0.52;
|
||||||
final topPadding = mediaQuery.padding.top;
|
final topPadding = mediaQuery.padding.top;
|
||||||
|
|
||||||
// ── DESKTOP layout ──────────────────────────────────────────────────
|
// ── DESKTOP layout ──────────────────────────────────────────────────
|
||||||
@@ -891,12 +892,12 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
|
|||||||
// ---- Foreground image with rounded corners ----
|
// ---- Foreground image with rounded corners ----
|
||||||
if (images.isNotEmpty)
|
if (images.isNotEmpty)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: topPad + 56, // below the icon row
|
top: topPad + 70, // safely below the icon row
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
bottom: 16,
|
bottom: 40, // clear from the bottom card's -28 overlap
|
||||||
child: Hero(
|
child: Hero(
|
||||||
tag: 'event-hero-${widget.eventId}',
|
tag: widget.heroTag ?? 'event-hero-${widget.eventId}',
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
@@ -926,10 +927,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> with SingleTickerProv
|
|||||||
// ---- No-image placeholder ----
|
// ---- No-image placeholder ----
|
||||||
if (images.isEmpty)
|
if (images.isEmpty)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: topPad + 56,
|
top: topPad + 70,
|
||||||
left: 20,
|
left: 20,
|
||||||
right: 20,
|
right: 20,
|
||||||
bottom: 16,
|
bottom: 40,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.15),
|
color: Colors.white.withOpacity(0.15),
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
bool _rememberMe = false;
|
bool _rememberMe = false;
|
||||||
|
|
||||||
late VideoPlayerController _videoController;
|
VideoPlayerController? _videoController;
|
||||||
bool _videoInitialized = false;
|
bool _videoInitialized = false;
|
||||||
|
|
||||||
// Glassmorphism color palette
|
// Glassmorphism color palette
|
||||||
@@ -53,17 +53,21 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initVideo() async {
|
Future<void> _initVideo() async {
|
||||||
_videoController = VideoPlayerController.asset('assets/login-bg.mp4');
|
try {
|
||||||
await _videoController.initialize();
|
_videoController = VideoPlayerController.asset('assets/login-bg.mp4');
|
||||||
_videoController.setLooping(true);
|
await _videoController!.initialize();
|
||||||
_videoController.setVolume(0);
|
_videoController!.setLooping(true);
|
||||||
_videoController.play();
|
_videoController!.setVolume(0);
|
||||||
if (mounted) setState(() => _videoInitialized = true);
|
_videoController!.play();
|
||||||
|
if (mounted) setState(() => _videoInitialized = true);
|
||||||
|
} catch (_) {
|
||||||
|
// Video asset not available — skip background video
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_videoController.dispose();
|
_videoController?.dispose();
|
||||||
_emailCtrl.dispose();
|
_emailCtrl.dispose();
|
||||||
_passCtrl.dispose();
|
_passCtrl.dispose();
|
||||||
_emailFocus.dispose();
|
_emailFocus.dispose();
|
||||||
@@ -240,14 +244,14 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Video background
|
// Video background
|
||||||
if (_videoInitialized)
|
if (_videoInitialized && _videoController != null)
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: _videoController.value.size.width,
|
width: _videoController!.value.size.width,
|
||||||
height: _videoController.value.size.height,
|
height: _videoController!.value.size.height,
|
||||||
child: VideoPlayer(_videoController),
|
child: VideoPlayer(_videoController!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
List<_LocationItem> _searchResults = [];
|
List<_LocationItem> _searchResults = [];
|
||||||
bool _showSearchResults = false;
|
bool _showSearchResults = false;
|
||||||
bool _loadingLocation = false;
|
bool _loadingLocation = false;
|
||||||
|
bool _isSearching = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -124,14 +125,48 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
Navigator.of(context).pop(result);
|
Navigator.of(context).pop(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _selectAndClose(String location) {
|
Future<void> _selectAndClose(String location) async {
|
||||||
// Looks up pincode + coordinates from the database for the given city name.
|
// Looks up pincode + coordinates from the database for the given city name.
|
||||||
final match = _locationDb.cast<_LocationItem?>().firstWhere(
|
final match = _locationDb.cast<_LocationItem?>().firstWhere(
|
||||||
(loc) => loc!.city.toLowerCase() == location.toLowerCase() ||
|
(loc) => loc != null && (loc.city.toLowerCase() == location.toLowerCase() ||
|
||||||
loc.displayTitle.toLowerCase() == location.toLowerCase(),
|
loc.displayTitle.toLowerCase() == location.toLowerCase()),
|
||||||
orElse: () => null,
|
orElse: () => null,
|
||||||
);
|
);
|
||||||
_selectWithPincode(location, pincode: match?.pincode, lat: match?.lat, lng: match?.lng);
|
|
||||||
|
if (match != null) {
|
||||||
|
_selectWithPincode(location, pincode: match.pincode, lat: match.lat, lng: match.lng);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Geocode the location name
|
||||||
|
setState(() => _isSearching = true);
|
||||||
|
try {
|
||||||
|
final placemarksByAddress = await locationFromAddress(location);
|
||||||
|
if (placemarksByAddress.isNotEmpty) {
|
||||||
|
final loc = placemarksByAddress.first;
|
||||||
|
final placemarks = await placemarkFromCoordinates(loc.latitude, loc.longitude);
|
||||||
|
String label = location;
|
||||||
|
String? pincode;
|
||||||
|
if (placemarks.isNotEmpty) {
|
||||||
|
final p = placemarks.first;
|
||||||
|
final parts = <String>[];
|
||||||
|
if ((p.locality ?? '').isNotEmpty) parts.add(p.locality!);
|
||||||
|
if ((p.subAdministrativeArea ?? '').isNotEmpty && p.subAdministrativeArea != p.locality) parts.add(p.subAdministrativeArea!);
|
||||||
|
if (parts.isNotEmpty) label = parts.join(', ');
|
||||||
|
pincode = p.postalCode;
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
_selectWithPincode(label, pincode: pincode ?? 'all', lat: loc.latitude, lng: loc.longitude);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// Geocoding failed, proceed with just the text label
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isSearching = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectWithPincode(location);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _useCurrentLocation() async {
|
Future<void> _useCurrentLocation() async {
|
||||||
@@ -263,6 +298,7 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _ctrl,
|
controller: _ctrl,
|
||||||
|
enabled: !_isSearching,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
hintText: 'Search city, area or locality',
|
hintText: 'Search city, area or locality',
|
||||||
hintStyle: TextStyle(color: Color(0xFF9CA3AF)),
|
hintStyle: TextStyle(color: Color(0xFF9CA3AF)),
|
||||||
@@ -282,7 +318,12 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_ctrl.text.isNotEmpty)
|
if (_isSearching)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8.0),
|
||||||
|
child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||||
|
)
|
||||||
|
else if (_ctrl.text.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.clear, size: 20),
|
icon: const Icon(Icons.clear, size: 20),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|||||||
36
pubspec.lock
36
pubspec.lock
@@ -69,10 +69,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -588,26 +588,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.16.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -873,10 +873,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: sqflite_android
|
name: sqflite_android
|
||||||
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
|
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2+3"
|
version: "2.4.2+2"
|
||||||
sqflite_common:
|
sqflite_common:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -961,10 +961,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.6"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1089,10 +1089,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
sha256: "48a7bdaa38a3d50ec10c78627abdbfad863fdf6f0d6e08c7c3c040cfd80ae36f"
|
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.1"
|
version: "2.10.1"
|
||||||
video_player_android:
|
video_player_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1105,10 +1105,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: video_player_avfoundation
|
name: video_player_avfoundation
|
||||||
sha256: af0e5b8a7a4876fb37e7cc8cb2a011e82bb3ecfa45844ef672e32cb14a1f259e
|
sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.4"
|
version: "2.8.9"
|
||||||
video_player_platform_interface:
|
video_player_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1174,5 +1174,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.10.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.35.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user