diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 8c3f905..ab5281f 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -13,6 +13,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -46,7 +47,7 @@ void main() async { FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - runApp(const MyApp()); + runApp(const ProviderScope(child: MyApp())); } Future _initializeNotifications() async { diff --git a/frontend/lib/providers/timetable_provider.dart b/frontend/lib/providers/timetable_provider.dart new file mode 100644 index 0000000..e8a4846 --- /dev/null +++ b/frontend/lib/providers/timetable_provider.dart @@ -0,0 +1,104 @@ +import 'package:dashbaord/models/lecture_model.dart'; +import 'package:dashbaord/models/time_table_model.dart'; +import 'package:dashbaord/services/api_service.dart'; +import 'package:dashbaord/services/shared_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// Provider for timetable state management +final timetableProvider = StateNotifierProvider>((ref) { + return TimetableNotifier(); +}); + +/// Notifier class for managing timetable state +class TimetableNotifier extends StateNotifier> { + TimetableNotifier() : super(const AsyncValue.loading()); + + /// Fetch timetable from API or local storage + Future fetchTimetable(BuildContext context, {bool isGuest = false}) async { + try { + state = const AsyncValue.loading(); + + Timetable? localTimetable = await SharedService().getTimetable(); + Timetable? response; + + if (!isGuest) { + response = await ApiServices().getTimetable(context); + } + + if (response == null) { + if (localTimetable == null) { + state = AsyncValue.data(Timetable(courses: {}, slots: [])); + return; + } else { + localTimetable.cleanUp(); + state = AsyncValue.data(localTimetable); + return; + } + } else { + response.cleanUp(); + state = AsyncValue.data(response); + await SharedService().saveTimetable(response); + } + } catch (e, stackTrace) { + state = AsyncValue.error(e, stackTrace); + } + } + + /// Add a course to the timetable + Future addCourse({ + required String courseCode, + required String courseName, + required List lectures, + String? classRoom, + String? slot, + }) async { + final currentTimetable = state.value; + if (currentTimetable == null) return; + + try { + final updatedTimetable = currentTimetable.addCourse( + courseCode, + courseName, + lectures, + classRoom: classRoom, + slot: slot, + ); + + state = AsyncValue.data(updatedTimetable); + + // Save to backend and local storage + final res = await ApiServices().postTimetable(updatedTimetable); + if (res['status'] == 200) { + await SharedService().saveTimetable(updatedTimetable); + } + } catch (e, stackTrace) { + state = AsyncValue.error(e, stackTrace); + } + } + + /// Update the entire timetable + Future updateTimetable(Timetable timetable) async { + try { + state = AsyncValue.data(timetable); + + // Save to backend and local storage + final res = await ApiServices().postTimetable(timetable); + if (res['status'] == 200) { + await SharedService().saveTimetable(timetable); + } + } catch (e, stackTrace) { + state = AsyncValue.error(e, stackTrace); + } + } + + /// Set timetable directly (useful for shared timetables) + void setTimetable(Timetable? timetable) { + state = AsyncValue.data(timetable); + } + + /// Clear timetable + void clearTimetable() { + state = AsyncValue.data(Timetable(courses: {}, slots: [])); + } +} diff --git a/frontend/lib/screens/calendar_screen.dart b/frontend/lib/screens/calendar_screen.dart index 99373a5..50be627 100644 --- a/frontend/lib/screens/calendar_screen.dart +++ b/frontend/lib/screens/calendar_screen.dart @@ -1,6 +1,7 @@ import 'package:dashbaord/extensions.dart'; import 'package:dashbaord/models/lecture_model.dart'; import 'package:dashbaord/models/time_table_model.dart'; +import 'package:dashbaord/providers/timetable_provider.dart'; import 'package:dashbaord/services/api_service.dart'; import 'package:dashbaord/services/shared_service.dart'; import 'package:dashbaord/widgets/custom_appbar.dart'; @@ -16,11 +17,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart'; -class CalendarScreen extends StatefulWidget { +class CalendarScreen extends ConsumerStatefulWidget { final Timetable? timetable; final Function(String, String, List, String?, String?)? onLectureAdded; @@ -34,13 +36,12 @@ class CalendarScreen extends StatefulWidget { }); @override - State createState() => _CalendarScreenState(); + ConsumerState createState() => _CalendarScreenState(); } -class _CalendarScreenState extends State { +class _CalendarScreenState extends ConsumerState { String selectedViewType = "List"; List viewTypeList = ["List", "Day", "Week", "Month"]; - Timetable? timetable; DateTime? initialDate = DateTime.now(); final List _events = []; @@ -98,7 +99,6 @@ class _CalendarScreenState extends State { @override void initState() { super.initState(); - timetable = widget.timetable; WidgetsBinding.instance.addPostFrameCallback((_) { requestNotifPerms(context); }); @@ -106,6 +106,8 @@ class _CalendarScreenState extends State { @override Widget build(BuildContext context) { + final timetableAsyncValue = ref.watch(timetableProvider); + return CalendarControllerProvider( controller: EventController()..addAll(_events), child: Scaffold( @@ -118,15 +120,10 @@ class _CalendarScreenState extends State { iconColor: context.customColors.customAccentColor, onSelected: (value) async { if (value == "refresh") { - final response = await ApiServices().getTimetable(context); - if (response == null) { + await ref.read(timetableProvider.notifier).fetchTimetable(context); + if (ref.read(timetableProvider).hasError) { showError(msg: "Failed to fetch timetable"); - return; } - setState(() { - timetable = response; - }); - SharedService().saveTimetable(response); } else if (value == "manageCourses") { _showManageCoursesBottomSheet(context); } else if (value == "shareCode") { @@ -178,61 +175,65 @@ class _CalendarScreenState extends State { ) ], ), - body: Column( - children: [ - const SizedBox( - height: 2, - ), - ToggleButtons( - isSelected: viewTypeList - .map((viewType) => viewType == selectedViewType) - .toList(), - onPressed: (int index) { - setState(() { - selectedViewType = viewTypeList[index]; - }); - }, - borderRadius: BorderRadius.circular(10), - selectedColor: Colors.white, - fillColor: context.customColors.customAccentColor, - constraints: BoxConstraints( - minHeight: 40.0, - minWidth: MediaQuery.of(context).size.width * 2 / 9, + body: timetableAsyncValue.when( + data: (timetable) => Column( + children: [ + const SizedBox( + height: 2, ), - children: viewTypeList.map((String viewType) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - viewType, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: viewType == selectedViewType - ? Colors.white - : Theme.of(context).textTheme.bodyLarge?.color, + ToggleButtons( + isSelected: viewTypeList + .map((viewType) => viewType == selectedViewType) + .toList(), + onPressed: (int index) { + setState(() { + selectedViewType = viewTypeList[index]; + }); + }, + borderRadius: BorderRadius.circular(10), + selectedColor: Colors.white, + fillColor: context.customColors.customAccentColor, + constraints: BoxConstraints( + minHeight: 40.0, + minWidth: MediaQuery.of(context).size.width * 2 / 9, + ), + children: viewTypeList.map((String viewType) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + viewType, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: viewType == selectedViewType + ? Colors.white + : Theme.of(context).textTheme.bodyLarge?.color, + ), ), - ), - ); - }).toList(), - ), - const SizedBox(height: 10), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _getCurrentView(context), + ); + }).toList(), ), - ), - SizedBox( - height: 10, - ) - ], + const SizedBox(height: 10), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _getCurrentView(context, timetable), + ), + ), + SizedBox( + height: 10, + ) + ], + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error loading timetable')), ), floatingActionButton: _buildFABs(), ), ); } - Widget _getCurrentView(BuildContext context) { + Widget _getCurrentView(BuildContext context, Timetable? timetable) { switch (selectedViewType) { case "List": return ListViewScreen( @@ -267,16 +268,20 @@ class _CalendarScreenState extends State { } void _showAddEventBottomSheet(BuildContext context) { + final timetable = ref.read(timetableProvider).value; showModalBottomSheet( context: context, builder: (context) { return AddLectureBottomSheet( timetable: timetable, - onLectureAdded: (courseCode, courseName, lectures, classRoom, slot, segment) { - setState(() { - timetable = - timetable!.addCourse(courseCode, courseName, lectures); - }); + onLectureAdded: (courseCode, courseName, lectures, classRoom, slot, segment) async { + await ref.read(timetableProvider.notifier).addCourse( + courseCode: courseCode, + courseName: courseName, + lectures: lectures, + classRoom: classRoom, + slot: slot, + ); widget.onLectureAdded!( courseCode, courseName, lectures, classRoom, slot); }, @@ -288,15 +293,14 @@ class _CalendarScreenState extends State { } void _showManageCoursesBottomSheet(BuildContext context) { + final timetable = ref.read(timetableProvider).value; showModalBottomSheet( context: context, builder: (context) { return ManageCoursesBottomSheet( timetable: timetable, - onEditTimetable: (editedTimetable) { - setState(() { - timetable = editedTimetable; - }); + onEditTimetable: (editedTimetable) async { + await ref.read(timetableProvider.notifier).updateTimetable(editedTimetable); widget.onEditTimetable!(editedTimetable); }, ); @@ -324,7 +328,13 @@ class _CalendarScreenState extends State { } void _shareSchedule() async { - final courseDetails = timetable!.courses.entries.map((entry) { + final timetable = ref.read(timetableProvider).value; + if (timetable == null) { + showError(msg: "No timetable to share"); + return; + } + + final courseDetails = timetable.courses.entries.map((entry) { final code = entry.key; final name = entry.value['title']; return '$code: $name'; diff --git a/frontend/lib/screens/home_screen.dart b/frontend/lib/screens/home_screen.dart index 34e0e94..2ec90e3 100644 --- a/frontend/lib/screens/home_screen.dart +++ b/frontend/lib/screens/home_screen.dart @@ -6,6 +6,7 @@ import 'package:dashbaord/main.dart'; import 'package:dashbaord/models/mess_menu_model.dart'; import 'package:dashbaord/models/time_table_model.dart'; import 'package:dashbaord/models/user_model.dart'; +import 'package:dashbaord/providers/timetable_provider.dart'; import 'package:dashbaord/services/analytics_service.dart'; import 'package:dashbaord/services/api_service.dart'; import 'package:dashbaord/services/event_notification_service.dart'; @@ -25,13 +26,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:home_widget/home_widget.dart'; import 'package:in_app_update/in_app_update.dart'; import 'package:text_scroll/text_scroll.dart'; -class HomeScreen extends StatefulWidget { +class HomeScreen extends ConsumerStatefulWidget { final bool isGuest; final ValueChanged onThemeChanged; final String? code; @@ -42,10 +44,10 @@ class HomeScreen extends StatefulWidget { this.code}); @override - State createState() => _HomeScreenState(); + ConsumerState createState() => _HomeScreenState(); } -class _HomeScreenState extends State { +class _HomeScreenState extends ConsumerState { final GlobalKey _scaffoldKey = GlobalKey(); final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; @@ -66,7 +68,6 @@ class _HomeScreenState extends State { bool isLoading = true; String image = ''; int mainGateStatus = -1; - Timetable? timetable; void sendTokenToServer(String token, String deviceType) async { final response = @@ -179,40 +180,7 @@ class _HomeScreenState extends State { await SharedService().saveBusSchedule(response); } - Future fetchTimetable() async { - Timetable? localTimetable = await SharedService().getTimetable(); - Timetable? response; - if (!widget.isGuest) { - response = await ApiServices().getTimetable(context); - } - if (response == null) { - if (localTimetable == null) { - showError(msg: "Timetable not found. Please add courses."); - setState(() { - timetable = Timetable(courses: {}, slots: []); - changeState(); - }); - return; - } else { - showError(msg: "Timetable Server refresh failed..."); - localTimetable.cleanUp(); - setState(() { - timetable = localTimetable; - changeState(); - }); - return; - } - } else { - response.cleanUp(); - setState(() { - timetable = response; - changeState(); - }); - - await SharedService().saveTimetable(response); - } - } Future fetchUser() async { final response = await ApiServices().getUserDetails(context); @@ -311,7 +279,10 @@ class _HomeScreenState extends State { } fetchMessMenu(); fetchBus(); - fetchTimetable(); + // Fetch timetable using Riverpod provider + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(timetableProvider.notifier).fetchTimetable(context, isGuest: widget.isGuest); + }); setUpFirebaseMessaging(); analyticsService.logScreenView(screenName: "HomeScreen"); // Initialize the controller properly @@ -328,7 +299,8 @@ class _HomeScreenState extends State { } fetchMessMenu(); fetchBus(); - fetchTimetable(); + // Refresh timetable using Riverpod provider + await ref.read(timetableProvider.notifier).fetchTimetable(context, isGuest: widget.isGuest); getMainGateStatus(); } @@ -433,32 +405,22 @@ class _HomeScreenState extends State { response[1] as int; // Assuming status is in response[1] String message = response[2] as String; if (status == 200) { + final timetable = ref.read(timetableProvider).value; showModalBottomSheet( context: context, builder: (context) { return ManageCoursesBottomSheet( timetable: timetable, onEditTimetable: (editedTimetable) async { - setState(() { - timetable = editedTimetable; - }); - final res = - await ApiServices().postTimetable(timetable!); - if (res['status'] != 200) { - showError(msg: "Failed to save timetable."); - } else { - showError(msg: "Timetable saved successfully!"); - await SharedService().saveTimetable(timetable!); - } + await ref.read(timetableProvider.notifier).updateTimetable(editedTimetable); + showError(msg: "Timetable saved successfully!"); }, isAddCourses: true, ); }, isScrollControlled: true, ); - setState(() { - timetable = sharedTimetable; - }); + ref.read(timetableProvider.notifier).setTimetable(sharedTimetable); showError(msg: "Timetable accepted successfully!"); } else { showError( @@ -483,6 +445,8 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { timeDilation = 1; + final timetableAsyncValue = ref.watch(timetableProvider); + return Scaffold( appBar: AppBar( toolbarHeight: 0.0, @@ -533,53 +497,38 @@ class _HomeScreenState extends State { selectable: true, ), const SizedBox(height: 28), - HomeScreenSchedule( - timetable: timetable, - onEditTimetable: (editedTimetable) async { - setState( - () { - timetable = editedTimetable; - }, - ); - final res = - await ApiServices().postTimetable(timetable!); - if (res['status'] != 200) { - showError(msg: "Failed to save timetable."); - } else { + timetableAsyncValue.when( + data: (timetable) => HomeScreenSchedule( + timetable: timetable, + onEditTimetable: (editedTimetable) async { + await ref.read(timetableProvider.notifier).updateTimetable(editedTimetable); showError(msg: "Timetable saved successfully!"); - await SharedService().saveTimetable(timetable!); clearAllNotifications(); EventNotificationService .scheduleWeeklyNotifications( - timetable: timetable!); - } - }, - onLectureAdded: (courseCode, courseName, lectures, - String? classRoom, String? slot) async { - if (timetable != null) { - setState( - () { - timetable = timetable!.addCourse( - courseCode, courseName, lectures, - classRoom: classRoom, slot: slot); - }, + timetable: editedTimetable); + }, + onLectureAdded: (courseCode, courseName, lectures, + String? classRoom, String? slot) async { + await ref.read(timetableProvider.notifier).addCourse( + courseCode: courseCode, + courseName: courseName, + lectures: lectures, + classRoom: classRoom, + slot: slot, ); - final res = await ApiServices() - .postTimetable(timetable!); - if (res['status'] != 200) { - showError(msg: "Failed to save timetable."); - } else { - showError( - msg: "Timetable saved successfully!"); - await SharedService() - .saveTimetable(timetable!); + final updatedTimetable = ref.read(timetableProvider).value; + if (updatedTimetable != null) { + showError(msg: "Timetable saved successfully!"); clearAllNotifications(); EventNotificationService .scheduleWeeklyNotifications( - timetable: timetable!); + timetable: updatedTimetable); } - } - }, + }, + ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error loading timetable')), ), const SizedBox(height: 15), HomeScreenBusTimings( diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 36eee28..d407df1 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -81,6 +81,7 @@ dependencies: flutter_markdown: ^0.7.6+2 geolocator: ^14.0.2 app_settings: ^6.1.1 + flutter_riverpod: ^2.6.1 dev_dependencies: flutter_test: