diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 97e9da7..498242b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + Bool { + GMSServices.provideAPIKey("YOUR_iOS_API_KEY_HERE") return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/lib/core/widgets/impact_row.dart b/lib/core/widgets/impact_row.dart index 637504b..287e79b 100644 --- a/lib/core/widgets/impact_row.dart +++ b/lib/core/widgets/impact_row.dart @@ -24,7 +24,7 @@ class ImpactRow extends StatelessWidget { LinearProgressIndicator( value: value, color: color, - backgroundColor: color.withOpacity(0.1), + backgroundColor: color.withValues(alpha: 0.1), minHeight: 12, borderRadius: BorderRadius.circular(10), ), diff --git a/lib/core/widgets/smart_result_modal.dart b/lib/core/widgets/smart_result_modal.dart index e1e323b..65b1d27 100644 --- a/lib/core/widgets/smart_result_modal.dart +++ b/lib/core/widgets/smart_result_modal.dart @@ -37,7 +37,7 @@ class SmartResultModal extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( - color: themeColor.withOpacity(0.1), + color: themeColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: themeColor, width: 2), ), diff --git a/lib/features/auth/data/auth_repository.dart b/lib/features/auth/data/auth_repository.dart index 362f824..7f144b0 100644 --- a/lib/features/auth/data/auth_repository.dart +++ b/lib/features/auth/data/auth_repository.dart @@ -22,4 +22,19 @@ class AuthRepository { Future logout() async { await auth.signOut(); } + Future changePassword({required String currentPassword, required String newPassword}) async { + final user = FirebaseAuth.instance.currentUser; + if (user == null || user.email == null) throw "User not found"; + + // 1. Re-authenticate the user (Required by Firebase for security) + AuthCredential credential = EmailAuthProvider.credential( + email: user.email!, + password: currentPassword, + ); + + await user.reauthenticateWithCredential(credential); + + // 2. Update to new password + await user.updatePassword(newPassword); +} } diff --git a/lib/features/auth/ui/register_screen.dart b/lib/features/auth/ui/register_screen.dart index 32251a6..7676c82 100644 --- a/lib/features/auth/ui/register_screen.dart +++ b/lib/features/auth/ui/register_screen.dart @@ -90,7 +90,7 @@ class _RegisterScreenState extends ConsumerState { color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: Padding( @@ -213,8 +213,8 @@ class _RegisterScreenState extends ConsumerState { ), child: Stack( children: [ - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), const Positioned( top: 80, left: 30, @@ -240,7 +240,7 @@ class _RegisterScreenState extends ConsumerState { labelText: label, prefixIcon: Icon(icon, color: Colors.green[700]), filled: true, - fillColor: Colors.green.withOpacity(0.05), + fillColor: Colors.green.withValues(alpha: 0.05), border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: const BorderSide(color: Colors.green, width: 2)), diff --git a/lib/features/auth/ui/welcome_screen.dart b/lib/features/auth/ui/welcome_screen.dart index 1af3495..79fdef1 100644 --- a/lib/features/auth/ui/welcome_screen.dart +++ b/lib/features/auth/ui/welcome_screen.dart @@ -65,7 +65,7 @@ class WelcomeScreen extends StatelessWidget { color: Colors.white, boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.05), + color: Colors.green.withValues(alpha: 0.05), blurRadius: 20, offset: const Offset(0, -5), ), @@ -136,8 +136,8 @@ class WelcomeScreen extends StatelessWidget { child: Stack( clipBehavior: Clip.none, children: [ - Positioned(top: -50, right: -20, child: _circleDeco(150, Colors.white.withOpacity(0.08))), - Positioned(bottom: -30, left: -40, child: _circleDeco(120, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -20, child: _circleDeco(150, Colors.white.withValues(alpha: 0.08))), + Positioned(bottom: -30, left: -40, child: _circleDeco(120, Colors.white.withValues(alpha: 0.05))), Center( child: Column( @@ -149,7 +149,7 @@ class WelcomeScreen extends StatelessWidget { color: Colors.white, shape: BoxShape.circle, boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: const Icon(Icons.eco_rounded, size: 50, color: Color(0xFF1B5E20)), @@ -163,7 +163,7 @@ class WelcomeScreen extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(20), ), child: const Text( @@ -186,14 +186,14 @@ class WelcomeScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 15, offset: const Offset(0, 8)), + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 15, offset: const Offset(0, 8)), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: iconColor.withOpacity(0.1), shape: BoxShape.circle), + decoration: BoxDecoration(color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle), child: Icon(icon, color: iconColor, size: 28), ), const SizedBox(width: 16), diff --git a/lib/features/home/controllers/analytics_controller.dart b/lib/features/home/controllers/analytics_controller.dart index 83bb9a3..7e711ca 100644 --- a/lib/features/home/controllers/analytics_controller.dart +++ b/lib/features/home/controllers/analytics_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import '../models/user_model.dart'; // Use a simple enum for the filters enum AnalyticsFilter { week, month, allTime } @@ -30,9 +29,8 @@ class AnalyticsController extends AsyncNotifier> { // INSTANT: Fetch from Category Counter in User Document final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get(); final data = doc.data()?['categoryCounts'] as Map? ?? {}; - - // Convert Map to Map for the UI bars - return data.map((key, value) => MapEntry(key, (value as num).toDouble())); + final Map rawData = Map.from(data); + return Map.from(rawData.map((key, value) => MapEntry(key, (value as num).toDouble()))); } else { // DYNAMIC: Query Scans sub-collection for specific range DateTime now = DateTime.now(); @@ -49,7 +47,8 @@ class AnalyticsController extends AsyncNotifier> { Map counts = {}; for (var doc in query.docs) { - final cat = doc.data()['category'] ?? 'General'; + final data = doc.data(); + final String cat = data['category']?.toString() ?? 'General'; counts[cat] = (counts[cat] ?? 0.0) + 1.0; } return counts; diff --git a/lib/features/home/controllers/centers_controller.dart b/lib/features/home/controllers/centers_controller.dart new file mode 100644 index 0000000..1608606 --- /dev/null +++ b/lib/features/home/controllers/centers_controller.dart @@ -0,0 +1,42 @@ +import 'package:flutter_map/flutter_map.dart'; // 🌟 CHANGED: Replaced Google Maps +import 'package:latlong2/latlong.dart'; // 🌟 CHANGED: Using free LatLng +import 'package:geolocator/geolocator.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/recycleingcenter_model.dart'; +import '../repositories/centers_repository.dart'; // Make sure this path matches your repo + +final centersProvider = StreamNotifierProvider>(() { + return CentersController(); +}); + +class CentersController extends StreamNotifier> { + // 🌟 CHANGED: Initialized the OpenStreetMap controller + final MapController mapController = MapController(); + String selectedCategory = "All"; + + @override + Stream> build() { + return ref.read(centersRepositoryProvider).getCentersStream(); + } + + void setFilter(String category) { + selectedCategory = category; + ref.invalidateSelf(); // Refresh the stream and UI + } + + // 🌟 CHANGED: Using flutter_map's `.move()` syntax instead of animateCamera + void animateToCenter(double lat, double lng) { + mapController.move(LatLng(lat, lng), 15.0); + } + + Future getCurrentLocation() async { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) return Future.error('Location services are disabled.'); + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + return await Geolocator.getCurrentPosition(); + } +} \ No newline at end of file diff --git a/lib/features/home/controllers/history_controller.dart b/lib/features/home/controllers/history_controller.dart new file mode 100644 index 0000000..7d7fb0a --- /dev/null +++ b/lib/features/home/controllers/history_controller.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/scan_model.dart'; +import '../repositories/history_repository.dart'; + +final scanHistoryProvider = StreamNotifierProvider>(() { + return HistoryController(); +}); + +class HistoryController extends StreamNotifier> { + @override + Stream> build() { + return ref.read(historyRepositoryProvider).getScanHistoryStream(); + } + + // Logic for the Floating Card metric + int getTodayScanCount(List scans) { + final now = DateTime.now(); + return scans.where((scan) { + final date = scan.timestamp.toDate(); + return date.year == now.year && + date.month == now.month && + date.day == now.day; + }).length; + } +} \ No newline at end of file diff --git a/lib/features/home/controllers/user_controller.dart b/lib/features/home/controllers/user_controller.dart index 0f6e7a1..ad8c5fc 100644 --- a/lib/features/home/controllers/user_controller.dart +++ b/lib/features/home/controllers/user_controller.dart @@ -1,9 +1,36 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:firebase_auth/firebase_auth.dart'; import '../models/user_model.dart'; import '../repositories/user_repository.dart'; -final userControllerProvider = - StreamProvider.family((ref, uid) { +// 🟢 DATA PROVIDER: Keep this as StreamProvider so your UI doesn't break +final userControllerProvider = StreamProvider.family((ref, uid) { final repo = ref.watch(userRepositoryProvider); return repo.getUser(uid); }); + +// 🔵 ACTION PROVIDER: Use this for updating profile settings +final userProfileActionsProvider = Provider((ref) => UserProfileActions(ref)); + +class UserProfileActions { + final Ref ref; + UserProfileActions(this.ref); + + Future updateProfile({ + required String username, + required String email, + String? profileUrl, + }) async { + final uid = FirebaseAuth.instance.currentUser?.uid; + if (uid == null) return; + + final updates = { + 'username': username, + 'email': email, + // Use the null-aware spread as suggested by your diagnostic + ...?profileUrl != null ? {'profileurl': profileUrl} : null, + }; + + await ref.read(userRepositoryProvider).updateUserSettings(uid, updates); + } +} \ No newline at end of file diff --git a/lib/features/home/models/recycleingcender_model.dart b/lib/features/home/models/recycleingcenter_model.dart similarity index 100% rename from lib/features/home/models/recycleingcender_model.dart rename to lib/features/home/models/recycleingcenter_model.dart diff --git a/lib/features/home/models/user_model.dart b/lib/features/home/models/user_model.dart index e6712c6..e0f8937 100644 --- a/lib/features/home/models/user_model.dart +++ b/lib/features/home/models/user_model.dart @@ -11,6 +11,7 @@ class UserModel { final int streak; final Map categoryCounts; final double nextMilestoneCo2; + final String lastScanWeekId; UserModel({ required this.id, @@ -25,6 +26,7 @@ class UserModel { required this.streak, required this.categoryCounts, required this.nextMilestoneCo2, + required this.lastScanWeekId, }); factory UserModel.fromMap(String id, Map json) { @@ -40,7 +42,8 @@ class UserModel { rankTier: json['rankTier'] ?? 'Bronze', streak: json['streak'] ?? 0, categoryCounts: Map.from(json['categoryCounts'] ?? {}), - nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 20.0).toDouble(), + nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 50.0).toDouble(), + lastScanWeekId: json['lastScanWeekId'] ?? '', ); } @@ -57,6 +60,7 @@ class UserModel { 'streak': streak, 'categoryCounts': categoryCounts, 'nextMilestoneCo2': nextMilestoneCo2, + 'lastScanWeekId': lastScanWeekId, }; } } \ No newline at end of file diff --git a/lib/features/home/repositories/centers_repository.dart b/lib/features/home/repositories/centers_repository.dart new file mode 100644 index 0000000..bb79944 --- /dev/null +++ b/lib/features/home/repositories/centers_repository.dart @@ -0,0 +1,18 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/recycleingcenter_model.dart'; + +final centersRepositoryProvider = Provider((ref) => CentersRepository()); + +class CentersRepository { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + Stream> getCentersStream() { + return _firestore + .collection('recyclingCenters') + .snapshots() + .map((snapshot) => snapshot.docs + .map((doc) => RecyclingCenterModel.fromMap(doc.data())) + .toList()); + } +} \ No newline at end of file diff --git a/lib/features/home/repositories/history_repository.dart b/lib/features/home/repositories/history_repository.dart new file mode 100644 index 0000000..ea2b301 --- /dev/null +++ b/lib/features/home/repositories/history_repository.dart @@ -0,0 +1,27 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/scan_model.dart'; + +final historyRepositoryProvider = Provider((ref) => HistoryRepository()); + +class HistoryRepository { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + + Stream> getScanHistoryStream() { + final uid = _auth.currentUser?.uid; + if (uid == null) return Stream.value([]); + + // Streams the specific user's scan sub-collection in real-time + return _firestore + .collection('users') + .doc(uid) + .collection('scans') + .orderBy('timestamp', descending: true) // Newest first + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) => ScanModel.fromMap(doc.data())).toList(); + }); + } +} \ No newline at end of file diff --git a/lib/features/home/repositories/scan_repository.dart b/lib/features/home/repositories/scan_repository.dart index 48ac1a9..ec3481e 100644 --- a/lib/features/home/repositories/scan_repository.dart +++ b/lib/features/home/repositories/scan_repository.dart @@ -16,20 +16,58 @@ class ScanRepository { final userRef = _firestore.collection('users').doc(uid); final scanRef = userRef.collection('scans').doc(); - WriteBatch batch = _firestore.batch(); + try { + // Fetch the current user data to check the last recorded week + final userDoc = await userRef.get(); + + WriteBatch batch = _firestore.batch(); - // 1. Save scan details - batch.set(scanRef, scan.toMap()); + // 1. Save individual scan details + batch.set(scanRef, scan.toMap()); - // 2. Update user's aggregate stats and check week reset logic - // Inside ScanRepository.saveScan() - batch.update(userRef, { - 'categoryCounts.${scan.category}': FieldValue.increment(1), // Updates the specific category - 'totalScans': FieldValue.increment(1), - 'ecoPoints': FieldValue.increment(scan.pointsEarned), - 'co2Offset': FieldValue.increment(scan.co2Saved), - }); + // 2. Weekly Reset Logic + // We check if the 'lastScanWeekId' in Firestore matches the scan's current 'weekId' + bool isNewWeek = true; + if (userDoc.exists) { + final data = userDoc.data() as Map; + final String lastWeekId = data['lastScanWeekId'] ?? ""; + + // If the stored week ID matches the current scan's week ID, it's NOT a new week + if (lastWeekId == scan.weekId) { + isNewWeek = false; + } + } - await batch.commit(); + // 3. Update user's aggregate stats + if (isNewWeek) { + // RESET: Start weekly points fresh for the new week + batch.update(userRef, { + 'categoryCounts.${scan.category}': FieldValue.increment(1), + 'totalScans': FieldValue.increment(1), + 'ecoPoints': FieldValue.increment(scan.pointsEarned), + 'weeklyPoints': scan.pointsEarned, // Set to current scan points (Overwrite old week) + 'co2Offset': FieldValue.increment(scan.co2Saved), + 'lastScanWeekId': scan.weekId, // Update the tracking week ID + }); + } else { + // INCREMENT: Add to existing weekly points + batch.update(userRef, { + 'categoryCounts.${scan.category}': FieldValue.increment(1), + 'totalScans': FieldValue.increment(1), + 'ecoPoints': FieldValue.increment(scan.pointsEarned), + 'weeklyPoints': FieldValue.increment(scan.pointsEarned), // Correctly updating the scoreboard + 'co2Offset': FieldValue.increment(scan.co2Saved), + 'lastScanWeekId': scan.weekId, + }); + } + + // Commit all changes at once + await batch.commit(); + + } catch (e) { + // Log error for debugging + print("Error saving scan and updating scoreboard: $e"); + rethrow; + } } } \ No newline at end of file diff --git a/lib/features/home/repositories/scoreboard_repository.dart b/lib/features/home/repositories/scoreboard_repository.dart index 6a200ce..3cd1d56 100644 --- a/lib/features/home/repositories/scoreboard_repository.dart +++ b/lib/features/home/repositories/scoreboard_repository.dart @@ -10,11 +10,11 @@ class ScoreboardRepository { .collection('users') .orderBy('weeklyPoints', descending: true) .limit(limit) - .snapshots() // <-- This is the magic word for real-time updates! + .snapshots() .map((querySnapshot) { - return querySnapshot.docs.map((doc) { - return UserModel.fromMap(doc.id, doc.data()); - }).toList(); - }); + return querySnapshot.docs.map((doc) { + return UserModel.fromMap(doc.id, doc.data()); + }).toList(); + }); } } \ No newline at end of file diff --git a/lib/features/home/repositories/user_repository.dart b/lib/features/home/repositories/user_repository.dart index 6040dcd..ea88725 100644 --- a/lib/features/home/repositories/user_repository.dart +++ b/lib/features/home/repositories/user_repository.dart @@ -1,5 +1,5 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../services/firebase_service.dart'; import '../models/user_model.dart'; final userRepositoryProvider = Provider((ref) { @@ -7,10 +7,10 @@ final userRepositoryProvider = Provider((ref) { }); class UserRepository { - final FirebaseService _service = FirebaseService(); + final FirebaseFirestore _firestore = FirebaseFirestore.instance; Stream getUser(String uid) { - return _service.streamUser(uid).map((snapshot) { + return _firestore.collection('users').doc(uid).snapshots().map((snapshot) { final data = snapshot.data(); if (data != null) { return UserModel.fromMap(snapshot.id, data); @@ -19,4 +19,8 @@ class UserRepository { } }); } -} + + Future updateUserSettings(String uid, Map data) async { + await _firestore.collection('users').doc(uid).update(data); + } +} \ No newline at end of file diff --git a/lib/features/home/ui/analytics_screen.dart b/lib/features/home/ui/analytics_screen.dart index 107a798..eaa0e6f 100644 --- a/lib/features/home/ui/analytics_screen.dart +++ b/lib/features/home/ui/analytics_screen.dart @@ -98,8 +98,8 @@ class AnalyticsScreen extends ConsumerWidget { ), child: Stack( children: [ - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), Positioned( top: 60, left: 0, @@ -129,7 +129,7 @@ class AnalyticsScreen extends ConsumerWidget { color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ - BoxShadow(color: Colors.green.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.green.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: Row( @@ -187,26 +187,30 @@ class AnalyticsScreen extends ConsumerWidget { } Widget _buildChartList(Map data) { - if (data.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text("No data for this period.", style: TextStyle(color: Colors.grey)), - ); - } - - final total = data.values.fold(0.0, (sum, val) => sum + val); - - return Column( - children: data.entries.map((e) { - return _buildImpactBar( - e.key, - e.value / total, - _getCategoryColor(e.key), - _getCategoryIcon(e.key), - ); - }).toList(), + // DEFECT FIX: If there is no data or total is zero, show a friendly message + final total = data.values.fold(0.0, (sum, val) => sum + val); + + if (data.isEmpty || total == 0) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Center( + child: Text("No recycling data found for this period.", + style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w500)), + ), ); } + + return Column( + children: data.entries.map((e) { + return _buildImpactBar( + e.key, + e.value / total, // Now safe because total > 0 + _getCategoryColor(e.key), + _getCategoryIcon(e.key), + ); + }).toList(), + ); +} Widget _buildMilestoneCard(double current, double target) { double progress = (target > 0) ? (current / target).clamp(0.0, 1.0) : 0.0; diff --git a/lib/features/home/ui/camera_screen.dart b/lib/features/home/ui/camera_screen.dart index 933b7d4..bac842e 100644 --- a/lib/features/home/ui/camera_screen.dart +++ b/lib/features/home/ui/camera_screen.dart @@ -155,7 +155,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr // Focus Overlay ColorFiltered( colorFilter: ColorFilter.mode( - Colors.black.withOpacity(0.5), + Colors.black.withValues(alpha: 0.5), BlendMode.srcOut, ), child: Stack( @@ -199,9 +199,9 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr height: 4, decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.green.withOpacity(0), Colors.green, Colors.green.withOpacity(0)], + colors: [Colors.green.withValues(alpha: 0), Colors.green, Colors.green.withValues(alpha: 0)], ), - boxShadow: [BoxShadow(color: Colors.green.withOpacity(0.6), blurRadius: 10, spreadRadius: 2)], + boxShadow: [BoxShadow(color: Colors.green.withValues(alpha: 0.6), blurRadius: 10, spreadRadius: 2)], ), ), ), @@ -218,7 +218,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr width: 320, height: 320, decoration: BoxDecoration( - border: Border.all(color: Colors.white.withOpacity(0.3), width: 1), + border: Border.all(color: Colors.white.withValues(alpha: 0.3), width: 1), borderRadius: BorderRadius.circular(25), ), child: Column( diff --git a/lib/features/home/ui/centers_screen.dart b/lib/features/home/ui/centers_screen.dart new file mode 100644 index 0000000..2dfd709 --- /dev/null +++ b/lib/features/home/ui/centers_screen.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../controllers/centers_controller.dart'; +import '../models/recycleingcenter_model.dart'; + +class CentersScreen extends ConsumerWidget { + const CentersScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final centersAsync = ref.watch(centersProvider); + final controller = ref.read(centersProvider.notifier); + + return Scaffold( + backgroundColor: const Color(0xFFF4F9F5), + appBar: AppBar( + title: const Text("Recycling Centers", style: TextStyle(fontWeight: FontWeight.bold)), + backgroundColor: Colors.white, + elevation: 0, + foregroundColor: Colors.black, + ), + body: Column( + children: [ + // 1. MAP SECTION (Top) - Now 100% Free! + SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + child: FlutterMap( + mapController: controller.mapController, + options: const MapOptions( + initialCenter: LatLng(1.85, 103.08), // Centered on campus + initialZoom: 12.0, + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'com.example.ecoscan', + ), + centersAsync.maybeWhen( + data: (centers) => MarkerLayer( + markers: centers.map((c) => Marker( + point: LatLng(c.location!.latitude, c.location!.longitude), + width: 40, + height: 40, + child: const Icon(Icons.location_on, color: Colors.green, size: 40), + )).toList(), + ), + orElse: () => const MarkerLayer(markers: []), + ), + ], + ), + ), + + // 2. CHIP FILTERS + _buildFilterBar(ref, controller), + + // 3. LIST SECTION (Bottom) + Expanded( + child: centersAsync.when( + data: (centers) { + final filtered = controller.selectedCategory == "All" + ? centers + : centers.where((c) => c.acceptedCategories.contains(controller.selectedCategory)).toList(); + + return ListView.builder( + padding: const EdgeInsets.all(15), + itemCount: filtered.length, + itemBuilder: (context, index) => _buildCenterCard(filtered[index], controller), + ); + }, + loading: () => const Center(child: CircularProgressIndicator(color: Colors.green)), + error: (err, _) => Center(child: Text("Error: $err")), + ), + ), + ], + ), + ); + } + + Widget _buildFilterBar(WidgetRef ref, CentersController controller) { + final categories = ["All", "Plastic", "Paper", "Glass", "Metal"]; + final current = controller.selectedCategory; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + child: Row( + children: categories.map((cat) => Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Text(cat), + selected: current == cat, + onSelected: (_) => controller.setFilter(cat), + selectedColor: Colors.green, + labelStyle: TextStyle(color: current == cat ? Colors.white : Colors.black), + ), + )).toList(), + ), + ); + } + + Widget _buildCenterCard(RecyclingCenterModel center, CentersController controller) { + return Card( + margin: const EdgeInsets.only(bottom: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + child: ListTile( + contentPadding: const EdgeInsets.all(15), + leading: const CircleAvatar(backgroundColor: Color(0xFFE8F5E9), child: Icon(Icons.location_on, color: Colors.green)), + title: Text(center.name, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(center.address, maxLines: 1, overflow: TextOverflow.ellipsis), + const SizedBox(height: 5), + Text("Accepted: ${center.acceptedCategories.join(', ')}", style: const TextStyle(fontSize: 11, color: Colors.blueGrey)), + ], + ), + trailing: IconButton( + icon: const Icon(Icons.directions_rounded, color: Colors.blue), + onPressed: () => _openDirections(center.location!.latitude, center.location!.longitude), + ), + onTap: () => controller.animateToCenter(center.location!.latitude, center.location!.longitude), + ), + ); + } + + // 🌟 BONUS FIX: Now actually dynamically routes to the specific lat/long of the center! + Future _openDirections(double lat, double lng) async { + final Uri url = Uri.parse('https://www.google.com/maps/dir/?api=1&destination=$lat,$lng'); + + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + debugPrint("Could not launch routing for $url"); + } + } +} \ No newline at end of file diff --git a/lib/features/home/ui/change_password_screen.dart b/lib/features/home/ui/change_password_screen.dart index f006d75..efd342e 100644 --- a/lib/features/home/ui/change_password_screen.dart +++ b/lib/features/home/ui/change_password_screen.dart @@ -1,13 +1,196 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:firebase_auth/firebase_auth.dart'; -class ChangePasswordScreen extends StatelessWidget { +class ChangePasswordScreen extends ConsumerStatefulWidget { const ChangePasswordScreen({super.key}); + @override + ConsumerState createState() => _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _currentPasswordController = TextEditingController(); + final _newPasswordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + bool _isLoading = false; + bool _obscureText = true; + + @override + void dispose() { + _currentPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _handleChangePassword() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + try { + final user = FirebaseAuth.instance.currentUser; + AuthCredential credential = EmailAuthProvider.credential( + email: user!.email!, + password: _currentPasswordController.text.trim(), + ); + + // Re-authenticate and update + await user.reauthenticateWithCredential(credential); + await user.updatePassword(_newPasswordController.text.trim()); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Password updated successfully!"), backgroundColor: Colors.green), + ); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Error: ${e.toString()}"), backgroundColor: Colors.redAccent), + ); + } + } finally { + if (mounted) setState(() => _isLoading = false); + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("Change Password")), - body: const Center(child: Text("Change Password Here")), + backgroundColor: const Color(0xFFF4F9F5), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1000), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + children: [ + _buildHeader(), + const SizedBox(height: 80), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + _buildPasswordField("Current Password", _currentPasswordController), + const SizedBox(height: 20), + _buildPasswordField("New Password", _newPasswordController), + const SizedBox(height: 20), + _buildPasswordField("Confirm New Password", _confirmPasswordController, isConfirm: true), + const SizedBox(height: 40), + + // Action Buttons + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ), + child: const Text("Cancel", style: TextStyle(color: Colors.grey)), + ), + ), + const SizedBox(width: 15), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isLoading ? null : _handleChangePassword, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + backgroundColor: Colors.orange, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ), + child: _isLoading + ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Text("Update Password", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + const SizedBox(height: 100), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _buildHeader() { + return Container( + height: 280, + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient(colors: [Color(0xFFE65100), Color(0xFFFF9800)]), + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)), + ), + child: Stack( + children: [ + Positioned(top: 80, left: 30, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text("Security", style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)), + Text("Keep your EcoScan account safe", style: TextStyle(color: Colors.white70, fontSize: 16)), + ], + )), + Positioned( + bottom: -40, + left: 0, + right: 0, + child: Center( + child: Container( + width: 340, + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [BoxShadow(color: Colors.orange.withValues(alpha: 0.2), blurRadius: 20)], + ), + child: Row( + children: const [ + CircleAvatar(backgroundColor: Color(0xFFFFF3E0), child: Icon(Icons.lock_reset, color: Colors.orange)), + SizedBox(width: 15), + Text("Change Password", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildPasswordField(String label, TextEditingController controller, {bool isConfirm = false}) { + return TextFormField( + controller: controller, + obscureText: _obscureText, + decoration: InputDecoration( + labelText: label, + prefixIcon: const Icon(Icons.lock_outline, color: Colors.orange), + suffixIcon: IconButton( + icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility, color: Colors.grey), + onPressed: () => setState(() => _obscureText = !_obscureText), + ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)), + filled: true, + fillColor: Colors.white, + ), + validator: (value) { + if (value == null || value.isEmpty) return "Please enter password"; + if (value.length < 6) return "Password must be at least 6 characters"; + if (isConfirm && value != _newPasswordController.text) return "Passwords do not match"; + return null; + }, ); } } \ No newline at end of file diff --git a/lib/features/home/ui/edit_profile_screen.dart b/lib/features/home/ui/edit_profile_screen.dart index c48c7db..a8a0376 100644 --- a/lib/features/home/ui/edit_profile_screen.dart +++ b/lib/features/home/ui/edit_profile_screen.dart @@ -1,13 +1,274 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; // Required for kIsWeb import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:firebase_storage/firebase_storage.dart'; +import '../controllers/user_controller.dart'; +import '../models/user_model.dart'; -class EditProfileScreen extends StatelessWidget { - const EditProfileScreen({super.key}); +class EditProfileScreen extends ConsumerStatefulWidget { + final UserModel user; + const EditProfileScreen({super.key, required this.user}); + + @override + ConsumerState createState() => _EditProfileScreenState(); +} + +class _EditProfileScreenState extends ConsumerState { + late TextEditingController _nameController; + late TextEditingController _emailController; + XFile? _pickedFile; + bool _isSaving = false; + final ImagePicker _picker = ImagePicker(); + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: widget.user.username); + _emailController = TextEditingController(text: widget.user.email); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + // 📸 IMPROVED PICK IMAGE WITH MODAL BOTTOM SHEET + Future _pickImage() async { + final XFile? image = await showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(25)), + ), + builder: (_) => SafeArea( + child: Wrap( + children: [ + const Padding( + padding: EdgeInsets.all(20), + child: Text("Change Profile Photo", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + ), + ListTile( + leading: const Icon(Icons.photo_library, color: Colors.green), + title: const Text('Choose from Gallery'), + onTap: () async { + final result = await _picker.pickImage( + source: ImageSource.gallery, + imageQuality: 80, + ); + if (mounted) Navigator.pop(context, result); + }, + ), + ListTile( + leading: const Icon(Icons.camera_alt, color: Colors.green), + title: const Text('Take a Photo'), + onTap: () async { + final result = await _picker.pickImage( + source: ImageSource.camera, + imageQuality: 80, + ); + if (mounted) Navigator.pop(context, result); + }, + ), + const SizedBox(height: 10), + ], + ), + ), + ); + + if (image != null) { + setState(() => _pickedFile = image); + } + } + + Future _saveProfile() async { + setState(() => _isSaving = true); + String? newImageUrl; + + try { + if (_pickedFile != null) { + final storageRef = FirebaseStorage.instance.ref().child('profiles/${widget.user.id}.jpg'); + + if (kIsWeb) { + await storageRef.putData(await _pickedFile!.readAsBytes()); + } else { + await storageRef.putFile(File(_pickedFile!.path)); + } + newImageUrl = await storageRef.getDownloadURL(); + } + + await ref.read(userProfileActionsProvider).updateProfile( + username: _nameController.text.trim(), + email: _emailController.text.trim(), + profileUrl: newImageUrl, + ); + + if (mounted) Navigator.pop(context); + } catch (e) { + if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Error: $e"))); + } finally { + if (mounted) setState(() => _isSaving = false); + } + } @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("Edit Profile")), - body: const Center(child: Text("Edit Profile Here")), + backgroundColor: const Color(0xFFF4F9F5), + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + child: Column( + children: [ + _buildHeaderWithPreview(), + const SizedBox(height: 80), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + children: [ + _buildEditField("Username", _nameController, Icons.person_outline), + const SizedBox(height: 20), + _buildEditField("Email Address", _emailController, Icons.email_outlined), + const SizedBox(height: 40), + + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: _isSaving ? null : () => Navigator.pop(context), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + side: const BorderSide(color: Colors.grey), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ), + child: const Text("Cancel", style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold)), + ), + ), + const SizedBox(width: 15), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isSaving ? null : _saveProfile, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 18), + backgroundColor: Colors.green, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + ), + child: _isSaving + ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) + : const Text("Save Changes", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + const SizedBox(height: 100), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildHeaderWithPreview() { + ImageProvider? profileImage; + if (_pickedFile != null) { + profileImage = kIsWeb ? NetworkImage(_pickedFile!.path) : FileImage(File(_pickedFile!.path)) as ImageProvider; + } else if (widget.user.profileurl.isNotEmpty) { + profileImage = NetworkImage(widget.user.profileurl); + } + + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Container( + height: 280, + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient(colors: [Color(0xFF1B5E20), Color(0xFF4CAF50)]), + borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)), + ), + child: const Padding( + padding: EdgeInsets.only(top: 80, left: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Edit Profile", style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)), + Text("Customize your warrior identity", style: TextStyle(color: Colors.white70, fontSize: 16)), + ], + ), + ), + ), + Positioned( + bottom: -50, + child: Container( + width: 340, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [BoxShadow(color: Colors.green.withValues(alpha: 0.2), blurRadius: 20)], + ), + child: Row( + children: [ + GestureDetector( + onTap: _pickImage, + child: Stack( + children: [ + CircleAvatar( + radius: 35, + backgroundColor: Colors.grey[200], + backgroundImage: profileImage, + child: profileImage == null ? const Icon(Icons.person, size: 35) : null, + ), + const Positioned( + bottom: 0, + right: 0, + child: CircleAvatar( + radius: 12, + backgroundColor: Colors.green, + child: Icon(Icons.camera_alt, color: Colors.white, size: 12), + ), + ) + ], + ), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(_nameController.text.isEmpty ? "Username" : _nameController.text, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + Text(_emailController.text, style: const TextStyle(color: Colors.grey, fontSize: 13)), + ], + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildEditField(String label, TextEditingController controller, IconData icon) { + return TextField( + controller: controller, + onChanged: (_) => setState(() {}), + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon, color: Colors.green), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)), + ), ); } } \ No newline at end of file diff --git a/lib/features/home/ui/history_screen.dart b/lib/features/home/ui/history_screen.dart new file mode 100644 index 0000000..77ab000 --- /dev/null +++ b/lib/features/home/ui/history_screen.dart @@ -0,0 +1,270 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../controllers/history_controller.dart'; +import '../controllers/user_controller.dart'; +import '../models/scan_model.dart'; + +class HistoryScreen extends ConsumerWidget { + const HistoryScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final historyAsync = ref.watch(scanHistoryProvider); + final uid = FirebaseAuth.instance.currentUser!.uid; + final userAsync = ref.watch(userControllerProvider(uid)); + + return Scaffold( + backgroundColor: const Color(0xFFF4F9F5), + body: userAsync.when( + loading: () => const Center(child: CircularProgressIndicator(color: Colors.green)), + error: (err, _) => Center(child: Text("User Data Error: $err")), + data: (user) => historyAsync.when( + loading: () => const Center(child: CircularProgressIndicator(color: Colors.green)), + error: (err, _) => Center(child: Text("History Error: $err")), + data: (scans) { + final todayCount = ref.read(scanHistoryProvider.notifier).getTodayScanCount(scans); + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1000), + child: SingleChildScrollView( + child: Column( + children: [ + // Updated Header Section with proper parameters + _buildHeaderSection( + user.ecoPoints, + user.totalScans, + MediaQuery.of(context).size.width, + user.rankTier, + todayCount + ), + const SizedBox(height: 80), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Scanning History", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 15), + + // The Real-time List + if (scans.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Center(child: Text("No scans recorded yet.", style: TextStyle(color: Colors.grey))), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: scans.length, + itemBuilder: (context, index) => _buildHistoryItem(context, scans[index]), + ), + + const SizedBox(height: 100), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } + + Widget _buildHistoryItem(BuildContext context, ScanModel scan) { + final dateStr = DateFormat('dd MMM yyyy, hh:mm a').format(scan.timestamp.toDate()); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: () => _showScanDetails(context, scan), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 10, + offset: const Offset(0, 4) + ) + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + shape: BoxShape.circle + ), + child: const Icon(Icons.history_edu_rounded, color: Colors.green), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(scan.wasteType, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + Text(dateStr, style: const TextStyle(color: Colors.grey, fontSize: 13)), + ], + ), + ), + Text("+${scan.pointsEarned} pts", style: const TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), + ], + ), + ), + ), + ); + } + + void _showScanDetails(BuildContext context, ScanModel scan) { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), + title: Text(scan.wasteType.toUpperCase(), textAlign: TextAlign.center), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: scan.imageUrl.isNotEmpty + ? Image.network(scan.imageUrl, height: 200, width: double.infinity, fit: BoxFit.cover) + : Container(height: 200, color: Colors.grey[200], child: const Icon(Icons.image)), + ), + const SizedBox(height: 20), + _popupRow("Category", scan.category), + _popupRow("CO2 Saved", "${scan.co2Saved} kg"), + _popupRow("Confidence", "${(scan.confidenceScore * 100).toInt()}%"), + ], + ), + ), + ); + } + + Widget _popupRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(color: Colors.grey)), + Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ); + } + + Widget _buildHeaderSection(int points, int scans, double screenWidth, String rank, int count) { + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + // 🟩 The Green Gradient Header + Container( + height: 280, + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1B5E20), Color(0xFF4CAF50)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(40), + bottomRight: Radius.circular(40), + ), + ), + child: Stack( + children: [ + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withAlpha(25))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withAlpha(13))), + + Padding( + padding: const EdgeInsets.only(top: 80, left: 30, right: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "History", + style: TextStyle(color: Colors.white70, fontSize: 16) + ), + const SizedBox(height: 5), + const Text( + "Your journey to a greener Earth 🌿", + style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold) + ), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + "Level: $rank", + style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ], + ), + ), + + // ☁️ The Overlapping Floating Card (Stats) + Positioned( + bottom: -50, + child: Container( + width: screenWidth > 600 ? 500 : 340, + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.green.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10) + ) + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.eco, color: Colors.green), + const SizedBox(width: 10), + Text( + "Today's Scans: $count", + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18) + ), + ], + ), + ), + ), + ], + ); + } + + Widget _circleDeco(double size, Color color) { + return Container( + width: size, + height: size, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ); + } +} \ No newline at end of file diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index d9dbd66..f2c56e6 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -7,6 +7,8 @@ import 'scoreboard_screen.dart'; import 'camera_screen.dart'; import 'analytics_screen.dart'; import 'profiles_screen.dart'; +import 'history_screen.dart'; +import 'centers_screen.dart'; // Controller import '../controllers/user_controller.dart'; @@ -24,7 +26,7 @@ class _HomeScreenState extends ConsumerState { void _onItemTapped(int index) async { // Logout index = 5 - if (index == 5) { + if (index == 7) { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( @@ -66,6 +68,8 @@ class _HomeScreenState extends ConsumerState { const ScoreboardScreen(), const CameraScreen(), const AnalyticsScreen(), + const HistoryScreen(), + const CentersScreen(), const ProfileScreen(), ]; @@ -86,6 +90,8 @@ class _HomeScreenState extends ConsumerState { NavigationDestination(icon: Icon(Icons.emoji_events_rounded), label: 'Ranks'), NavigationDestination(icon: Icon(Icons.camera_enhance_rounded), label: 'Scan'), NavigationDestination(icon: Icon(Icons.insights_rounded), label: 'Impact'), + NavigationDestination(icon: Icon(Icons.history_rounded), label: 'History'), + NavigationDestination(icon: Icon(Icons.map_rounded), label: 'Centers'), NavigationDestination(icon: Icon(Icons.person_rounded), label: 'Profile'), ], ), @@ -108,6 +114,8 @@ class _HomeScreenState extends ConsumerState { NavigationRailDestination(icon: Icon(Icons.emoji_events_rounded), label: Text('Ranks')), NavigationRailDestination(icon: Icon(Icons.camera_enhance_rounded), label: Text('Scan')), NavigationRailDestination(icon: Icon(Icons.insights_rounded), label: Text('Impact')), + NavigationRailDestination(icon: Icon(Icons.history_rounded), label: Text('History')), + NavigationRailDestination(icon: Icon(Icons.map_rounded), label: Text('Centers')), NavigationRailDestination(icon: Icon(Icons.person_rounded), label: Text('Profile')), ], ), @@ -306,13 +314,9 @@ class DashboardTab extends ConsumerWidget { children: [ Expanded(child: _actionButton(Icons.qr_code_scanner, "Scan Now", Colors.blue, () => onSwitchTab(2))), const SizedBox(width: 15), - Expanded(child: _actionButton(Icons.history_rounded, "History", Colors.orange, () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("History coming soon!"))); - })), + Expanded(child: _actionButton(Icons.history_rounded, "History", Colors.orange, () => onSwitchTab(4))), const SizedBox(width: 15), - Expanded(child: _actionButton(Icons.map_rounded, "Centers", Colors.teal, () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Maps coming soon!"))); - })), + Expanded(child: _actionButton(Icons.map_rounded, "Centers", Colors.teal, () => onSwitchTab(5))), ], ); } diff --git a/lib/features/home/ui/profiles_screen.dart b/lib/features/home/ui/profiles_screen.dart index e62357e..8fa2ba9 100644 --- a/lib/features/home/ui/profiles_screen.dart +++ b/lib/features/home/ui/profiles_screen.dart @@ -7,6 +7,7 @@ import '../controllers/user_controller.dart'; import 'package:ecoscan/features/auth/logic/auth_provider.dart'; import 'edit_profile_screen.dart'; import 'change_password_screen.dart'; +import '../models/user_model.dart'; class ProfileScreen extends ConsumerStatefulWidget { const ProfileScreen({super.key}); @@ -19,6 +20,7 @@ class _ProfileScreenState extends ConsumerState { File? _pickedImage; final ImagePicker _picker = ImagePicker(); + Future _pickImage() async { final picked = await showModalBottomSheet( context: context, @@ -117,21 +119,27 @@ class _ProfileScreenState extends ConsumerState { bottomRight: Radius.circular(40), ), ), - child: const Padding( - padding: EdgeInsets.only(top: 80, left: 30), - child: Text( - "My Profile", - style: TextStyle( - color: Colors.white, - fontSize: 28, - fontWeight: FontWeight.bold, - ), - ), + child: Stack( + children:[ + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), + Positioned( + top: 60, + left: 0, + right: 0, + child: Column( + children: [ + const Text("Profile", style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold)), + const SizedBox(height: 20), + ], + ), + ), + ], ), ), Positioned( - bottom: -60, + bottom: -70, child: Container( width: 340, padding: const EdgeInsets.all(25), @@ -267,41 +275,75 @@ class _ProfileScreenState extends ConsumerState { } // ================= BUTTONS ================= - Widget _buildActionButtons(dynamic user) { - return Wrap( - spacing: 20, - runSpacing: 16, - alignment: WrapAlignment.center, - children: [ - _actionButton( - icon: Icons.edit, - label: "Edit Profile", - color: Colors.green, - onTap: () => Navigator.push( - context, MaterialPageRoute(builder: (_) => const EditProfileScreen())), - ), - _actionButton( - icon: Icons.lock, - label: "Change Password", - color: Colors.orange, - onTap: () => Navigator.push( - context, MaterialPageRoute(builder: (_) => const ChangePasswordScreen())), + Widget _buildActionButtons(UserModel user) { // Use the specific model type + return Wrap( + spacing: 20, + runSpacing: 16, + alignment: WrapAlignment.center, + children: [ + // 1. Edit Profile Button + _actionButton( + icon: Icons.edit, + label: "Edit Profile", + color: Colors.green, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + // Pass the 'user' object directly from the parameter + builder: (context) => EditProfileScreen(user: user), + ), + ); + }, + ), + + // 2. Change Password Button + _actionButton( + icon: Icons.lock, + label: "Change Password", + color: Colors.orange, + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const ChangePasswordScreen()) ), - _actionButton( - icon: Icons.logout, - label: "Logout", - color: Colors.redAccent, - width: 200, - onTap: () async { + ), + + // 3. Logout Button + _actionButton( + icon: Icons.logout, + label: "Logout", + color: Colors.redAccent, + width: 200, // Matching your desktop style + onTap: () async { + final confirm = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Logout'), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Logout'), + ), + ], + ), + ); + + if (confirm == true) { await ref.read(authProvider.notifier).logout(); if (mounted) { Navigator.pushNamedAndRemoveUntil(context, '/login', (_) => false); } - }, - ), - ], - ); - } + } + }, + ), + ], + ); +} Widget _actionButton({ required IconData icon, @@ -325,7 +367,7 @@ class _ProfileScreenState extends ConsumerState { ), ); } - Widget _circleDeco(double size, Color color) { + Widget _circleDeco(double size, Color color) { return Container( width: size, height: size, diff --git a/lib/features/home/ui/scoreboard_screen.dart b/lib/features/home/ui/scoreboard_screen.dart index ad5f504..b42e448 100644 --- a/lib/features/home/ui/scoreboard_screen.dart +++ b/lib/features/home/ui/scoreboard_screen.dart @@ -38,7 +38,7 @@ class ScoreboardScreen extends ConsumerWidget { return Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800), + constraints: const BoxConstraints(maxWidth: 1000), child: SingleChildScrollView( child: Column( children: [ @@ -70,7 +70,7 @@ class ScoreboardScreen extends ConsumerWidget { boxShadow: [ BoxShadow( color: - Colors.black.withOpacity(0.03), + Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4), ) @@ -180,12 +180,12 @@ class ScoreboardScreen extends ConsumerWidget { top: -50, right: -50, child: _circleDeco( - 150, Colors.white.withOpacity(0.1))), + 150, Colors.white.withValues(alpha: 0.1))), Positioned( top: 50, left: -20, child: _circleDeco( - 100, Colors.white.withOpacity(0.05))), + 100, Colors.white.withValues(alpha: 0.05))), const Positioned( top: 50, left: 0, @@ -222,7 +222,7 @@ class ScoreboardScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.2), + color: Colors.green.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10)), ], @@ -268,7 +268,7 @@ class ScoreboardScreen extends ConsumerWidget { CircleAvatar( radius: rank == 1 ? 25 : 18, backgroundColor: - isMe ? Colors.green : color.withOpacity(0.2), + isMe ? Colors.green : color.withValues(alpha: 0.2), backgroundImage: user.profileurl.isNotEmpty ? NetworkImage(user.profileurl) : null, @@ -306,8 +306,8 @@ class ScoreboardScreen extends ConsumerWidget { const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: isMe - ? Colors.green.withOpacity(0.3) - : color.withOpacity(0.3), + ? Colors.green.withValues(alpha: 0.3) + : color.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(5)), alignment: Alignment.center, child: Text( diff --git a/lib/features/home/ui/template_screen.dart b/lib/features/home/ui/template_screen.dart index de4d637..f726e99 100644 --- a/lib/features/home/ui/template_screen.dart +++ b/lib/features/home/ui/template_screen.dart @@ -75,8 +75,8 @@ class TemplateScreen extends ConsumerWidget { child: Stack( children: [ // 🫧 Decorative Circles - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), // 🅰️ Header Text const Padding( @@ -105,7 +105,7 @@ class TemplateScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.2), // ❇️ Soft Green Glow + color: Colors.green.withValues(alpha: 0.2), // ❇️ Soft Green Glow blurRadius: 20, offset: const Offset(0, 10), ), @@ -129,7 +129,7 @@ class TemplateScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(20), // 🟢 20px Radius boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), // Super subtle shadow + color: Colors.black.withValues(alpha: 0.03), // Super subtle shadow blurRadius: 10, offset: const Offset(0, 4), ), @@ -141,7 +141,7 @@ class TemplateScreen extends ConsumerWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), // 10% Opacity Background + color: Colors.blue.withValues(alpha: 0.1), // 10% Opacity Background shape: BoxShape.circle, ), child: const Icon(Icons.eco_rounded, color: Colors.blue, size: 28), diff --git a/lib/features/home/ui/text.dart b/lib/features/home/ui/text.txt similarity index 88% rename from lib/features/home/ui/text.dart rename to lib/features/home/ui/text.txt index 0d1a9eb..32d5867 100644 --- a/lib/features/home/ui/text.dart +++ b/lib/features/home/ui/text.txt @@ -54,7 +54,7 @@ class ScoreboardScreen extends StatelessWidget { borderRadius: BorderRadius.circular(20), border: isMe ? Border.all(color: Colors.green, width: 2) : null, boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4)) + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4)) ], ), child: ListTile( @@ -111,8 +111,8 @@ class ScoreboardScreen extends StatelessWidget { ), child: Stack( children: [ - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), // ⚠️ FIXED: Moved text higher up (top: 50) so the card doesn't cover it const Positioned( @@ -143,7 +143,7 @@ class ScoreboardScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ - BoxShadow(color: Colors.green.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.green.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: Row( @@ -169,7 +169,7 @@ class ScoreboardScreen extends StatelessWidget { if (rank == 1) const Icon(Icons.emoji_events, color: Colors.amber, size: 24), CircleAvatar( radius: rank == 1 ? 25 : 18, - backgroundColor: color.withOpacity(0.2), + backgroundColor: color.withValues(alpha: 0.2), child: Text(user['avatar'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Colors.black87)), ), const SizedBox(height: 5), @@ -181,7 +181,7 @@ class ScoreboardScreen extends StatelessWidget { height: rank == 1 ? 60 : (rank == 2 ? 40 : 30), width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 4), - decoration: BoxDecoration(color: color.withOpacity(0.3), borderRadius: BorderRadius.circular(5)), + decoration: BoxDecoration(color: color.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(5)), alignment: Alignment.center, child: Text("$rank", style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 18)), ) @@ -197,4 +197,93 @@ class ScoreboardScreen extends StatelessWidget { decoration: BoxDecoration(shape: BoxShape.circle, color: color), ); } -} \ No newline at end of file +} + + +createdAt +February 19, 2026 at 1:00:58 AM UTC+8 +(timestamp) + + +ecoPoints +5420 +(number) + + +email +"nabil.0413@gmail.com" +(string) + + +lastActive +February 19, 2026 at 1:01:19 AM UTC+8 +(timestamp) + + +rankTier +"Gold" +(string) + + +streak +5 +(number) + + +totalScans +85 +(number) + + +username +"Syed Nabil" +(string) + + +weekId +"2026-W07" +(string) + + +weeklyPoints +320 + + + +category +"plastic" +(string) + + +co2Saved +0.12 +(number) + + +confidenceScore +0.94 +(number) + + +imageUrl +"" +(string) + + +pointsEarned +20 +(number) + + +timestamp +February 19, 2026 at 2:08:15 AM UTC+8 +(timestamp) + + +wasteType +"Plastic Bottle" +(string) + + +weekId +"2026-W07" \ No newline at end of file diff --git a/lib/services/firebase_service.dart b/lib/services/firebase_service.dart index 2019568..0c5960f 100644 --- a/lib/services/firebase_service.dart +++ b/lib/services/firebase_service.dart @@ -1,7 +1,6 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:flutter/material.dart'; class FirebaseService { FirebaseService(); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 64a0ece..7299b5c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2db3c22..786ff5c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 6c7c6a2..5c91395 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,12 +9,18 @@ import cloud_firestore import file_selector_macos import firebase_auth import firebase_core +import firebase_storage +import geolocator_apple import google_sign_in_ios +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) + GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index a450860..78ea355 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -193,6 +201,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + custom_info_window: + dependency: "direct main" + description: + name: custom_info_window + sha256: b7fce9017cb4b8a4319fa7f8080e4fdabfc65996df61c65cd2079dbaf93f56f5 + url: "https://pub.dev" + source: hosted + version: "1.0.1" + dart_earcut: + dependency: transitive + description: + name: dart_earcut + sha256: e485001bfc05dcbc437d7bfb666316182e3522d4c3f9668048e004d0eb2ce43b + url: "https://pub.dev" + source: hosted + version: "1.2.0" + dart_polylabel2: + dependency: transitive + description: + name: dart_polylabel2 + sha256: "7eeab15ce72894e4bdba6a8765712231fc81be0bd95247de4ad9966abc57adc6" + url: "https://pub.dev" + source: hosted + version: "1.0.0" fake_async: dependency: transitive description: @@ -297,6 +329,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.0" + firebase_storage: + dependency: "direct main" + description: + name: firebase_storage + sha256: d6a0ed2ecb860cbf7537fc94773926036952e89a6ef5b6714673c9d1cb741baa + url: "https://pub.dev" + source: hosted + version: "13.0.6" + firebase_storage_platform_interface: + dependency: transitive + description: + name: firebase_storage_platform_interface + sha256: "99032301dd8ac20acd094270a4472ff8e9510701d9c6ed335df0b91a3ddf4eac" + url: "https://pub.dev" + source: hosted + version: "5.2.17" + firebase_storage_web: + dependency: transitive + description: + name: firebase_storage_web + sha256: d820ddb9419d563f398c85a3c7ad6255af28c3bf9d53d9af7b346b872f54a901 + url: "https://pub.dev" + source: hosted + version: "3.11.2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -318,6 +382,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + sha256: "391e7dc95cc3f5190748210a69d4cfeb5d8f84dcdfa9c3235d0a9d7742ccb3f8" + url: "https://pub.dev" + source: hosted + version: "8.2.2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -352,6 +424,54 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + url: "https://pub.dev" + source: hosted + version: "10.1.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.dev" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" glob: dependency: transitive description: @@ -376,6 +496,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.3+1" + google_maps: + dependency: transitive + description: + name: google_maps + sha256: "5d410c32112d7c6eb7858d359275b2aa04778eed3e36c745aeae905fb2fa6468" + url: "https://pub.dev" + source: hosted + version: "8.2.0" + google_maps_flutter: + dependency: "direct main" + description: + name: google_maps_flutter + sha256: "9b0d6dab3de6955837575dc371dd772fcb5d0a90f6a4954e8c066472f9938550" + url: "https://pub.dev" + source: hosted + version: "2.14.2" + google_maps_flutter_android: + dependency: transitive + description: + name: google_maps_flutter_android + sha256: "8b569c7abc52bc62d4502bf93847d487c0843f3e6a2a8e122b72e98843b2ab4c" + url: "https://pub.dev" + source: hosted + version: "2.19.1" + google_maps_flutter_ios: + dependency: transitive + description: + name: google_maps_flutter_ios + sha256: "174d730bc3f253e1c06a342d7a5efb216f15003a6e26693c2d70d60973625af4" + url: "https://pub.dev" + source: hosted + version: "2.17.5" + google_maps_flutter_platform_interface: + dependency: transitive + description: + name: google_maps_flutter_platform_interface + sha256: "0f8c6674d70c7e9a09cd34f63b18ebaf8a5822e85b558128eae0fdf02b4a3e93" + url: "https://pub.dev" + source: hosted + version: "2.14.2" + google_maps_flutter_web: + dependency: transitive + description: + name: google_maps_flutter_web + sha256: d416602944e1859f3cbbaa53e34785c223fa0a11eddb34a913c964c5cbb5d8cf + url: "https://pub.dev" + source: hosted + version: "0.5.14+3" google_sign_in: dependency: "direct main" description: @@ -424,6 +592,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" http: dependency: transitive description: @@ -512,6 +688,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -520,6 +704,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + latlong2: + dependency: "direct main" + description: + name: latlong2 + sha256: "98227922caf49e6056f91b6c56945ea1c7b166f28ffcd5fb8e72fc0b453cc8fe" + url: "https://pub.dev" + source: hosted + version: "0.9.1" leak_tracker: dependency: transitive description: @@ -552,6 +744,22 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + lists: + dependency: transitive + description: + name: lists + sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" logging: dependency: transitive description: @@ -584,6 +792,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + sha256: fb89ae62f05fa0bb90f70c31fc870bcbcfd516c843fb554452ab3396f78586f7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -752,6 +968,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.2" + proj4dart: + dependency: transitive + description: + name: proj4dart + sha256: c8a659ac9b6864aa47c171e78d41bbe6f5e1d7bd790a5814249e6b68bc44324e + url: "https://pub.dev" + source: hosted + version: "2.1.0" pub_semver: dependency: transitive description: @@ -768,6 +992,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + sanitize_html: + dependency: transitive + description: + name: sanitize_html + sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" + url: "https://pub.dev" + source: hosted + version: "2.1.0" shelf: dependency: transitive description: @@ -909,6 +1141,86 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + unicode: + dependency: transitive + description: + name: unicode + sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.dev" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + url: "https://pub.dev" + source: hosted + version: "2.4.2" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" vector_math: dependency: transitive description: @@ -965,6 +1277,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + sha256: "8a555fc60de3116c00aad67891bcab20f81a958e4219cc106e3c037aa3937f13" + url: "https://pub.dev" + source: hosted + version: "2.0.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5ae6d52..ed88eee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,14 @@ dependencies: path: ^1.9.1 permission_handler: ^12.0.1 image_picker: ^1.1.1 + intl: ^0.20.2 + google_maps_flutter: ^2.5.3 + geolocator: ^10.1.0 + custom_info_window: ^1.0.1 + url_launcher: ^6.3.2 + firebase_storage: ^13.0.6 + flutter_map: ^8.2.2 + latlong2: ^0.9.1 dev_dependencies: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3c09f24..251170e 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,7 +10,10 @@ #include #include #include +#include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { CloudFirestorePluginCApiRegisterWithRegistrar( @@ -21,6 +24,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FirebaseStoragePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); + GeolocatorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("GeolocatorWindows")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f570661..a256365 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,7 +7,10 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows firebase_auth firebase_core + firebase_storage + geolocator_windows permission_handler_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST